125 lines
3.9 KiB
Go
125 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var ErrUnauthorized = errors.New("searxng: bearer rejected")
|
|
|
|
type searxngAPI struct {
|
|
baseURL string
|
|
token func(context.Context) (string, error)
|
|
client *http.Client
|
|
}
|
|
|
|
type searchParams struct {
|
|
Query string
|
|
Categories string
|
|
Engines string
|
|
Language string
|
|
TimeRange string
|
|
SafeSearch *int
|
|
Page int
|
|
Limit int
|
|
}
|
|
|
|
type searxngSearchResponse struct {
|
|
Query string `json:"query"`
|
|
NumberOfResults int `json:"number_of_results"`
|
|
Results []searxngResult `json:"results"`
|
|
Suggestions []string `json:"suggestions,omitempty"`
|
|
Answers []string `json:"answers,omitempty"`
|
|
Corrections []string `json:"corrections,omitempty"`
|
|
}
|
|
|
|
type searxngResult struct {
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
Content string `json:"content,omitempty"`
|
|
Engine string `json:"engine,omitempty"`
|
|
Engines []string `json:"engines,omitempty"`
|
|
Category string `json:"category,omitempty"`
|
|
Score float64 `json:"score,omitempty"`
|
|
PublishedDate string `json:"publishedDate,omitempty"`
|
|
ImgSrc string `json:"img_src,omitempty"`
|
|
Thumbnail string `json:"thumbnail,omitempty"`
|
|
}
|
|
|
|
func (s *searxngAPI) search(ctx context.Context, p searchParams) (searxngSearchResponse, error) {
|
|
q := url.Values{
|
|
"format": []string{"json"},
|
|
"q": []string{p.Query},
|
|
}
|
|
setIf(q, "categories", p.Categories)
|
|
setIf(q, "engines", p.Engines)
|
|
setIf(q, "language", p.Language)
|
|
setIf(q, "time_range", p.TimeRange)
|
|
if p.SafeSearch != nil {
|
|
q.Set("safesearch", strconv.Itoa(*p.SafeSearch))
|
|
}
|
|
if p.Page > 0 {
|
|
q.Set("pageno", strconv.Itoa(p.Page))
|
|
}
|
|
|
|
full := s.baseURL + "/search?" + q.Encode()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, full, nil)
|
|
if err != nil {
|
|
return searxngSearchResponse{}, err
|
|
}
|
|
tok, err := s.token(ctx)
|
|
if err != nil {
|
|
return searxngSearchResponse{}, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return searxngSearchResponse{}, err
|
|
}
|
|
defer func() { _, _ = io.Copy(io.Discard, resp.Body); _ = resp.Body.Close() }()
|
|
if err := checkStatus(resp, req); err != nil {
|
|
return searxngSearchResponse{}, err
|
|
}
|
|
if ct := resp.Header.Get("Content-Type"); ct != "" && !strings.Contains(ct, "application/json") {
|
|
return searxngSearchResponse{}, fmt.Errorf("searxng: %s returned %q, not JSON; ensure SearXNG enables format=json and Caddy is not serving a login page", req.URL.Path, ct)
|
|
}
|
|
|
|
var out searxngSearchResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
return searxngSearchResponse{}, fmt.Errorf("searxng: decode JSON search response: %w", err)
|
|
}
|
|
if p.Limit > 0 && len(out.Results) > p.Limit {
|
|
out.Results = out.Results[:p.Limit]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func checkStatus(resp *http.Response, req *http.Request) error {
|
|
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
|
return fmt.Errorf("%w by %s (try `sherlock logout searxng` and retry; Caddy must accept the OAuth bearer)", ErrUnauthorized, req.URL.Host)
|
|
}
|
|
if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusSeeOther || resp.StatusCode == http.StatusTemporaryRedirect || resp.StatusCode == http.StatusPermanentRedirect {
|
|
return fmt.Errorf("searxng: %s %s redirected to %q; Caddy is likely requiring browser-session auth instead of accepting the OAuth bearer", req.Method, req.URL.Path, resp.Header.Get("Location"))
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return fmt.Errorf("searxng: %s %s: HTTP %d: %s", req.Method, req.URL.Path, resp.StatusCode, strings.TrimSpace(string(body)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setIf(q url.Values, k, v string) {
|
|
if v != "" {
|
|
q.Set(k, v)
|
|
}
|
|
}
|