package handler import ( "encoding/base64" "encoding/json" "fmt" "net/http" "net/url" "os" "strings" "time" "cyberstrike-ai/internal/config" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type FofaHandler struct { cfg *config.Config logger *zap.Logger client *http.Client } func NewFofaHandler(cfg *config.Config, logger *zap.Logger) *FofaHandler { return &FofaHandler{ cfg: cfg, logger: logger, client: &http.Client{Timeout: 30 * time.Second}, } } type fofaSearchRequest struct { Query string `json:"query" binding:"required"` Size int `json:"size,omitempty"` Page int `json:"page,omitempty"` Fields string `json:"fields,omitempty"` Full bool `json:"full,omitempty"` } type fofaAPIResponse struct { Error bool `json:"error"` ErrMsg string `json:"errmsg"` Size int `json:"size"` Page int `json:"page"` Total int `json:"total"` Mode string `json:"mode"` Query string `json:"query"` Results [][]interface{} `json:"results"` } type fofaSearchResponse struct { Query string `json:"query"` Size int `json:"size"` Page int `json:"page"` Total int `json:"total"` Fields []string `json:"fields"` ResultsCount int `json:"results_count"` Results []map[string]interface{} `json:"results"` } func (h *FofaHandler) resolveCredentials() (email, apiKey string) { // 优先环境变量(便于容器部署),其次配置文件 email = strings.TrimSpace(os.Getenv("FOFA_EMAIL")) apiKey = strings.TrimSpace(os.Getenv("FOFA_API_KEY")) if email != "" && apiKey != "" { return email, apiKey } if h.cfg != nil { if email == "" { email = strings.TrimSpace(h.cfg.FOFA.Email) } if apiKey == "" { apiKey = strings.TrimSpace(h.cfg.FOFA.APIKey) } } return email, apiKey } func (h *FofaHandler) resolveBaseURL() string { if h.cfg != nil { if v := strings.TrimSpace(h.cfg.FOFA.BaseURL); v != "" { return v } } return "https://fofa.info/api/v1/search/all" } // Search FOFA 查询(后端代理,避免前端暴露 key) func (h *FofaHandler) Search(c *gin.Context) { var req fofaSearchRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) return } req.Query = strings.TrimSpace(req.Query) if req.Query == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "query 不能为空"}) return } if req.Size <= 0 { req.Size = 100 } if req.Page <= 0 { req.Page = 1 } // FOFA 接口 size 上限和账户权限相关,这里只做一个合理的保护 if req.Size > 10000 { req.Size = 10000 } if req.Fields == "" { req.Fields = "host,ip,port,domain,title,protocol,country,province,city,server" } email, apiKey := h.resolveCredentials() if email == "" || apiKey == "" { c.JSON(http.StatusBadRequest, gin.H{ "error": "FOFA 未配置:请在系统设置中填写 FOFA Email/API Key,或设置环境变量 FOFA_EMAIL/FOFA_API_KEY", "need": []string{"fofa.email", "fofa.api_key"}, "env_key": []string{"FOFA_EMAIL", "FOFA_API_KEY"}, }) return } baseURL := h.resolveBaseURL() qb64 := base64.StdEncoding.EncodeToString([]byte(req.Query)) u, err := url.Parse(baseURL) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "FOFA base_url 无效: " + err.Error()}) return } params := u.Query() params.Set("email", email) params.Set("key", apiKey) params.Set("qbase64", qb64) params.Set("size", fmt.Sprintf("%d", req.Size)) params.Set("page", fmt.Sprintf("%d", req.Page)) params.Set("fields", strings.TrimSpace(req.Fields)) if req.Full { params.Set("full", "true") } else { // 明确传 false,便于排查 params.Set("full", "false") } u.RawQuery = params.Encode() httpReq, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, u.String(), nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败: " + err.Error()}) return } resp, err := h.client.Do(httpReq) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": "请求 FOFA 失败: " + err.Error()}) return } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("FOFA 返回非 2xx: %d", resp.StatusCode)}) return } var apiResp fofaAPIResponse if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": "解析 FOFA 响应失败: " + err.Error()}) return } if apiResp.Error { msg := strings.TrimSpace(apiResp.ErrMsg) if msg == "" { msg = "FOFA 返回错误" } c.JSON(http.StatusBadGateway, gin.H{"error": msg}) return } fields := splitAndCleanCSV(req.Fields) results := make([]map[string]interface{}, 0, len(apiResp.Results)) for _, row := range apiResp.Results { item := make(map[string]interface{}, len(fields)) for i, f := range fields { if i < len(row) { item[f] = row[i] } else { item[f] = nil } } results = append(results, item) } c.JSON(http.StatusOK, fofaSearchResponse{ Query: req.Query, Size: apiResp.Size, Page: apiResp.Page, Total: apiResp.Total, Fields: fields, ResultsCount: len(results), Results: results, }) } func splitAndCleanCSV(s string) []string { parts := strings.Split(s, ",") out := make([]string, 0, len(parts)) seen := make(map[string]struct{}, len(parts)) for _, p := range parts { v := strings.TrimSpace(p) if v == "" { continue } if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} out = append(out, v) } return out }