diff --git a/search-proxy/README.md b/search-proxy/README.md index 7d8e084..0e3b323 100644 --- a/search-proxy/README.md +++ b/search-proxy/README.md @@ -1,25 +1,27 @@ # search-proxy -香港服务器上的搜索引擎代理。本应用通过 HTTP API 调用,直连 Google/Bing/DDG。 +部署在可直连 Google/Bing/DuckDuckGo 的云服务器上,通过 HTTP API 提供搜索引擎代理。 ## 部署 ```bash -# 1. 拷贝到服务器 +# 1. 上传到服务器 scp search-proxy user@your-server:/opt/search-proxy/ -# 2. 编译(如果有 Go 环境) +# 2. 编译 cd /opt/search-proxy && go build -o search-proxy . -# 或者本地交叉编译后上传: +# 本地交叉编译: # GOOS=linux GOARCH=amd64 go build -o search-proxy . -# 3. 测试 -./search-proxy & -curl "http://localhost:8080/health" -curl "http://localhost:8080/search?q=hello&engine=google&n=5" +# 3. 启动(默认端口 18080) +PORT=18080 ./search-proxy & -# 4. 安装 systemd 服务(可选,开机自启) +# 4. 测试 +curl "http://localhost:18080/health" +curl "http://localhost:18080/search?q=hello&n=5" + +# 5. 安装 systemd 服务 sudo cp search-proxy.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now search-proxy @@ -30,16 +32,15 @@ sudo systemctl enable --now search-proxy ``` GET /search?q=关键词&engine=google&n=10 -engine: google | bing | ddg | all +engine: google | bing | ddg | baidu | all (默认) n: 1-20,默认10 缓存: 5分钟 +限流: 30 req/s GET /health -→ {"status":"ok"} +→ {"status":"ok","engines":["google","bing","ddg","baidu","all"],"time":"..."} ``` ## 本应用配置 -在设置面板的代理输入框中填入:`http://你的服务器IP:8080` - -或者通过代理设置:在 search-proxy 前挂 nginx 反代加 HTTPS。 +设置面板 → 代理设置 → 填入 `http://服务器IP:18080` → 重启 diff --git a/search-proxy/main.go b/search-proxy/main.go index a506b71..26ee11c 100644 --- a/search-proxy/main.go +++ b/search-proxy/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "io" @@ -8,112 +9,176 @@ import ( "net/http" "net/url" "os" + "os/signal" "regexp" "strings" "sync" + "sync/atomic" + "syscall" "time" ) -var cache = sync.Map{} - -type cacheEntry struct { - Results []SearchResult `json:"-"` - ExpiresAt int64 `json:"-"` +// ── 限流器 ── +type RateLimiter struct { + tokens atomic.Int64 + capacity int64 + rate int64 // tokens/sec + lastFill atomic.Int64 } -type SearchResult struct { +func newRateLimiter(rate, cap int64) *RateLimiter { + rl := &RateLimiter{rate: rate, capacity: cap} + rl.tokens.Store(cap) + rl.lastFill.Store(time.Now().UnixNano()) + return rl +} + +func (rl *RateLimiter) Allow() bool { + now := time.Now().UnixNano() + last := rl.lastFill.Load() + elapsed := float64(now-last) / 1e9 + newTokens := int64(elapsed * float64(rl.rate)) + if newTokens > 0 { + rl.lastFill.Store(now) + current := rl.tokens.Load() + next := current + newTokens + if next > rl.capacity { + next = rl.capacity + } + rl.tokens.Store(next) + } + for { + current := rl.tokens.Load() + if current <= 0 { + return false + } + if rl.tokens.CompareAndSwap(current, current-1) { + return true + } + } +} + +// ── 缓存 ── +var cache sync.Map + +type cacheEntry struct { + Results []Result `json:"-"` + ExpiresAt int64 `json:"-"` +} + +type Result struct { Title string `json:"title"` URL string `json:"url"` Snippet string `json:"snippet"` Engine string `json:"engine"` } -type SearchResponse struct { - Success bool `json:"success"` - Query string `json:"query"` - Total int `json:"total"` - Results []SearchResult `json:"results"` - Error string `json:"error,omitempty"` +type SearchResp struct { + Success bool `json:"success"` + Query string `json:"query"` + Total int `json:"total"` + Results []Result `json:"results"` + Engine string `json:"engine,omitempty"` + Error string `json:"error,omitempty"` + FromCache bool `json:"from_cache,omitempty"` } -var httpClient = &http.Client{Timeout: 15 * time.Second} -var ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +// ── 全局配置 ── +var ( + httpClient = &http.Client{ + Timeout: 12 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 20, + IdleConnTimeout: 90 * time.Second, + }, + } + ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + acceptLg = "zh-CN,zh;q=0.9,en;q=0.8" + limiter = newRateLimiter(30, 60) // 30 req/s, burst 60 +) -// ── Google ── -func searchGoogle(q string, n int) []SearchResult { - u := fmt.Sprintf("https://www.google.com/search?q=%s&num=%d&hl=zh-CN", url.QueryEscape(q), n) +// ── recovery middleware ── +func recoverWrap(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("PANIC: %v", err) + writeJSON(w, http.StatusInternalServerError, SearchResp{Error: "internal error"}) + } + }() + h(w, r) + } +} + +// ── 引擎实现 ── + +func doGet(u string) (*http.Response, error) { req, _ := http.NewRequest("GET", u, nil) req.Header.Set("User-Agent", ua) - req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9") + req.Header.Set("Accept-Language", acceptLg) + return httpClient.Do(req) +} - resp, err := httpClient.Do(req) +func searchGoogle(q string, n int) []Result { + u := fmt.Sprintf("https://www.google.com/search?q=%s&num=%d&hl=zh-CN", url.QueryEscape(q), n) + resp, err := doGet(u) if err != nil { - log.Printf("Google: %v", err) return nil } defer resp.Body.Close() - if resp.StatusCode != 200 { - log.Printf("Google HTTP %d", resp.StatusCode) return nil } - body, _ := io.ReadAll(resp.Body) html := string(body) - reBlock := regexp.MustCompile(`
]*>([\s\S]*?)
`) + reB := regexp.MustCompile(`]*>([\s\S]*?)
`) - var results []SearchResult - for _, m := range reBlock.FindAllStringSubmatch(html, -1) { - if len(results) >= n { + var out []Result + for _, m := range reB.FindAllStringSubmatch(html, -1) { + if len(out) >= n { break } - tm := reTitle.FindStringSubmatch(m[1]) + tm := reT.FindStringSubmatch(m[1]) if tm == nil { continue } @@ -121,35 +186,30 @@ func searchBing(q string, n int) []SearchResult { if title == "" { continue } - sn := "" - if sm := reSn.FindStringSubmatch(m[1]); sm != nil { - sn = clean(sm[1]) + snip := "" + if sm := reS.FindStringSubmatch(m[1]); sm != nil { + snip = clean(sm[1]) } - results = append(results, SearchResult{Title: title, URL: tm[1], Snippet: sn, Engine: "bing"}) + out = append(out, Result{Title: title, URL: tm[1], Snippet: snip, Engine: "bing"}) } - return results + return out } -// ── DuckDuckGo Lite ── -func searchDDG(q string, n int) []SearchResult { +func searchDDG(q string, n int) []Result { u := fmt.Sprintf("https://lite.duckduckgo.com/lite/?q=%s", url.QueryEscape(q)) - req, _ := http.NewRequest("GET", u, nil) - req.Header.Set("User-Agent", ua) - - resp, err := httpClient.Do(req) + resp, err := doGet(u) if err != nil { return nil } defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) html := string(body) - reLink := regexp.MustCompile(`]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)`) + reL := regexp.MustCompile(`]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)`) - var results []SearchResult - for _, m := range reLink.FindAllStringSubmatch(html, -1) { - if len(results) >= n { + var out []Result + for _, m := range reL.FindAllStringSubmatch(html, -1) { + if len(out) >= n { break } link := decodeEntities(m[1]) @@ -157,11 +217,55 @@ func searchDDG(q string, n int) []SearchResult { if title == "" || strings.Contains(link, "duckduckgo.com") { continue } - results = append(results, SearchResult{Title: title, URL: link, Snippet: "", Engine: "ddg"}) + out = append(out, Result{Title: title, URL: link, Snippet: "", Engine: "ddg"}) } - return results + return out } +func searchBaidu(q string, n int) []Result { + u := fmt.Sprintf("https://www.baidu.com/s?wd=%s&rn=%d", url.QueryEscape(q), n) + resp, err := doGet(u) + if err != nil { + return nil + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + html := string(body) + + reB := regexp.MustCompile(`