dnscrawl.go 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304
  1. package main
  2. import (
  3. "bytes"
  4. "crypto/tls"
  5. "database/sql"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "log"
  10. "net"
  11. "net/http"
  12. "net/url"
  13. "os"
  14. "strings"
  15. "sync"
  16. "sync/atomic"
  17. "time"
  18. "github.com/PuerkitoBio/goquery"
  19. _ "github.com/lib/pq"
  20. "github.com/miekg/dns"
  21. "golang.org/x/net/html/charset"
  22. )
  23. // --- CONFIGURATION STRUCTURES ---
  24. type DBConfig struct {
  25. Host string `json:"host"`
  26. Port string `json:"port"`
  27. User string `json:"user"`
  28. Password string `json:"password"`
  29. DBName string `json:"dbname"`
  30. SSLMode string `json:"sslmode"`
  31. }
  32. type CFCredential struct {
  33. Comment string `json:"comment"`
  34. AccountID string `json:"account_id"`
  35. APIToken string `json:"api_token"`
  36. Model string `json:"model"`
  37. }
  38. type AppConfig struct {
  39. Database DBConfig `json:"database"`
  40. DBConnLegacy string `json:"db_conn,omitempty"`
  41. StartServers []string `json:"start_servers"`
  42. ZoneNames []string `json:"zone_names"`
  43. MaxDepth int `json:"max_depth"`
  44. MaxWorkers int `json:"max_workers"`
  45. MaxPagesPerHost int `json:"max_pages_per_host"`
  46. WebUIPort string `json:"web_ui_port"`
  47. ScheduleHours int `json:"schedule_hours"`
  48. ScheduleMinutes int `json:"schedule_minutes"`
  49. ScanOnStart bool `json:"scan_on_start"`
  50. CoreServices []string `json:"core_services"`
  51. CFCredentials []CFCredential `json:"cf_credentials"`
  52. AIPrompt string `json:"ai_prompt"`
  53. }
  54. func (c DBConfig) GetConnString() string {
  55. return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", c.Host, c.Port, c.User, c.Password, c.DBName, c.SSLMode)
  56. }
  57. // --- GLOBAL VARIABLES ---
  58. var (
  59. cfg AppConfig
  60. cfgMutex sync.RWMutex
  61. dbGlobal *sql.DB
  62. daemonEnabled bool = true
  63. isScanning bool
  64. stopRequested int32
  65. lastRun time.Time
  66. nextRunTime time.Time
  67. stateMutex sync.Mutex
  68. visitedZones sync.Map
  69. visitedWeb sync.Map
  70. hostVisitCount sync.Map
  71. sitesOffline uint64
  72. sitesActive uint64
  73. sitesSkipped uint64
  74. mediaFound uint64
  75. spiderPending int32
  76. aiPending int32
  77. spiderQueue = make(chan SpiderJob, 300000)
  78. aiQueue = make(chan AIJob, 100000)
  79. globalStats = &StatsManager{CategoryCounts: make(map[string]int)}
  80. forceLookups = []string{
  81. "minecraft.maiden2", // ΝΕΟ: Αναγκάζει να ψάξει αυτό πρώτο!
  82. "routemap", "activeservices", "router", "stats", "www", "node", "auth",
  83. "camera", "weather", "voip", "irc", "matrix", "speedtest", "proxy",
  84. "mirror", "archive", "git", "cloud", "files", "game", "mc", "ts3",
  85. "mumble", "ntp", "dns", "mail", "iot", "sensor", "radio", "tv", "stream",
  86. "torrent", "tracker", "portal", "p2p", "dcplusplus", "hub", "search",
  87. "forum", "board", "bb", "vbulletin", "smf", "phpbb", "discuss",
  88. }
  89. httpClient = &http.Client{
  90. Timeout: 8 * time.Second,
  91. Transport: &http.Transport{
  92. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  93. MaxIdleConns: 1000,
  94. MaxIdleConnsPerHost: 100,
  95. },
  96. }
  97. aiClient = &http.Client{Timeout: 30 * time.Second}
  98. logBuffer []string
  99. logMutex sync.Mutex
  100. )
  101. type SpiderJob struct {
  102. RecordID int
  103. CurrentURL string
  104. BaseHost string
  105. Depth int
  106. }
  107. type AIJob struct {
  108. URL string
  109. Title string
  110. MetaDesc string
  111. Generator string
  112. H1 string
  113. Preview string
  114. }
  115. type AIAnalysis struct {
  116. Category string `json:"category"`
  117. Summary string `json:"summary"`
  118. Status string `json:"status"`
  119. }
  120. type StatsManager struct {
  121. sync.Mutex
  122. CategoryCounts map[string]int
  123. }
  124. func (s *StatsManager) Update(category string) {
  125. s.Lock()
  126. defer s.Unlock()
  127. s.CategoryCounts[category]++
  128. }
  129. func (s *StatsManager) Reset() {
  130. s.Lock()
  131. defer s.Unlock()
  132. s.CategoryCounts = make(map[string]int)
  133. }
  134. func logMsg(format string, a ...interface{}) {
  135. msg := fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05"), fmt.Sprintf(format, a...))
  136. // 1. Τυπώνει στην οθόνη (Terminal)
  137. fmt.Println(msg)
  138. logMutex.Lock()
  139. // 2. Το κρατάει στη μνήμη για το Web UI (Dashboard)
  140. logBuffer = append(logBuffer, msg)
  141. if len(logBuffer) > 300 { logBuffer = logBuffer[1:] }
  142. // 3. ΝΕΟ: Το γράφει μόνιμα στο αρχείο /var/log/wisearch-crawler.log
  143. f, err := os.OpenFile("/var/log/wisearch-crawler.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
  144. if err == nil {
  145. f.WriteString(msg + "\n")
  146. f.Close()
  147. } else {
  148. // Αν δεν έχει δικαιώματα, το τυπώνει στην οθόνη για να το δεις
  149. fmt.Printf("[⚠️ FILE ERROR] Δεν μπορώ να γράψω στο log file: %v\n", err)
  150. }
  151. logMutex.Unlock()
  152. }
  153. func loadConfig() {
  154. cfgMutex.Lock()
  155. defer cfgMutex.Unlock()
  156. defaultPrompt := "You are an AWMN web classifier. Return ONLY JSON. Do not include markdown.\nCategories: \"Router & Admin\", \"Network Tools & Stats\", \"Media & Streaming\", \"P2P & Torrents\", \"Storage & FTP\", \"Forums & Communities\", \"Communication & VoIP\", \"Gaming\", \"IoT & Sensors\", \"Web Services & Blogs\", \"Default Page\", \"Other\".\nFormat: {\"category\": \"Exact category\", \"summary\": \"Short Greek summary, max 8 words\", \"status\": \"Active\"}"
  157. data, err := os.ReadFile("config.json")
  158. if err != nil {
  159. logMsg("[⚠️ SYSTEM] config.json not found. Creating default configuration.")
  160. cfg = AppConfig{
  161. Database: DBConfig{Host: "192.168.131.124", Port: "5432", User: "postgres", Password: "postpass", DBName: "wisearch", SSLMode: "disable"},
  162. StartServers: []string{"192.168.131.98"},
  163. ZoneNames: []string{"awmn."},
  164. MaxDepth: 50,
  165. MaxWorkers: 60,
  166. MaxPagesPerHost: 10000,
  167. WebUIPort: ":8080",
  168. ScheduleHours: 12,
  169. ScheduleMinutes: 0,
  170. ScanOnStart: false,
  171. AIPrompt: defaultPrompt,
  172. CoreServices: []string{}, // Άδειο από default
  173. CFCredentials: []CFCredential{}, // Άδειο από default
  174. }
  175. saveConfigInternal()
  176. return
  177. }
  178. json.Unmarshal(data, &cfg)
  179. if cfg.AIPrompt == "" {
  180. cfg.AIPrompt = defaultPrompt
  181. }
  182. logMsg("[🔧 SYSTEM] Configuration loaded.")
  183. }
  184. func saveConfigInternal() {
  185. cfg.DBConnLegacy = ""
  186. data, _ := json.MarshalIndent(cfg, "", " ")
  187. os.WriteFile("config.json", data, 0644)
  188. }
  189. func isCoreService(hostname string) bool {
  190. cfgMutex.RLock()
  191. defer cfgMutex.RUnlock()
  192. for _, svc := range cfg.CoreServices {
  193. if svc == hostname { return true }
  194. }
  195. return false
  196. }
  197. func main() {
  198. loadConfig()
  199. logMsg("[🚀 SYSTEM] WiSearch Daemon Initialized | C&C Mode")
  200. var err error
  201. dbGlobal, err = sql.Open("postgres", cfg.Database.GetConnString())
  202. if err != nil { log.Fatal("[❌ FATAL] Database connection string failed:", err) }
  203. if err = dbGlobal.Ping(); err != nil {
  204. logMsg("[❌ FATAL DB ERROR] Η Βάση Δεδομένων δεν απαντάει! Έλεγξε IP/Κωδικό: %v", err)
  205. } else {
  206. logMsg("[✅ SYSTEM] Database connection successful.")
  207. }
  208. defer dbGlobal.Close()
  209. dbGlobal.SetMaxOpenConns(150)
  210. dbGlobal.SetMaxIdleConns(75)
  211. go startWebUI()
  212. cfgMutex.RLock()
  213. workers := cfg.MaxWorkers
  214. onStart := cfg.ScanOnStart
  215. cfgMutex.RUnlock()
  216. for i := 0; i < workers; i++ { go spiderWorker() }
  217. for i := 0; i < 4; i++ { go aiWorker() }
  218. calcNextRun()
  219. go schedulerLoop()
  220. go watchdogLoop()
  221. if onStart {
  222. logMsg("[▶️ SYSTEM] ScanOnStart is enabled. Triggering immediate scan.")
  223. triggerScan()
  224. }
  225. select {}
  226. }
  227. func calcNextRun() {
  228. cfgMutex.RLock()
  229. h, m := cfg.ScheduleHours, cfg.ScheduleMinutes
  230. cfgMutex.RUnlock()
  231. stateMutex.Lock()
  232. defer stateMutex.Unlock()
  233. if h == 0 && m == 0 {
  234. nextRunTime = time.Time{}
  235. return
  236. }
  237. interval := time.Duration(h)*time.Hour + time.Duration(m)*time.Minute
  238. now := time.Now()
  239. if lastRun.IsZero() { lastRun = now }
  240. nextRunTime = lastRun.Add(interval)
  241. if nextRunTime.Before(now) {
  242. lastRun = now
  243. nextRunTime = now.Add(interval)
  244. }
  245. }
  246. func triggerScan() {
  247. stateMutex.Lock()
  248. if isScanning {
  249. stateMutex.Unlock()
  250. logMsg("[⚠️ SYSTEM] Scan is already running. Ignoring trigger.")
  251. return
  252. }
  253. isScanning = true
  254. lastRun = time.Now()
  255. stateMutex.Unlock()
  256. calcNextRun()
  257. atomic.StoreInt32(&stopRequested, 0)
  258. logMsg("[🔄 SYSTEM] Initiating New Multi-Zone Scan Cycle...")
  259. visitedZones = sync.Map{}
  260. visitedWeb = sync.Map{}
  261. hostVisitCount = sync.Map{}
  262. atomic.StoreUint64(&sitesActive, 0)
  263. atomic.StoreUint64(&sitesOffline, 0)
  264. atomic.StoreUint64(&sitesSkipped, 0)
  265. atomic.StoreUint64(&mediaFound, 0)
  266. globalStats.Reset()
  267. cfgMutex.RLock()
  268. zones := cfg.ZoneNames
  269. servers := cfg.StartServers
  270. cfgMutex.RUnlock()
  271. for _, z := range zones {
  272. for _, s := range servers {
  273. atomic.AddInt32(&spiderPending, 1)
  274. go fetchAndStoreDNS(z, s)
  275. }
  276. }
  277. go processSubmittedURLs()
  278. }
  279. func stopScan() {
  280. atomic.StoreInt32(&stopRequested, 1)
  281. logMsg("[🛑 SYSTEM] Emergency Stop Requested! Draining queues...")
  282. }
  283. func schedulerLoop() {
  284. for {
  285. time.Sleep(10 * time.Second)
  286. stateMutex.Lock()
  287. scanning := isScanning
  288. enabled := daemonEnabled
  289. target := nextRunTime
  290. stateMutex.Unlock()
  291. if !enabled || scanning || target.IsZero() { continue }
  292. if time.Now().After(target) {
  293. logMsg("[⏰ SCHEDULE] Scheduled time reached. Starting scan.")
  294. triggerScan()
  295. }
  296. }
  297. }
  298. func watchdogLoop() {
  299. idleSeconds := 0
  300. for {
  301. time.Sleep(5 * time.Second)
  302. stateMutex.Lock()
  303. scanning := isScanning
  304. stateMutex.Unlock()
  305. if !scanning { continue }
  306. sPend := atomic.LoadInt32(&spiderPending)
  307. aPend := atomic.LoadInt32(&aiPending)
  308. stopped := atomic.LoadInt32(&stopRequested) == 1
  309. if sPend == 0 && aPend == 0 && len(spiderQueue) == 0 && len(aiQueue) == 0 {
  310. idleSeconds += 5
  311. if idleSeconds >= 15 || stopped {
  312. if stopped {
  313. logMsg("[🛑 SYSTEM] Scan aborted successfully.")
  314. } else {
  315. logMsg("[✅ SYSTEM] Scan Cycle Complete!")
  316. logMsg("[🔄 SYSTEM] Updating PageRank scores...")
  317. updatePageRank()
  318. }
  319. stateMutex.Lock()
  320. isScanning = false
  321. stateMutex.Unlock()
  322. idleSeconds = 0
  323. }
  324. } else {
  325. idleSeconds = 0
  326. }
  327. }
  328. }
  329. func processSubmittedURLs() {
  330. rows, err := dbGlobal.Query(`SELECT id, url FROM submitted_urls`)
  331. if err != nil { return }
  332. defer rows.Close()
  333. for rows.Next() {
  334. var id int
  335. var rawURL string
  336. if err := rows.Scan(&id, &rawURL); err == nil {
  337. u, pErr := url.Parse(rawURL)
  338. if pErr == nil && u.Hostname() != "" {
  339. baseHost := strings.ToLower(u.Hostname())
  340. var zID int
  341. dbGlobal.QueryRow(`INSERT INTO zones (zone_name, dns_server_ip) VALUES ('custom_submissions', '0.0.0.0') ON CONFLICT (zone_name) DO UPDATE SET last_synced = CURRENT_TIMESTAMP RETURNING id`).Scan(&zID)
  342. var recID int
  343. dbGlobal.QueryRow(`INSERT INTO dns_records (zone_id, hostname, record_type, value) VALUES ($1, $2, 'A', 'Χειροκίνητο') ON CONFLICT (zone_id, hostname, record_type, value) DO UPDATE SET last_seen = CURRENT_TIMESTAMP RETURNING id`, zID, baseHost).Scan(&recID)
  344. if recID > 0 {
  345. atomic.AddInt32(&spiderPending, 1)
  346. spiderQueue <- SpiderJob{RecordID: recID, CurrentURL: rawURL, BaseHost: baseHost, Depth: 0}
  347. dbGlobal.Exec(`DELETE FROM submitted_urls WHERE id = $1`, id)
  348. logMsg("[🔗 SUBMIT] Βρέθηκε χειροκίνητο URL: %s", rawURL)
  349. }
  350. }
  351. }
  352. }
  353. }
  354. func spiderWorker() {
  355. for job := range spiderQueue {
  356. if atomic.LoadInt32(&stopRequested) == 0 {
  357. processSpider(job)
  358. }
  359. atomic.AddInt32(&spiderPending, -1)
  360. }
  361. }
  362. func aiWorker() {
  363. for job := range aiQueue {
  364. if atomic.LoadInt32(&stopRequested) == 0 {
  365. analysis := getDeepAnalysis(job.URL, job.Title, job.MetaDesc, job.Generator, job.H1, job.Preview)
  366. u, err := url.Parse(job.URL)
  367. if err == nil {
  368. hostname := strings.ToLower(strings.TrimSuffix(u.Hostname(), "."))
  369. if isCoreService(hostname) { analysis.Category = "Core Services" }
  370. }
  371. globalStats.Update(analysis.Category)
  372. logMsg("[🤖 AI-TAG] %-25s -> [%s]", u.Hostname(), analysis.Category)
  373. dbGlobal.Exec(`UPDATE web_health SET category = $1, ai_summary = $2, activity_status = $3 WHERE url = $4`,
  374. analysis.Category, analysis.Summary, analysis.Status, job.URL)
  375. time.Sleep(1000 * time.Millisecond)
  376. }
  377. atomic.AddInt32(&aiPending, -1)
  378. }
  379. }
  380. func processSpider(job SpiderJob) {
  381. cfgMutex.RLock()
  382. mDepth := cfg.MaxDepth
  383. mPages := cfg.MaxPagesPerHost
  384. cfgMutex.RUnlock()
  385. if job.Depth > mDepth { return }
  386. cleanURL := cleanURLForDB(job.CurrentURL)
  387. if _, loaded := visitedWeb.LoadOrStore(cleanURL, true); loaded { return }
  388. lowerURL := strings.ToLower(cleanURL)
  389. skipExt := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".zip", ".php", "graphs", "?print=", "&print=", ".iso", ".rar", ".torrent", ".mkv", ".mp4", ".mp3"}
  390. for _, ext := range skipExt {
  391. if strings.Contains(lowerURL, ext) {
  392. atomic.AddUint64(&sitesSkipped, 1)
  393. return
  394. }
  395. }
  396. vCountRaw, _ := hostVisitCount.LoadOrStore(job.BaseHost, 0)
  397. vCount := vCountRaw.(int)
  398. if vCount >= mPages { return }
  399. hostVisitCount.Store(job.BaseHost, vCount+1)
  400. // --- HTTP REQUEST (ΜΕ FAKE USER AGENT) ---
  401. req, _ := http.NewRequest("GET", cleanURL, nil)
  402. req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 WiSearchBot/1.0")
  403. req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
  404. resp, err := httpClient.Do(req)
  405. if err != nil {
  406. atomic.AddUint64(&sitesOffline, 1)
  407. logMsg("[❌ DEAD] Σφάλμα σύνδεσης στο %s: %v", cleanURL, err)
  408. dbGlobal.Exec(`UPDATE web_health SET is_up = false, activity_status = 'Down', last_crawled = CURRENT_TIMESTAMP WHERE url = $1`, cleanURL)
  409. return
  410. }
  411. defer resp.Body.Close()
  412. if resp.StatusCode != 200 {
  413. atomic.AddUint64(&sitesOffline, 1)
  414. logMsg("[⚠️ HTTP ERROR] Το %s γύρισε κωδικό %d", cleanURL, resp.StatusCode)
  415. return
  416. }
  417. contentType := resp.Header.Get("Content-Type")
  418. if !strings.Contains(contentType, "text/html") {
  419. atomic.AddUint64(&sitesSkipped, 1)
  420. logMsg("[⚠️ SKIPPED-TYPE] URL: %s | Ήρθε με Content-Type: %s", cleanURL, contentType)
  421. return
  422. }
  423. utf8Body, _ := decodeToUTF8(resp.Body, contentType)
  424. doc, err := goquery.NewDocumentFromReader(utf8Body)
  425. if err != nil { return }
  426. doc.Find("img").Each(func(i int, s *goquery.Selection) {
  427. src, exists := s.Attr("src")
  428. alt, _ := s.Attr("alt")
  429. if exists && len(src) > 2 && !strings.HasPrefix(src, "data:image") {
  430. u, err := url.Parse(src)
  431. if err == nil {
  432. base, _ := url.Parse(job.CurrentURL)
  433. absImg := base.ResolveReference(u).String()
  434. dbGlobal.Exec(`INSERT INTO media_files (source_url, media_url, media_type, alt_text) VALUES ($1, $2, 'image', $3) ON CONFLICT DO NOTHING`, cleanURL, absImg, strings.TrimSpace(alt))
  435. atomic.AddUint64(&mediaFound, 1)
  436. }
  437. }
  438. })
  439. title := strings.TrimSpace(doc.Find("title").Text())
  440. metaDesc, _ := doc.Find("meta[name=description]").Attr("content")
  441. generator, _ := doc.Find("meta[name=generator]").Attr("content")
  442. h1 := strings.TrimSpace(doc.Find("h1").First().Text())
  443. doc.Find("script, style, nav, footer, header, aside, noscript, iframe, svg, canvas").Remove()
  444. bodyText := strings.Join(strings.Fields(doc.Find("body").Text()), " ")
  445. // --- ΔΙΟΡΘΩΣΗ: ΠΙΟ ΕΛΑΣΤΙΚΟΣ ΚΟΦΤΗΣ ΧΑΡΑΚΤΗΡΩΝ ---
  446. if len(strings.TrimSpace(bodyText)) < 5 && title == "" {
  447. atomic.AddUint64(&sitesSkipped, 1)
  448. logMsg("[⚠️ SKIPPED-EMPTY] URL: %s | Βρήκε κείμενο μήκους: %d | Τίτλος: '%s'", cleanURL, len(strings.TrimSpace(bodyText)), title)
  449. return
  450. }
  451. atomic.AddUint64(&sitesActive, 1)
  452. logMsg("[✅ ALIVE] Active Node: %s", cleanURL)
  453. dbGlobal.Exec(`INSERT INTO web_health (dns_record_id, url, status_code, page_title, is_up, last_crawled, full_content)
  454. VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, $6)
  455. ON CONFLICT (url) DO UPDATE SET page_title = EXCLUDED.page_title, is_up = true, last_crawled = CURRENT_TIMESTAMP, full_content = EXCLUDED.full_content`,
  456. job.RecordID, cleanURL, resp.StatusCode, title, true, bodyText)
  457. runes := []rune(bodyText)
  458. preview := ""
  459. if len(runes) > 400 { preview = string(runes[:400]) } else { preview = string(runes) }
  460. atomic.AddInt32(&aiPending, 1)
  461. aiQueue <- AIJob{URL: cleanURL, Title: title, MetaDesc: metaDesc, Generator: generator, H1: h1, Preview: preview}
  462. // --- ΕΞΑΓΩΓΗ LINKS (SPIDER) ---
  463. doc.Find("a").Each(func(i int, s *goquery.Selection) {
  464. if atomic.LoadInt32(&stopRequested) == 1 { return }
  465. href, _ := s.Attr("href")
  466. u, err := url.Parse(href)
  467. if err == nil {
  468. base, _ := url.Parse(job.CurrentURL)
  469. abs := base.ResolveReference(u)
  470. absHost := strings.ToLower(abs.Hostname())
  471. isAllowed := false
  472. // 1. Είναι το ίδιο site ή subdomain (πχ wind.awmn -> stats.wind.awmn)
  473. if absHost == job.BaseHost || strings.HasSuffix(absHost, "."+job.BaseHost) {
  474. isAllowed = true
  475. } else if strings.HasSuffix(absHost, ".awmn") || strings.HasSuffix(absHost, ".local") {
  476. // 2. Είναι οποιοδήποτε άλλο site στο δίκτυο (.awmn ή .local)
  477. isAllowed = true
  478. } else {
  479. // 3. Ή ταιριάζει με κάτι που έχουμε βάλει στα Zones στο Admin Panel
  480. cfgMutex.RLock()
  481. for _, z := range cfg.ZoneNames {
  482. cz := strings.ToLower(strings.TrimSuffix(z, "."))
  483. if strings.HasSuffix(absHost, "."+cz) || absHost == cz {
  484. isAllowed = true
  485. break
  486. }
  487. }
  488. cfgMutex.RUnlock()
  489. }
  490. if isAllowed && (abs.Scheme == "http" || abs.Scheme == "https") {
  491. target := cleanURLForDB(abs.String())
  492. if _, loaded := visitedWeb.Load(target); !loaded {
  493. dbGlobal.Exec(`INSERT INTO web_links (source_url, target_url) VALUES ($1, $2) ON CONFLICT DO NOTHING`, cleanURL, target)
  494. targetRecordID := job.RecordID
  495. if absHost != job.BaseHost {
  496. var zID int
  497. dbGlobal.QueryRow(`INSERT INTO zones (zone_name, dns_server_ip) VALUES ('auto_discovered', '0.0.0.0') ON CONFLICT (zone_name) DO UPDATE SET last_synced = CURRENT_TIMESTAMP RETURNING id`).Scan(&zID)
  498. dbGlobal.QueryRow(`INSERT INTO dns_records (zone_id, hostname, record_type, value) VALUES ($1, $2, 'A', 'Scraped') ON CONFLICT (zone_id, hostname, record_type, value) DO UPDATE SET last_seen = CURRENT_TIMESTAMP RETURNING id`, zID, absHost).Scan(&targetRecordID)
  499. }
  500. if targetRecordID > 0 {
  501. atomic.AddInt32(&spiderPending, 1)
  502. spiderQueue <- SpiderJob{RecordID: targetRecordID, CurrentURL: target, BaseHost: absHost, Depth: job.Depth + 1}
  503. }
  504. }
  505. }
  506. }
  507. })
  508. }
  509. func getDeepAnalysis(targetURL, title, metaDesc, generator, h1, body string) AIAnalysis {
  510. if len(strings.TrimSpace(body)) < 30 && strings.TrimSpace(title) == "" {
  511. return AIAnalysis{Category: "Other", Summary: "Insufficient content.", Status: "Active"}
  512. }
  513. cfgMutex.RLock()
  514. systemPrompt := cfg.AIPrompt
  515. creds := cfg.CFCredentials
  516. cfgMutex.RUnlock()
  517. if systemPrompt == "" {
  518. systemPrompt = "You are an AWMN web classifier. Return ONLY JSON."
  519. }
  520. userPrompt := fmt.Sprintf("URL: %s\nTitle: %s\nMeta: %s\nContent: %s", targetURL, title, metaDesc, body)
  521. payload := map[string]interface{}{"messages": []map[string]string{{"role": "system", "content": systemPrompt}, {"role": "user", "content": userPrompt}}}
  522. bodyJSON, _ := json.Marshal(payload)
  523. for _, cred := range creds {
  524. if atomic.LoadInt32(&stopRequested) == 1 { break }
  525. apiURL := fmt.Sprintf("https://api.cloudflare.com/client/v4/accounts/%s/ai/run/%s", cred.AccountID, cred.Model)
  526. req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(bodyJSON))
  527. req.Header.Set("Authorization", "Bearer "+cred.APIToken)
  528. req.Header.Set("Content-Type", "application/json")
  529. resp, err := aiClient.Do(req)
  530. if err != nil { continue }
  531. if resp.StatusCode != 200 { resp.Body.Close(); continue }
  532. var cfRes struct { Result struct { Response string `json:"response"` } `json:"result"`; Success bool `json:"success"` }
  533. json.NewDecoder(resp.Body).Decode(&cfRes)
  534. resp.Body.Close()
  535. cleanJSON := cfRes.Result.Response
  536. if start := strings.Index(cleanJSON, "{"); start != -1 {
  537. if end := strings.LastIndex(cleanJSON, "}"); end != -1 && start <= end {
  538. cleanJSON = cleanJSON[start : end+1]
  539. }
  540. }
  541. var analysis AIAnalysis
  542. if err := json.Unmarshal([]byte(cleanJSON), &analysis); err == nil {
  543. if analysis.Category == "" { analysis.Category = "Other" }
  544. return analysis
  545. }
  546. }
  547. return AIAnalysis{Category: "Other", Summary: "AI Classification Failed", Status: "Active"}
  548. }
  549. func cleanURLForDB(inputURL string) string {
  550. u, err := url.Parse(inputURL)
  551. if err != nil { return inputURL }
  552. q := u.Query()
  553. // --- ΔΙΟΡΘΩΣΗ ΛΑΒΥΡΙΝΘΟΥ: Αφαιρέσαμε τα junk parameters (node, page, id κλπ) ---
  554. junk := []string{
  555. "sid", "s", "osid", "sort", "order", "phpsessid", "view", "lang",
  556. "search", "highlight", "mode", "start", "form_search_nodes_search",
  557. "limit", "offset", "direction", "node", "page", "table_nodes_showpage",
  558. "action", "id", "p", "t",
  559. }
  560. for _, k := range junk { q.Del(k) }
  561. u.RawQuery = q.Encode()
  562. u.Fragment = ""
  563. return u.String()
  564. }
  565. func decodeToUTF8(r io.Reader, contentType string) (io.Reader, error) {
  566. buf, err := io.ReadAll(r)
  567. if err != nil { return nil, err }
  568. determineEncoding, _, _ := charset.DetermineEncoding(buf, contentType)
  569. utf8Reader := determineEncoding.NewDecoder().Reader(bytes.NewReader(buf))
  570. finalBuf, _ := io.ReadAll(utf8Reader)
  571. cleanStr := strings.Map(func(r rune) rune {
  572. if r == '\uFFFD' || r < 0 { return -1 }
  573. return r
  574. }, string(finalBuf))
  575. return strings.NewReader(cleanStr), nil
  576. }
  577. func updatePageRank() {
  578. dbGlobal.Exec(`UPDATE web_health wh SET pagerank_score = (1.0 + (SELECT COUNT(*) FROM web_links wl WHERE wl.target_url = wh.url)) * (CASE WHEN wh.category = 'Core Services' THEN 10.0 WHEN wh.category IN ('Gaming', 'Media & Streaming', 'P2P & Torrents', 'Web Services & Blogs', 'Forums & Communities', 'Communication & VoIP') THEN 3.0 WHEN wh.category IN ('Storage & FTP') THEN 2.0 WHEN wh.category IN ('Network Tools & Stats', 'IoT & Sensors') THEN 1.0 ELSE 1.0 END)`)
  579. }
  580. // --- DNS LOGIC ---
  581. func fetchAndStoreDNS(zoneName string, dnsServer string) {
  582. defer atomic.AddInt32(&spiderPending, -1)
  583. if atomic.LoadInt32(&stopRequested) == 1 { return }
  584. zoneName = dns.Fqdn(strings.ToLower(zoneName))
  585. if _, loaded := visitedZones.LoadOrStore(zoneName, true); loaded { return }
  586. var zoneID int
  587. err := dbGlobal.QueryRow(`INSERT INTO zones (zone_name, dns_server_ip) VALUES ($1, $2) ON CONFLICT (zone_name) DO UPDATE SET last_synced = CURRENT_TIMESTAMP RETURNING id`, strings.TrimSuffix(zoneName, "."), dnsServer).Scan(&zoneID)
  588. if err != nil {
  589. logMsg("[❌ DB ERROR] Αποτυχία αποθήκευσης Ζώνης '%s': %v", zoneName, err)
  590. return
  591. }
  592. atomic.AddInt32(&spiderPending, 1)
  593. go directLookup(zoneID, zoneName, dnsServer)
  594. for _, sub := range forceLookups {
  595. atomic.AddInt32(&spiderPending, 1)
  596. go directLookup(zoneID, fmt.Sprintf("%s.%s", sub, zoneName), dnsServer)
  597. }
  598. m := new(dns.Msg); m.SetAxfr(zoneName)
  599. tr := &dns.Transfer{DialTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second}
  600. channel, err := tr.In(m, net.JoinHostPort(dnsServer, "53"))
  601. if err != nil {
  602. logMsg("[⚠️ DNS] Zone Transfer (AXFR) Failed for %s: %v", zoneName, err)
  603. return
  604. }
  605. for env := range channel {
  606. if env.Error != nil {
  607. logMsg("[⚠️ DNS] Ενδιάμεσο σφάλμα AXFR για %s: %v", zoneName, env.Error)
  608. break
  609. }
  610. for _, rr := range env.RR {
  611. if atomic.LoadInt32(&stopRequested) == 1 { return }
  612. hName := strings.ToLower(rr.Header().Name)
  613. switch t := rr.(type) {
  614. case *dns.A:
  615. var lastID int
  616. err := dbGlobal.QueryRow(`INSERT INTO dns_records (zone_id, hostname, record_type, value) VALUES ($1, $2, $3, $4) ON CONFLICT (zone_id, hostname, record_type, value) DO UPDATE SET last_seen = CURRENT_TIMESTAMP RETURNING id`, zoneID, hName, "A", t.A.String()).Scan(&lastID)
  617. if err != nil {
  618. logMsg("[❌ DB ERROR] Αποτυχία αποθήκευσης DNS ρεκόρ '%s': %v", hName, err)
  619. } else if lastID > 0 {
  620. atomic.AddInt32(&spiderPending, 1)
  621. spiderQueue <- SpiderJob{RecordID: lastID, CurrentURL: "http://" + strings.TrimSuffix(hName, "."), BaseHost: strings.TrimSuffix(hName, "."), Depth: 0}
  622. }
  623. case *dns.CNAME:
  624. var lastID int
  625. err := dbGlobal.QueryRow(`INSERT INTO dns_records (zone_id, hostname, record_type, value) VALUES ($1, $2, $3, $4) ON CONFLICT (zone_id, hostname, record_type, value) DO UPDATE SET last_seen = CURRENT_TIMESTAMP RETURNING id`, zoneID, hName, "CNAME", t.Target).Scan(&lastID)
  626. if err == nil && lastID > 0 {
  627. atomic.AddInt32(&spiderPending, 1)
  628. spiderQueue <- SpiderJob{RecordID: lastID, CurrentURL: "http://" + strings.TrimSuffix(hName, "."), BaseHost: strings.TrimSuffix(hName, "."), Depth: 0}
  629. }
  630. case *dns.NS:
  631. if dns.Fqdn(hName) != zoneName {
  632. atomic.AddInt32(&spiderPending, 1)
  633. go func(sz, nt string) {
  634. defer atomic.AddInt32(&spiderPending, -1)
  635. c := new(dns.Client); c.Timeout = 2 * time.Second
  636. m2 := new(dns.Msg); m2.SetQuestion(dns.Fqdn(nt), dns.TypeA)
  637. r, _, _ := c.Exchange(m2, net.JoinHostPort(dnsServer, "53"))
  638. if r != nil && len(r.Answer) > 0 {
  639. if a, ok := r.Answer[0].(*dns.A); ok {
  640. atomic.AddInt32(&spiderPending, 1)
  641. fetchAndStoreDNS(sz, a.A.String())
  642. }
  643. }
  644. }(hName, t.Ns)
  645. }
  646. }
  647. }
  648. }
  649. }
  650. func directLookup(zoneID int, hostname string, server string) {
  651. defer atomic.AddInt32(&spiderPending, -1)
  652. if atomic.LoadInt32(&stopRequested) == 1 { return }
  653. m := new(dns.Msg); m.SetQuestion(dns.Fqdn(hostname), dns.TypeA)
  654. c := new(dns.Client); c.Timeout = 2 * time.Second
  655. r, _, err := c.Exchange(m, net.JoinHostPort(server, "53"))
  656. if err == nil && r != nil {
  657. for _, ans := range r.Answer {
  658. if t, ok := ans.(*dns.A); ok {
  659. var lastID int
  660. h := strings.ToLower(t.Header().Name)
  661. errDB := dbGlobal.QueryRow(`INSERT INTO dns_records (zone_id, hostname, record_type, value) VALUES ($1, $2, $3, $4) ON CONFLICT (zone_id, hostname, record_type, value) DO UPDATE SET last_seen = CURRENT_TIMESTAMP RETURNING id`, zoneID, h, "A", t.A.String()).Scan(&lastID)
  662. if errDB != nil {
  663. logMsg("[❌ DB ERROR] Αποτυχία αποθήκευσης Force Lookup '%s': %v", h, errDB)
  664. } else if lastID > 0 {
  665. atomic.AddInt32(&spiderPending, 1)
  666. spiderQueue <- SpiderJob{RecordID: lastID, CurrentURL: "http://" + strings.TrimSuffix(h, "."), BaseHost: strings.TrimSuffix(h, "."), Depth: 0}
  667. }
  668. }
  669. }
  670. }
  671. }
  672. func startWebUI() {
  673. mux := http.NewServeMux()
  674. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  675. if r.URL.Path != "/" { http.NotFound(w, r); return }
  676. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  677. w.Write([]byte(htmlDashboard))
  678. })
  679. mux.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) {
  680. globalStats.Lock()
  681. defer globalStats.Unlock()
  682. stateMutex.Lock()
  683. scanStat := isScanning
  684. enabled := daemonEnabled
  685. target := nextRunTime
  686. stateMutex.Unlock()
  687. nextRunStr := "Daemon Stopped"
  688. if enabled {
  689. if target.IsZero() {
  690. nextRunStr = "Manual Only"
  691. } else {
  692. rem := time.Until(target)
  693. if rem > 0 {
  694. nextRunStr = fmt.Sprintf("%dh %dm", int(rem.Hours()), int(rem.Minutes())%60)
  695. } else {
  696. nextRunStr = "Imminent"
  697. }
  698. }
  699. }
  700. json.NewEncoder(w).Encode(map[string]interface{}{
  701. "isScanning": scanStat,
  702. "daemonEnabled": enabled,
  703. "nextRun": nextRunStr,
  704. "spiderQueue": len(spiderQueue),
  705. "aiQueue": len(aiQueue),
  706. "active": atomic.LoadUint64(&sitesActive),
  707. "dead": atomic.LoadUint64(&sitesOffline),
  708. "skipped": atomic.LoadUint64(&sitesSkipped),
  709. "media": atomic.LoadUint64(&mediaFound),
  710. "categories": globalStats.CategoryCounts,
  711. })
  712. })
  713. mux.HandleFunc("/api/logs", func(w http.ResponseWriter, r *http.Request) {
  714. logMutex.Lock()
  715. defer logMutex.Unlock()
  716. json.NewEncoder(w).Encode(logBuffer)
  717. })
  718. mux.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
  719. if r.Method == "GET" {
  720. cfgMutex.RLock()
  721. defer cfgMutex.RUnlock()
  722. json.NewEncoder(w).Encode(cfg)
  723. } else if r.Method == "POST" {
  724. var newCfg AppConfig
  725. if err := json.NewDecoder(r.Body).Decode(&newCfg); err == nil {
  726. cfgMutex.Lock()
  727. cfg = newCfg
  728. saveConfigInternal()
  729. cfgMutex.Unlock()
  730. calcNextRun()
  731. logMsg("[🔧 SYSTEM] Config saved via Web UI.")
  732. w.WriteHeader(http.StatusOK)
  733. } else {
  734. http.Error(w, err.Error(), http.StatusBadRequest)
  735. }
  736. }
  737. })
  738. mux.HandleFunc("/api/config/reload", func(w http.ResponseWriter, r *http.Request) {
  739. if r.Method == "POST" {
  740. loadConfig()
  741. calcNextRun()
  742. w.WriteHeader(http.StatusOK)
  743. }
  744. })
  745. mux.HandleFunc("/api/daemon/start", func(w http.ResponseWriter, r *http.Request) {
  746. stateMutex.Lock()
  747. daemonEnabled = true
  748. stateMutex.Unlock()
  749. calcNextRun()
  750. logMsg("[▶️ SYSTEM] Daemon Started (Scheduler active).")
  751. w.WriteHeader(http.StatusOK)
  752. })
  753. mux.HandleFunc("/api/daemon/stop", func(w http.ResponseWriter, r *http.Request) {
  754. stateMutex.Lock()
  755. daemonEnabled = false
  756. stateMutex.Unlock()
  757. stopScan()
  758. w.WriteHeader(http.StatusOK)
  759. })
  760. mux.HandleFunc("/api/scan/force", func(w http.ResponseWriter, r *http.Request) {
  761. if r.Method == "POST" {
  762. go triggerScan()
  763. w.WriteHeader(http.StatusOK)
  764. }
  765. })
  766. cfgMutex.RLock()
  767. port := cfg.WebUIPort
  768. cfgMutex.RUnlock()
  769. if !strings.Contains(port, ":") { port = ":" + port }
  770. logMsg("[🌍 WEB-UI] Daemon C&C Dashboard running on 0.0.0.0%s", port)
  771. err := http.ListenAndServe(port, mux)
  772. if err != nil { logMsg("[❌ FATAL] Web UI Failed: %v", err) }
  773. }
  774. const htmlDashboard = `
  775. <!DOCTYPE html>
  776. <html lang="en">
  777. <head>
  778. <meta charset="UTF-8">
  779. <title>WiSearch C&C</title>
  780. <style>
  781. body { background: #121212; color: #e0e0e0; font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; padding: 20px; }
  782. .header { border-bottom: 2px solid #333; padding-bottom: 15px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;}
  783. .title { color: #bb86fc; margin: 0; font-size: 24px; }
  784. .nav-tabs { margin-bottom: 10px; }
  785. .nav-tabs button { background: #1e1e1e; color: #e0e0e0; border: 1px solid #333; padding: 10px 20px; cursor: pointer; border-radius: 4px; margin-right: 10px; font-weight: bold; }
  786. .nav-tabs button.active { background: #bb86fc; color: #121212; }
  787. .sub-tabs { margin-bottom: 20px; background: #1a1a1a; padding: 10px; border-radius: 8px; border: 1px solid #2d2d2d; display:flex; gap:10px; }
  788. .sub-tabs button { background: transparent; color: #9e9e9e; border: none; padding: 8px 15px; cursor: pointer; border-radius: 4px; font-weight: bold; transition: 0.2s;}
  789. .sub-tabs button:hover { background: #333; color: #fff;}
  790. .sub-tabs button.active { background: #03dac6; color: #000; }
  791. .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 20px; }
  792. .card { background: #1e1e1e; padding: 20px; border-radius: 8px; border: 1px solid #333; text-align: center; }
  793. .card h3 { margin: 0 0 10px 0; font-size: 13px; color: #9e9e9e; text-transform: uppercase; }
  794. .card p { margin: 0; font-size: 24px; font-weight: bold; color: #03dac6; }
  795. .card.red p { color: #cf6679; }
  796. .card.purple p { color: #bb86fc; }
  797. .main-container { display: flex; gap: 20px; height: calc(100vh - 270px); }
  798. .terminal { flex: 2; background: #000; border-radius: 8px; padding: 15px; overflow-y: auto; font-family: 'Consolas', monospace; font-size: 13px; line-height: 1.5; border: 1px solid #333; box-shadow: inset 0 0 10px rgba(0,0,0,0.8); }
  799. .log-line { margin-bottom: 4px; word-wrap: break-word; }
  800. .log-info { color: #4af626; }
  801. .log-err { color: #ff5555; font-weight: bold; }
  802. .log-warn { color: #ffb86c; }
  803. .log-ai { color: #bd93f9; }
  804. .sidebar { flex: 1; background: #1e1e1e; border-radius: 8px; padding: 15px; border: 1px solid #333; overflow-y: auto; }
  805. .cat-list { list-style: none; padding: 0; margin: 0; }
  806. .cat-list li { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #2d2d2d; font-size: 14px; }
  807. .cat-list li span { color: #03dac6; font-weight: bold; }
  808. .btn { border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-weight: bold; color:#fff; }
  809. .btn-start { background: #03dac6; color:#000; }
  810. .btn-stop { background: #cf6679; }
  811. .btn-force { background: #bb86fc; color:#000; }
  812. .btn-reload { background: #3700b3; }
  813. .btn-add { background: #03dac6; color: #000; font-size:12px; padding: 4px 8px; margin-left: 10px; }
  814. .btn-del { background: #cf6679; color: #fff; font-size:12px; padding: 4px 8px; margin-left: 5px; }
  815. #settings-panel { display: none; background: #1e1e1e; padding: 20px; border-radius: 8px; border: 1px solid #333; height: calc(100vh - 180px); display:flex; flex-direction:column; }
  816. .set-section { display: none; flex: 1; overflow-y: auto; }
  817. .set-section.active { display: block; }
  818. .form-group { display: flex; flex-direction: column; margin-bottom: 15px; background: rgba(255,255,255,0.02); padding: 15px; border-radius: 8px; border: 1px solid #2d2d2d;}
  819. .form-group.inline { flex-direction: row; gap:10px; align-items:center; background:none; padding:0; border:none; margin-bottom: 10px;}
  820. label { font-size: 12px; color: #bb86fc; margin-bottom: 5px; font-weight:bold; text-transform:uppercase; }
  821. input, select, textarea { background: #121212; color: #e0e0e0; border: 1px solid #333; padding: 10px; border-radius: 4px; font-family: monospace; width: 100%; box-sizing: border-box;}
  822. .list-item { display: flex; gap: 10px; margin-bottom: 5px; }
  823. .cf-item { background: #121212; padding: 10px; border-radius: 4px; border: 1px solid #333; margin-bottom: 10px; position:relative; }
  824. </style>
  825. </head>
  826. <body>
  827. <div class="header">
  828. <h1 class="title">WiSearch <span>C&C</span></h1>
  829. <div style="display:flex; gap:10px; align-items:center;">
  830. <span id="status-badge" style="font-family: monospace; padding: 5px 10px; border-radius: 4px;">● STATUS</span>
  831. <span style="font-family: monospace; color: #9e9e9e; margin-right:15px;">Next Run: <b id="next-run" style="color:#bb86fc;">--</b></span>
  832. <button class="btn btn-start" onclick="action('start')">▶ Start</button>
  833. <button class="btn btn-stop" onclick="action('stop')">⏸ Stop</button>
  834. <button class="btn btn-force" onclick="action('force')">⚡ Force Scan</button>
  835. <button class="btn btn-reload" onclick="action('reload')">🔄 Reload Config</button>
  836. </div>
  837. </div>
  838. <div class="nav-tabs">
  839. <button id="tab-dash" class="active" onclick="switchMainTab('dash')">Dashboard</button>
  840. <button id="tab-set" onclick="switchMainTab('set')">Settings Form</button>
  841. </div>
  842. <div id="dash-panel">
  843. <div class="grid">
  844. <div class="card purple"><h3>Spider Queue</h3><p id="sq">0</p></div>
  845. <div class="card purple"><h3>AI Queue</h3><p id="aq">0</p></div>
  846. <div class="card"><h3>Active Nodes</h3><p id="ac">0</p></div>
  847. <div class="card red"><h3>Dead Nodes</h3><p id="de">0</p></div>
  848. <div class="card"><h3>Skipped</h3><p id="sk">0</p></div>
  849. <div class="card"><h3>Media</h3><p id="me">0</p></div>
  850. </div>
  851. <div class="main-container">
  852. <div class="terminal" id="terminal"></div>
  853. <div class="sidebar">
  854. <h3 style="margin-top:0; color: #bb86fc; border-bottom: 1px solid #333; padding-bottom: 10px;">AI Classifier</h3>
  855. <ul class="cat-list" id="cat-list"></ul>
  856. </div>
  857. </div>
  858. </div>
  859. <div id="settings-panel" style="display:none;">
  860. <div class="sub-tabs">
  861. <button id="sub-db" class="active" onclick="switchSetTab('db')">Database</button>
  862. <button id="sub-crawl" onclick="switchSetTab('crawl')">Crawler & Schedule</button>
  863. <button id="sub-targets" onclick="switchSetTab('targets')">Targets (DNS/Zones)</button>
  864. <button id="sub-ai" onclick="switchSetTab('ai')">AI Accounts</button>
  865. <button id="sub-prompt" onclick="switchSetTab('prompt')">AI Prompt & Whitelist</button>
  866. </div>
  867. <div id="set-db" class="set-section active">
  868. <div class="form-group">
  869. <label>Database Connection Details</label>
  870. <div class="form-group inline"><label style="width:60px;">Host:</label><input type="text" id="db-host"></div>
  871. <div class="form-group inline"><label style="width:60px;">Port:</label><input type="text" id="db-port"></div>
  872. <div class="form-group inline"><label style="width:60px;">User:</label><input type="text" id="db-user"></div>
  873. <div class="form-group inline"><label style="width:60px;">Pass:</label><input type="password" id="db-pass"></div>
  874. <div class="form-group inline"><label style="width:60px;">DB Name:</label><input type="text" id="db-name"></div>
  875. <div class="form-group inline"><label style="width:60px;">SSL Mode:</label><input type="text" id="db-ssl"></div>
  876. </div>
  877. </div>
  878. <div id="set-crawl" class="set-section">
  879. <div class="form-group">
  880. <label>Crawler Limits</label>
  881. <div class="form-group inline"><label style="width:120px;">Web UI Port:</label><input type="text" id="cfg-port"></div>
  882. <div class="form-group inline"><label style="width:120px;">Max Depth:</label><input type="number" id="cfg-depth"></div>
  883. <div class="form-group inline"><label style="width:120px;">Workers:</label><input type="number" id="cfg-workers"></div>
  884. <div class="form-group inline"><label style="width:120px;">Pages/Host:</label><input type="number" id="cfg-pages"></div>
  885. </div>
  886. <div class="form-group">
  887. <label>Scheduler <span style="color:#e0e0e0; font-weight:normal; text-transform:none;">(0 to disable)</span></label>
  888. <div class="form-group inline"><label style="width:120px;">Run Every (Hours):</label><input type="number" id="cfg-sh"></div>
  889. <div class="form-group inline"><label style="width:120px;">Run Every (Mins):</label><input type="number" id="cfg-sm"></div>
  890. <label style="display:flex; align-items:center; gap:10px; color:#e0e0e0; margin-top:10px; cursor:pointer;">
  891. <input type="checkbox" id="cfg-onstart" style="width:auto; transform:scale(1.5);"> Scan immediately on daemon start
  892. </label>
  893. </div>
  894. </div>
  895. <div id="set-targets" class="set-section">
  896. <div class="form-group">
  897. <label>DNS Servers <button class="btn btn-add" onclick="addListInput('dns-list')">+ Add</button></label>
  898. <div id="dns-list"></div>
  899. </div>
  900. <div class="form-group">
  901. <label>DNS Zones <button class="btn btn-add" onclick="addListInput('zones-list')">+ Add</button></label>
  902. <div id="zones-list"></div>
  903. </div>
  904. </div>
  905. <div id="set-ai" class="set-section">
  906. <div class="form-group">
  907. <label>Cloudflare AI Accounts <button class="btn btn-add" onclick="addCFInput()">+ Add Account</button></label>
  908. <div id="cf-list" style="max-height: 400px; overflow-y:auto; padding-right:10px;"></div>
  909. </div>
  910. </div>
  911. <div id="set-prompt" class="set-section">
  912. <div class="form-group">
  913. <label>System Prompt for AI Classifier</label>
  914. <textarea id="cfg-prompt" style="height:200px; resize:vertical; font-size:13px;" placeholder="Enter system prompt here..."></textarea>
  915. <span style="font-size:11px; color:#9e9e9e; margin-top:5px;">This prompt tells the LLM how to format the JSON response and what categories to use.</span>
  916. </div>
  917. <div class="form-group">
  918. <label>Core Services Whitelist <button class="btn btn-add" onclick="addListInput('core-list')">+ Add</button></label>
  919. <div id="core-list" style="max-height: 200px; overflow-y:auto; padding-right:10px;"></div>
  920. </div>
  921. </div>
  922. <div style="margin-top: 15px; border-top: 1px solid #333; padding-top: 15px;">
  923. <button class="btn btn-start" style="padding:15px 30px; font-size:16px; box-shadow: 0 4px 6px rgba(0,0,0,0.3);" onclick="saveConfig()">💾 Save Configuration</button>
  924. </div>
  925. </div>
  926. <script>
  927. function colorizeLog(text) {
  928. if (text.includes("ERR") || text.includes("DEAD") || text.includes("FATAL")) return '<span class="log-err">' + text + '</span>';
  929. if (text.includes("RATE LIMIT") || text.includes("[🛑")) return '<span class="log-err">' + text + '</span>';
  930. if (text.includes("[🤖")) return '<span class="log-ai">' + text + '</span>';
  931. if (text.includes("[🌐") || text.includes("[🚀") || text.includes("[⏰") || text.includes("[🔄") || text.includes("[🌍") || text.includes("[▶️")) return '<span class="log-warn">' + text + '</span>';
  932. if (text.includes("[✅") || text.includes("[🔧")) return '<span class="log-info">' + text + '</span>';
  933. return text;
  934. }
  935. async function fetchStats() {
  936. try {
  937. let res = await fetch('/api/stats');
  938. let data = await res.json();
  939. let badge = document.getElementById('status-badge');
  940. if(!data.daemonEnabled) {
  941. badge.innerText = "● STOPPED"; badge.style.color = "#cf6679"; badge.style.background = "rgba(207,102,121,0.1)";
  942. } else if(data.isScanning) {
  943. badge.innerText = "● SCANNING"; badge.style.color = "#03dac6"; badge.style.background = "rgba(3,218,198,0.1)";
  944. } else {
  945. badge.innerText = "● IDLE"; badge.style.color = "#9e9e9e"; badge.style.background = "rgba(158,158,158,0.1)";
  946. }
  947. document.getElementById('next-run').innerText = data.nextRun;
  948. document.getElementById('sq').innerText = data.spiderQueue;
  949. document.getElementById('aq').innerText = data.aiQueue;
  950. document.getElementById('ac').innerText = data.active;
  951. document.getElementById('de').innerText = data.dead;
  952. document.getElementById('sk').innerText = data.skipped;
  953. document.getElementById('me').innerText = data.media;
  954. let catHtml = '';
  955. for (const [key, value] of Object.entries(data.categories)) {
  956. catHtml += '<li>' + key + ' <span>' + value + '</span></li>';
  957. }
  958. document.getElementById('cat-list').innerHTML = catHtml || '<li>Awaiting Data...</li>';
  959. } catch (e) {}
  960. }
  961. async function fetchLogs() {
  962. try {
  963. let res = await fetch('/api/logs');
  964. let logs = await res.json();
  965. let term = document.getElementById('terminal');
  966. let isScrolledToBottom = term.scrollHeight - term.clientHeight <= term.scrollTop + 10;
  967. term.innerHTML = logs.map(l => '<div class="log-line">' + colorizeLog(l) + '</div>').join('');
  968. if (isScrolledToBottom) term.scrollTop = term.scrollHeight;
  969. } catch (e) {}
  970. }
  971. async function action(type) {
  972. if(type === 'start') await fetch('/api/daemon/start', {method: 'POST'});
  973. if(type === 'stop') await fetch('/api/daemon/stop', {method: 'POST'});
  974. if(type === 'force') await fetch('/api/scan/force', {method: 'POST'});
  975. if(type === 'reload') {
  976. await fetch('/api/config/reload', {method: 'POST'});
  977. alert("Config Reloaded from Disk!");
  978. }
  979. }
  980. function addListInput(containerId, val = "") {
  981. let div = document.createElement('div');
  982. div.className = "list-item";
  983. div.innerHTML = '<input type="text" value="' + val + '"><button class="btn btn-del" onclick="this.parentElement.remove()">X</button>';
  984. document.getElementById(containerId).appendChild(div);
  985. }
  986. function getListValues(containerId) {
  987. return Array.from(document.getElementById(containerId).querySelectorAll('input')).map(inp => inp.value.trim()).filter(v => v);
  988. }
  989. function addCFInput(comment="", acc="", token="", model="@cf/meta/llama-3-8b-instruct") {
  990. let div = document.createElement('div');
  991. div.className = "cf-item";
  992. let sel1 = (model==='@cf/meta/llama-3-8b-instruct') ? 'selected' : '';
  993. let sel2 = (model==='@cf/meta/llama-2-7b-chat-int8') ? 'selected' : '';
  994. let sel3 = (model==='@cf/mistral/mistral-7b-instruct-v0.1') ? 'selected' : '';
  995. div.innerHTML = '<button class="btn btn-del" style="position:absolute; top:5px; right:5px;" onclick="this.parentElement.remove()">X</button>' +
  996. '<div class="form-group inline"><label style="width:60px;">Tag:</label><input type="text" class="cf-comment" value="' + comment + '" placeholder="e.g. email@gmail.com"></div>' +
  997. '<div class="form-group inline"><label style="width:60px;">Acc ID:</label><input type="text" class="cf-acc" value="' + acc + '"></div>' +
  998. '<div class="form-group inline"><label style="width:60px;">Token:</label><input type="password" class="cf-token" value="' + token + '"></div>' +
  999. '<div class="form-group inline"><label style="width:60px;">Model:</label>' +
  1000. '<select class="cf-model">' +
  1001. '<option value="@cf/meta/llama-3-8b-instruct" ' + sel1 + '>LLaMA-3 8B (Fast)</option>' +
  1002. '<option value="@cf/meta/llama-2-7b-chat-int8" ' + sel2 + '>LLaMA-2 7B</option>' +
  1003. '<option value="@cf/mistral/mistral-7b-instruct-v0.1" ' + sel3 + '>Mistral 7B</option>' +
  1004. '</select>' +
  1005. '</div>';
  1006. document.getElementById('cf-list').appendChild(div);
  1007. }
  1008. function getCFValues() {
  1009. return Array.from(document.querySelectorAll('.cf-item')).map(item => ({
  1010. comment: item.querySelector('.cf-comment').value.trim(),
  1011. account_id: item.querySelector('.cf-acc').value.trim(),
  1012. api_token: item.querySelector('.cf-token').value.trim(),
  1013. model: item.querySelector('.cf-model').value
  1014. })).filter(c => c.account_id && c.api_token);
  1015. }
  1016. async function loadConfigUI() {
  1017. let res = await fetch('/api/config');
  1018. let j = await res.json();
  1019. let db = j.database || {};
  1020. if(j.db_conn && !db.host) {
  1021. let parts = j.db_conn.split(' ');
  1022. parts.forEach(p => {
  1023. let kv = p.split('=');
  1024. if(kv.length === 2) {
  1025. if(kv[0]==='host') db.host = kv[1];
  1026. if(kv[0]==='port') db.port = kv[1];
  1027. if(kv[0]==='user') db.user = kv[1];
  1028. if(kv[0]==='password') db.password = kv[1];
  1029. if(kv[0]==='dbname') db.dbname = kv[1];
  1030. if(kv[0]==='sslmode') db.sslmode = kv[1];
  1031. }
  1032. });
  1033. }
  1034. document.getElementById('db-host').value = db.host || '';
  1035. document.getElementById('db-port').value = db.port || '';
  1036. document.getElementById('db-user').value = db.user || '';
  1037. document.getElementById('db-pass').value = db.password || '';
  1038. document.getElementById('db-name').value = db.dbname || '';
  1039. document.getElementById('db-ssl').value = db.sslmode || '';
  1040. document.getElementById('cfg-port').value = j.web_ui_port || '';
  1041. document.getElementById('cfg-depth').value = j.max_depth || 50;
  1042. document.getElementById('cfg-workers').value = j.max_workers || 60;
  1043. document.getElementById('cfg-pages').value = j.max_pages_per_host || 10000;
  1044. document.getElementById('cfg-sh').value = j.schedule_hours || 0;
  1045. document.getElementById('cfg-sm').value = j.schedule_minutes || 0;
  1046. document.getElementById('cfg-onstart').checked = j.scan_on_start || false;
  1047. document.getElementById('cfg-prompt').value = j.ai_prompt || '';
  1048. document.getElementById('dns-list').innerHTML = '';
  1049. document.getElementById('zones-list').innerHTML = '';
  1050. document.getElementById('core-list').innerHTML = '';
  1051. document.getElementById('cf-list').innerHTML = '';
  1052. (j.start_servers || []).forEach(v => addListInput('dns-list', v));
  1053. (j.zone_names || []).forEach(v => addListInput('zones-list', v));
  1054. (j.core_services || []).forEach(v => addListInput('core-list', v));
  1055. (j.cf_credentials || []).forEach(c => addCFInput(c.comment, c.account_id, c.api_token, c.model));
  1056. if((j.start_servers||[]).length===0) addListInput('dns-list');
  1057. if((j.zone_names||[]).length===0) addListInput('zones-list');
  1058. if((j.core_services||[]).length===0) addListInput('core-list');
  1059. if((j.cf_credentials||[]).length===0) addCFInput();
  1060. }
  1061. async function saveConfig() {
  1062. let payload = {
  1063. database: {
  1064. host: document.getElementById('db-host').value,
  1065. port: document.getElementById('db-port').value,
  1066. user: document.getElementById('db-user').value,
  1067. password: document.getElementById('db-pass').value,
  1068. dbname: document.getElementById('db-name').value,
  1069. sslmode: document.getElementById('db-ssl').value,
  1070. },
  1071. web_ui_port: document.getElementById('cfg-port').value,
  1072. max_depth: parseInt(document.getElementById('cfg-depth').value),
  1073. max_workers: parseInt(document.getElementById('cfg-workers').value),
  1074. max_pages_per_host: parseInt(document.getElementById('cfg-pages').value),
  1075. schedule_hours: parseInt(document.getElementById('cfg-sh').value),
  1076. schedule_minutes: parseInt(document.getElementById('cfg-sm').value),
  1077. scan_on_start: document.getElementById('cfg-onstart').checked,
  1078. ai_prompt: document.getElementById('cfg-prompt').value,
  1079. start_servers: getListValues('dns-list'),
  1080. zone_names: getListValues('zones-list'),
  1081. core_services: getListValues('core-list'),
  1082. cf_credentials: getCFValues()
  1083. };
  1084. let res = await fetch('/api/config', {
  1085. method: 'POST',
  1086. headers: {'Content-Type': 'application/json'},
  1087. body: JSON.stringify(payload)
  1088. });
  1089. if(res.ok) alert("Configuration Saved Successfully! Changes apply to next run.");
  1090. else alert("Failed to save configuration.");
  1091. }
  1092. function switchMainTab(t) {
  1093. document.getElementById('tab-dash').classList.remove('active');
  1094. document.getElementById('tab-set').classList.remove('active');
  1095. document.getElementById('dash-panel').style.display = 'none';
  1096. document.getElementById('settings-panel').style.display = 'none';
  1097. if(t === 'dash') {
  1098. document.getElementById('tab-dash').classList.add('active');
  1099. document.getElementById('dash-panel').style.display = 'block';
  1100. } else {
  1101. document.getElementById('tab-set').classList.add('active');
  1102. document.getElementById('settings-panel').style.display = 'flex';
  1103. loadConfigUI();
  1104. }
  1105. }
  1106. function switchSetTab(t) {
  1107. document.querySelectorAll('.sub-tabs button').forEach(b => b.classList.remove('active'));
  1108. document.getElementById('sub-'+t).classList.add('active');
  1109. document.querySelectorAll('.set-section').forEach(s => s.classList.remove('active'));
  1110. document.getElementById('set-'+t).classList.add('active');
  1111. }
  1112. setInterval(fetchStats, 2000);
  1113. setInterval(fetchLogs, 1000);
  1114. fetchStats(); fetchLogs();
  1115. </script>
  1116. </body>
  1117. </html>
  1118. `