mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-03-31 16:20:28 +02:00
224 lines
5.7 KiB
Go
224 lines
5.7 KiB
Go
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
|
||
}
|