kafka/internal/search/request_params.go
ashisgreat22 fcd9be16df refactor: remove SearXNG references and rename binary to kafka
- Rename cmd/searxng-go to cmd/kafka
- Remove all SearXNG references from source comments while keeping
  "SearXNG-compatible API" in user-facing docs
- Update binary paths in README, CLAUDE.md, and Dockerfile
- Update log message to "kafka starting"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 01:47:03 +01:00

208 lines
5 KiB
Go

package search
import (
"errors"
"net/http"
"regexp"
"strconv"
"strings"
)
var languageCodeRe = regexp.MustCompile(`^[a-z]{2,3}(-[a-zA-Z]{2})?$`)
func ParseSearchRequest(r *http.Request) (SearchRequest, error) {
// Supports both GET and POST and relies on form values for routing.
if err := r.ParseForm(); err != nil {
return SearchRequest{}, errors.New("invalid request: cannot parse form")
}
format := strings.ToLower(r.FormValue("format"))
switch OutputFormat(format) {
case FormatJSON, FormatCSV, FormatRSS, FormatHTML:
// explicit format — use as-is
default:
// No format specified: default to HTML for browser requests, JSON for API clients.
accept := r.Header.Get("Accept")
if strings.Contains(accept, "text/html") {
format = string(FormatHTML)
} else {
format = string(FormatJSON)
}
}
q := r.FormValue("q")
if strings.TrimSpace(q) == "" {
return SearchRequest{}, errors.New("missing required parameter: q")
}
pageno := 1
if s := strings.TrimSpace(r.FormValue("pageno")); s != "" {
n, err := strconv.Atoi(s)
if err != nil || n < 1 {
return SearchRequest{}, errors.New("invalid parameter: pageno")
}
pageno = n
}
// MVP defaults.
safesearch := 0
if s := strings.TrimSpace(r.FormValue("safesearch")); s != "" {
n, err := strconv.Atoi(s)
if err != nil || n < 0 || n > 2 {
return SearchRequest{}, errors.New("invalid parameter: safesearch")
}
safesearch = n
}
var timeRange *string
if tr := strings.TrimSpace(r.FormValue("time_range")); tr != "" && tr != "None" {
switch tr {
case "day", "week", "month", "year":
tt := tr
timeRange = &tt
default:
return SearchRequest{}, errors.New("invalid parameter: time_range")
}
}
var timeoutLimit *float64
if s := strings.TrimSpace(r.FormValue("timeout_limit")); s != "" && s != "None" {
v, err := strconv.ParseFloat(s, 64)
if err != nil || v <= 0 {
return SearchRequest{}, errors.New("invalid parameter: timeout_limit")
}
timeoutLimit = &v
}
language := strings.TrimSpace(r.FormValue("language"))
if language == "" {
language = "auto"
}
switch language {
case "auto", "all":
// ok
default:
if !languageCodeRe.MatchString(language) {
return SearchRequest{}, errors.New("invalid parameter: language")
}
}
// engines is an explicit list of engine names.
engines := splitCSV(strings.TrimSpace(r.FormValue("engines")))
// categories and category_<name> params mirror the webadapter parsing.
// We don't validate against a registry here; we just preserve the requested values.
catSet := map[string]bool{}
if catsParam := strings.TrimSpace(r.FormValue("categories")); catsParam != "" {
for _, cat := range splitCSV(catsParam) {
catSet[cat] = true
}
}
for k, v := range r.Form {
if !strings.HasPrefix(k, "category_") {
continue
}
category := strings.TrimPrefix(k, "category_")
if category == "" {
continue
}
val := ""
if len(v) > 0 {
val = strings.TrimSpace(v[0])
}
if val == "" || val != "off" {
catSet[category] = true
} else {
delete(catSet, category)
}
}
categories := make([]string, 0, len(catSet))
for c := range catSet {
categories = append(categories, c)
}
if len(categories) == 0 {
categories = []string{"general"}
}
// Parse engine_data-<engine>-<key>=<value> parameters.
engineData := map[string]map[string]string{}
for k, v := range r.Form {
if !strings.HasPrefix(k, "engine_data-") {
continue
}
parts := strings.SplitN(k, "-", 3) // engine_data-<engine>-<key>
if len(parts) != 3 {
continue
}
engine := parts[1]
key := parts[2]
// For HTML forms, r.Form[k] can contain multiple values; keep first.
val := ""
if len(v) > 0 {
val = v[0]
}
if _, ok := engineData[engine]; !ok {
engineData[engine] = map[string]string{}
}
engineData[engine][key] = val
}
accessToken := parseAccessToken(r)
return SearchRequest{
Format: OutputFormat(format),
Query: q,
Pageno: pageno,
Safesearch: safesearch,
TimeRange: timeRange,
TimeoutLimit: timeoutLimit,
Language: language,
Engines: engines,
Categories: categories,
EngineData: engineData,
AccessToken: accessToken,
}, nil
}
func splitCSV(s string) []string {
if s == "" {
return nil
}
raw := strings.Split(s, ",")
out := make([]string, 0, len(raw))
for _, item := range raw {
item = strings.TrimSpace(item)
if item == "" {
continue
}
out = append(out, item)
}
return out
}
func parseAccessToken(r *http.Request) string {
// Supported sources (first non-empty wins):
// - `Authorization: Bearer <token>`
// - `X-Search-Token` / `X-Brave-Access-Token`
// - `token` form value
if auth := r.Header.Get("Authorization"); auth != "" {
const prefix = "Bearer "
if len(auth) > len(prefix) && auth[:len(prefix)] == prefix {
return strings.TrimSpace(auth[len(prefix):])
}
}
if v := strings.TrimSpace(r.Header.Get("X-Search-Token")); v != "" {
return v
}
if v := strings.TrimSpace(r.Header.Get("X-Brave-Access-Token")); v != "" {
return v
}
if v := strings.TrimSpace(r.FormValue("token")); v != "" {
return v
}
return ""
}