mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 11:14:35 +03:00
272 lines
6.3 KiB
Go
272 lines
6.3 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/sartoopjj/thefeed/internal/client"
|
|
)
|
|
|
|
func (s *Server) handleScannerPresets(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", 405)
|
|
return
|
|
}
|
|
src := defaultScannerPresets
|
|
var lines []string
|
|
for _, line := range strings.Split(src, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" && !strings.HasPrefix(line, "#") {
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
writeJSON(w, lines)
|
|
}
|
|
|
|
func (s *Server) handleScannerStart(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", 405)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Targets []string `json:"targets"`
|
|
MaxIPs int `json:"maxIPs"`
|
|
RateLimit int `json:"rateLimit"`
|
|
Timeout float64 `json:"timeout"`
|
|
ExpandSubnet bool `json:"expandSubnet"`
|
|
QueryMode string `json:"queryMode"`
|
|
ProfileID string `json:"profileId"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", 400)
|
|
return
|
|
}
|
|
|
|
if len(req.Targets) == 0 {
|
|
http.Error(w, "targets required", 400)
|
|
return
|
|
}
|
|
|
|
// Resolve profile config.
|
|
pl, err := s.loadProfiles()
|
|
if err != nil || pl == nil {
|
|
http.Error(w, "no profiles configured", 400)
|
|
return
|
|
}
|
|
|
|
var profileCfg *Config
|
|
if req.ProfileID != "" {
|
|
for _, p := range pl.Profiles {
|
|
if p.ID == req.ProfileID {
|
|
profileCfg = &p.Config
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if profileCfg == nil {
|
|
// Fall back to active profile.
|
|
for _, p := range pl.Profiles {
|
|
if p.ID == pl.Active {
|
|
profileCfg = &p.Config
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if profileCfg == nil {
|
|
http.Error(w, "no profile found", 400)
|
|
return
|
|
}
|
|
|
|
if profileCfg.Domain == "" || profileCfg.Key == "" {
|
|
http.Error(w, "profile missing domain or passphrase", 400)
|
|
return
|
|
}
|
|
|
|
queryMode := req.QueryMode
|
|
if queryMode == "" {
|
|
queryMode = profileCfg.QueryMode
|
|
}
|
|
|
|
cfg := client.ScannerConfig{
|
|
Targets: req.Targets,
|
|
MaxIPs: req.MaxIPs,
|
|
RateLimit: req.RateLimit,
|
|
Timeout: req.Timeout,
|
|
ExpandSubnet: req.ExpandSubnet,
|
|
QueryMode: queryMode,
|
|
Domain: profileCfg.Domain,
|
|
Passphrase: profileCfg.Key,
|
|
}
|
|
|
|
s.scanner.SetLogFunc(func(msg string) {
|
|
s.addLog(msg)
|
|
})
|
|
|
|
if err := s.scanner.Start(cfg); err != nil {
|
|
http.Error(w, fmt.Sprintf("start scanner: %v", err), 400)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleScannerStop(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", 405)
|
|
return
|
|
}
|
|
s.scanner.Stop()
|
|
writeJSON(w, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleScannerPause(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", 405)
|
|
return
|
|
}
|
|
s.scanner.Pause()
|
|
writeJSON(w, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleScannerResume(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", 405)
|
|
return
|
|
}
|
|
s.scanner.Resume()
|
|
writeJSON(w, map[string]any{"ok": true})
|
|
}
|
|
|
|
func (s *Server) handleScannerProgress(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", 405)
|
|
return
|
|
}
|
|
prog := s.scanner.Progress()
|
|
writeJSON(w, prog)
|
|
}
|
|
|
|
func (s *Server) handleScannerApply(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", 405)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Resolvers []string `json:"resolvers"`
|
|
Mode string `json:"mode"` // "append" or "overwrite"
|
|
ProfileID string `json:"profileId"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", 400)
|
|
return
|
|
}
|
|
|
|
// If no resolvers explicitly provided, pull them from scanner results.
|
|
resolvers := req.Resolvers
|
|
if len(resolvers) == 0 {
|
|
prog := s.scanner.Progress()
|
|
for _, r := range prog.Results {
|
|
resolvers = append(resolvers, r.IP)
|
|
}
|
|
}
|
|
if len(resolvers) == 0 {
|
|
http.Error(w, "no resolvers to apply", 400)
|
|
return
|
|
}
|
|
|
|
// Make sure resolvers have :53 suffix.
|
|
for i, r := range resolvers {
|
|
if !strings.Contains(r, ":") {
|
|
resolvers[i] = r + ":53"
|
|
}
|
|
}
|
|
|
|
// Determine which profile to apply to.
|
|
pl, _ := s.loadProfiles()
|
|
if pl == nil {
|
|
http.Error(w, "no profiles configured", 400)
|
|
return
|
|
}
|
|
|
|
targetProfileID := req.ProfileID
|
|
if targetProfileID == "" {
|
|
targetProfileID = pl.Active
|
|
}
|
|
|
|
var targetIdx int = -1
|
|
for i, p := range pl.Profiles {
|
|
if p.ID == targetProfileID {
|
|
targetIdx = i
|
|
break
|
|
}
|
|
}
|
|
if targetIdx < 0 {
|
|
http.Error(w, "profile not found", 400)
|
|
return
|
|
}
|
|
|
|
var newResolvers []string
|
|
if req.Mode == "overwrite" {
|
|
newResolvers = resolvers
|
|
} else {
|
|
// Append — deduplicate.
|
|
seen := make(map[string]bool)
|
|
for _, r := range pl.Profiles[targetIdx].Config.Resolvers {
|
|
seen[r] = true
|
|
newResolvers = append(newResolvers, r)
|
|
}
|
|
for _, r := range resolvers {
|
|
if !seen[r] {
|
|
newResolvers = append(newResolvers, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
pl.Profiles[targetIdx].Config.Resolvers = newResolvers
|
|
if err := s.saveProfiles(pl); err != nil {
|
|
http.Error(w, fmt.Sprintf("save profiles: %v", err), 500)
|
|
return
|
|
}
|
|
|
|
// If this is the active profile, also update config + fetcher.
|
|
if targetProfileID == pl.Active {
|
|
s.mu.Lock()
|
|
cfg := s.config
|
|
s.mu.Unlock()
|
|
if cfg != nil {
|
|
cfg.Resolvers = newResolvers
|
|
_ = s.saveConfig(cfg)
|
|
s.mu.Lock()
|
|
s.config = cfg
|
|
s.mu.Unlock()
|
|
}
|
|
if err := s.initFetcher(); err != nil {
|
|
http.Error(w, fmt.Sprintf("init fetcher: %v", err), 500)
|
|
return
|
|
}
|
|
// The scanner already verified these resolvers, so skip the initial
|
|
// health-check scan — set them as active directly, start only the
|
|
// periodic checker, and fetch metadata immediately.
|
|
s.mu.RLock()
|
|
fetcher := s.fetcher
|
|
checker := s.checker
|
|
ctx := s.fetcherCtx
|
|
s.mu.RUnlock()
|
|
if fetcher != nil {
|
|
fetcher.SetActiveResolvers(newResolvers)
|
|
s.saveLastScan(newResolvers)
|
|
}
|
|
if checker != nil && ctx != nil {
|
|
checker.StartPeriodic(ctx)
|
|
}
|
|
go s.refreshMetadataOnly()
|
|
}
|
|
|
|
s.addLog(fmt.Sprintf("Scanner resolvers applied: %d resolvers (%s) to profile %s", len(resolvers), req.Mode, pl.Profiles[targetIdx].Nickname))
|
|
writeJSON(w, map[string]any{"ok": true, "count": len(newResolvers)})
|
|
}
|