feat: add OpenSearch XML endpoint
- Serve /opensearch.xml with configurable base URL - Browsers can now add gosearch as a search engine from the address bar - Configurable via [server] base_url or BASE_URL env var - XML template embedded in the binary via go:embed - Added base_url to config.example.toml
This commit is contained in:
parent
3caf702c4f
commit
4ec600f6c0
6 changed files with 51 additions and 0 deletions
|
|
@ -63,6 +63,7 @@ func main() {
|
||||||
mux.HandleFunc("/", h.Index)
|
mux.HandleFunc("/", h.Index)
|
||||||
mux.HandleFunc("/healthz", h.Healthz)
|
mux.HandleFunc("/healthz", h.Healthz)
|
||||||
mux.HandleFunc("/search", h.Search)
|
mux.HandleFunc("/search", h.Search)
|
||||||
|
mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL))
|
||||||
|
|
||||||
// Serve embedded static files (CSS, JS, images).
|
// Serve embedded static files (CSS, JS, images).
|
||||||
staticFS, err := views.StaticFS()
|
staticFS, err := views.StaticFS()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ port = 8080
|
||||||
# HTTP timeout for engine and upstream calls (env: HTTP_TIMEOUT)
|
# HTTP timeout for engine and upstream calls (env: HTTP_TIMEOUT)
|
||||||
http_timeout = "10s"
|
http_timeout = "10s"
|
||||||
|
|
||||||
|
# Public base URL for OpenSearch XML (env: BASE_URL)
|
||||||
|
# Set this so browsers can add gosearch as a search engine.
|
||||||
|
# Example: "https://search.example.com"
|
||||||
|
base_url = ""
|
||||||
|
|
||||||
[upstream]
|
[upstream]
|
||||||
# URL of an upstream SearXNG instance for unported engines (env: UPSTREAM_SEARXNG_URL)
|
# URL of an upstream SearXNG instance for unported engines (env: UPSTREAM_SEARXNG_URL)
|
||||||
# Leave empty to run without an upstream proxy.
|
# Leave empty to run without an upstream proxy.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ type Config struct {
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
HTTPTimeout string `toml:"http_timeout"`
|
HTTPTimeout string `toml:"http_timeout"`
|
||||||
|
BaseURL string `toml:"base_url"` // Public URL for OpenSearch XML (e.g. "https://search.example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpstreamConfig struct {
|
type UpstreamConfig struct {
|
||||||
|
|
@ -158,6 +159,9 @@ func applyEnvOverrides(cfg *Config) {
|
||||||
if v := os.Getenv("RATE_LIMIT_CLEANUP_INTERVAL"); v != "" {
|
if v := os.Getenv("RATE_LIMIT_CLEANUP_INTERVAL"); v != "" {
|
||||||
cfg.RateLimit.CleanupInterval = v
|
cfg.RateLimit.CleanupInterval = v
|
||||||
}
|
}
|
||||||
|
if v := os.Getenv("BASE_URL"); v != "" {
|
||||||
|
cfg.Server.BaseURL = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPTimeout parses the configured timeout string into a time.Duration.
|
// HTTPTimeout parses the configured timeout string into a time.Duration.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,19 @@ func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenSearch serves the OpenSearch description XML.
|
||||||
|
func (h *Handler) OpenSearch(baseURL string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
xml, err := views.OpenSearchXML(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/opensearchdescription+xml; charset=utf-8")
|
||||||
|
w.Write(xml)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||||
// For HTML format with no query, redirect to homepage.
|
// For HTML format with no query, redirect to homepage.
|
||||||
if r.FormValue("q") == "" && (r.FormValue("format") == "" || r.FormValue("format") == "html") {
|
if r.FormValue("q") == "" && (r.FormValue("format") == "" || r.FormValue("format") == "html") {
|
||||||
|
|
|
||||||
16
internal/views/templates/opensearch.xml
Normal file
16
internal/views/templates/opensearch.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
<ShortName>gosearch</ShortName>
|
||||||
|
<Description>A privacy-respecting, open metasearch engine</Description>
|
||||||
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
|
<OutputEncoding>UTF-8</OutputEncoding>
|
||||||
|
<LongName>gosearch — Privacy-respecting metasearch</LongName>
|
||||||
|
<Image width="16" height="16" type="image/svg+xml">/static/img/favicon.svg</Image>
|
||||||
|
<Contact>https://git.ashisgreat.xyz/penal-colony/gosearch</Contact>
|
||||||
|
<Url type="text/html" method="GET" template="{baseUrl}/search?q={searchTerms}&format=html">
|
||||||
|
<Param name="pageno" value="{startPage?}" />
|
||||||
|
<Param name="language" value="{language?}" />
|
||||||
|
</Url>
|
||||||
|
<Url type="application/json" method="GET" template="{baseUrl}/search?q={searchTerms}&format=json" />
|
||||||
|
<Query role="example" searchTerms="golang" />
|
||||||
|
</OpenSearchDescription>
|
||||||
|
|
@ -80,6 +80,18 @@ func StaticFS() (fs.FS, error) {
|
||||||
return fs.Sub(staticFS, "static")
|
return fs.Sub(staticFS, "static")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenSearchXML returns the OpenSearch description XML with {baseUrl}
|
||||||
|
// replaced by the provided base URL.
|
||||||
|
func OpenSearchXML(baseURL string) ([]byte, error) {
|
||||||
|
tmplFS, _ := fs.Sub(templatesFS, "templates")
|
||||||
|
data, err := fs.ReadFile(tmplFS, "opensearch.xml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := strings.ReplaceAll(string(data), "{baseUrl}", baseURL)
|
||||||
|
return []byte(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
// FromResponse builds PageData from a search response and request params.
|
// FromResponse builds PageData from a search response and request params.
|
||||||
func FromResponse(resp contracts.SearchResponse, query string, pageno int) PageData {
|
func FromResponse(resp contracts.SearchResponse, query string, pageno int) PageData {
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue