mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-01 12:01:46 +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
|
|
}
|