searxng 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b8849ed3269f41fc39d8b6274f8b0ae92ade2649dd1f6e5111b1799fec375e5
4
- data.tar.gz: cd78466743cbad58a93d2453f2128e7bfb2f139a05c84eba0c984166da662e2f
3
+ metadata.gz: 0752ce85481fa3a6a5846b4326effa0251ff69e77a32912dc046f06e35542832
4
+ data.tar.gz: 1700a1ad55569d5cbb7137c6b3c6cde6583c8b6198508975e6b66030dfd2bc39
5
5
  SHA512:
6
- metadata.gz: 38818edbabaf94b55bca1e43cbee38fb8bf7d54319d1b2fe3b56a10653f33152011697dd01e9135d1e05fc23f07629a716578dce791b55addc97ca3f471afd92
7
- data.tar.gz: 4a77e1ba98532c75d7da8c5e3da80c5cb53127488c094fa8dfc331de5285afb4a78e5278f332aa92e27d8e489a1273e8e39eea1027d4643988869c9bdd75b610
6
+ metadata.gz: e85f85f3363f0468cef0d8dd6b5318f2d2eba8ef7268e6c607fbbe318a5140347e395400b747c33716f4ee73a2fa99e6af54398e7e20e42dbfb2e1a76f60593a
7
+ data.tar.gz: ff609005b3e6993106c5c29855801675e48b25221456d7f4561f43390a31af1002d98a6abc718d407cd3c87bcede1ff8bed6b587cf166c6fdd5dddfb699aa3a8
data/CHANGELOG.md CHANGED
@@ -1,4 +1,39 @@
1
- # Changelog
1
+ # CHANGELOG
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.2.1 (2026-10-29)
6
+
7
+ - Fix cleartext HTTP proxy fetches to dial the `NetworkPolicy`-pinned IP and set `Host` to the original hostname.
8
+ - Fix unbounded `web_url_read` response buffering with a shared byte ceiling across HTTP, FTP, SFTP, SMB, and Gemini.
9
+ - Fix process-wide `@@url_cache` growth with TTL purge, entry cap, byte budget, and a mutex.
10
+ - Fix Gemini TLS verification by loading the default CA store via `SSLContext#set_params`.
11
+ - Fix SMB credentials leaking through `smbclient` argv; pass the password via `PASSWD`.
12
+ - Fix website HTTP errors mislabeled as SearXNG search failures; include redirect `Location` when present.
13
+ - Fix invalid `time_range` / `safesearch` values being silently dropped; raise `ConfigurationError`.
14
+ - Fix IPFS gateway path traversal via unsafe CID or `..` path segments.
15
+ - Fix `timeoutMs` of `0` disabling deadlines; clamp to `1000..30000`.
16
+ - Fix release gate ignoring lint/test failures (`pipefail`) and dirty staged/untracked trees.
17
+ - Fix release confirmation treating EOF as publish; require an explicit Enter for the default.
18
+ - Require a `CHANGELOG.md` version heading matching `VERSION` before gem push.
19
+ - Fix `searxng serve` swallowing startup errors into an unbounded `StringIO`; reopen stderr to `File::NULL` and print configuration failures.
20
+ - Add `NetworkPolicy` coverage for local NAT64 (`64:ff9b:1::/48`), IPv4-compatible embeds, documentation ranges, and bracketed IPv6 hostnames via `uri.hostname`.
21
+ - Document Ruby `>= 3.4` and that `.onion` / `.i2p` are not supported by `web_url_read`.
22
+ - Redact URL userinfo from fetch error URIs; default `maxLength` to `20000`; honor `SEARXNG_USER_AGENT` for URL reads.
23
+
24
+ ## 0.2.0
25
+
26
+ - MCP: Added `web_url_read` tool (URL fetch + Markdown conversion with pagination/section options).
27
+ - MCP: Added `gemini://` support in `web_url_read` (Gemini fetch + Gemtext to Markdown).
28
+ - MCP: Added `ipfs://` support in `web_url_read` via configurable `IPFS_GATEWAY`.
29
+ - MCP: Added `ftp://`, `sftp://`, and `smb://` support in `web_url_read`.
30
+ - MCP: Added resources `config://server-config` and `help://usage-guide`.
31
+ - MCP: Added startup environment validation for `SEARXNG_URL` and auth variable pairs.
32
+ - Errors: Added richer, user-facing configuration/network/HTTP messages.
33
+ - Client: Added auth env aliases (`AUTH_USERNAME`/`AUTH_PASSWORD`) and proxy auto-detection from `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY` with `NO_PROXY`.
34
+ - Docs: Added protocol support notes for Gemini, IPFS, Tor, I2P, and SOCKS5-related setups.
35
+ - Docs: Added protocol support and environment notes for FTP/SFTP/SMB.
36
+ - Search tool: Improved no-results response to include actionable guidance.
2
37
 
3
38
  ## 0.1.0
4
39
 
data/README.md CHANGED
@@ -1,18 +1,12 @@
1
- # searxng
1
+ # searxng.rb
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/searxng.svg)](https://badge.fury.io/rb/searxng) [![Test Status](https://github.com/amkisko/searxng.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/searxng.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/searxng.rb/graph/badge.svg)](https://codecov.io/gh/amkisko/searxng.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/searxng.svg?v=0.2.1)](https://badge.fury.io/rb/searxng) [![Test Status](https://github.com/amkisko/searxng.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/searxng.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/searxng.rb/graph/badge.svg)](https://app.codecov.io/github/amkisko/searxng.rb)
4
4
 
5
5
  Ruby gem providing a SearXNG HTTP client, CLI (search), and MCP (Model Context Protocol) server for web search. Integrates with MCP-compatible clients like Codex, Cursor, Claude, and other MCP-enabled tools.
6
6
 
7
- Sponsored by [Kisko Labs](https://www.kiskolabs.com).
8
-
9
- <a href="https://www.kiskolabs.com">
10
- <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
11
- </a>
12
-
13
7
  ## Requirements
14
8
 
15
- - **Ruby 3.1 or higher** (Ruby 3.0 and earlier are not supported). For managing Ruby versions, [rbenv](https://github.com/rbenv/rbenv) or [mise](https://mise.jdx.dev/) are recommended; system Ruby may be sufficient if it meets the version requirement.
9
+ - **Ruby 3.4 or higher**. For managing Ruby versions, [rbenv](https://github.com/rbenv/rbenv) or [mise](https://mise.jdx.dev/) are recommended; system Ruby may be sufficient if it meets the version requirement.
16
10
 
17
11
  ## Quick Start
18
12
 
@@ -26,15 +20,63 @@ Or add to your Gemfile:
26
20
  gem "searxng"
27
21
  ```
28
22
 
23
+ **Quick start with a local SearXNG instance:** The repo includes a ready-made [docker-compose](examples/docker-compose.yml) and [settings](examples/searxng/settings.yml) so you can run SearXNG locally (JSON format and limiter preconfigured for the gem). From the repo root: `docker compose -f examples/docker-compose.yml up -d`, then `export SEARXNG_URL="http://localhost:8080"`. See [examples/SETUP.md](examples/SETUP.md) for details.
24
+
25
+ ### CLI
26
+
27
+ ```bash
28
+ searxng search "your query"
29
+ searxng search "ruby" --page 2 --language en --time-range day --json
30
+ ```
31
+
32
+ Options: `--url`, `--page`, `--language`, `--time-range` (day|month|year), `--safesearch` (0|1|2), `--json`.
33
+
34
+ ### Ruby API
35
+
36
+ ```ruby
37
+ require "searxng"
38
+
39
+ client = Searxng::Client.new
40
+ data = client.search("ruby programming", pageno: 1, language: "en")
41
+
42
+ data[:results].each do |r|
43
+ puts r[:title], r[:url], r[:content]
44
+ end
45
+ ```
46
+
29
47
  ### Configuration
30
48
 
31
- - **SEARXNG_URL** (required for remote): Base URL of your SearXNG instance (e.g. `http://localhost:8080` or `https://search.example.com`). Defaults to `http://localhost:8080` when not set.
32
- - **SEARXNG_USER** / **SEARXNG_PASSWORD** (optional): Basic auth credentials if your instance is protected.
49
+ - **SEARXNG_URL**: Base URL of your SearXNG instance (e.g. `http://localhost:8080` or `https://search.example.com`). The client defaults to `http://localhost:8080`; MCP server startup validates that this variable is explicitly set and well-formed.
50
+ - **SEARXNG_USER** / **SEARXNG_PASSWORD** (optional): Basic auth credentials if your instance is protected. `AUTH_USERNAME` / `AUTH_PASSWORD` are also accepted for compatibility. Auth vars must be provided as a pair.
33
51
  - **SEARXNG_CA_FILE** / **SEARXNG_CA_PATH** (optional): Custom CA certificate file or directory for HTTPS. You can also pass `ca_file:`, `ca_path:`, or `verify_mode:` to `Searxng::Client.new`.
34
52
  - **SEARXNG_USER_AGENT** (optional): Custom User-Agent string. The client sends a default identifying the gem; if your instance returns 403 Forbidden (e.g. bot detection), set a custom User-Agent or pass `user_agent:` to `Searxng::Client.new`.
53
+ - **HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY** (optional): Proxy configuration for HTTP(S) requests.
54
+ - **SEARXNG_WEB_URL_SCHEMES** (optional): Comma-separated protocols enabled for `web_url_read`. The secure default is `http,https,ipfs`; opt in to any required subset of `ftp,sftp,smb,gemini`.
55
+ - **SEARXNG_WEB_URL_CREDENTIAL_HOSTS** (optional): Comma-separated host allowlist permitted to receive credentials from the FTP, SFTP, or SMB environment variables. Credentials embedded directly in a URL remain scoped to that URL.
56
+ - **FTP_USER / FTP_PASSWORD** (optional): FTP credentials used only for hosts in `SEARXNG_WEB_URL_CREDENTIAL_HOSTS`.
57
+ - **SFTP_USER / SFTP_PASSWORD** (optional): SFTP credentials used only for hosts in `SEARXNG_WEB_URL_CREDENTIAL_HOSTS`.
58
+ - **SMB_USER / SMB_PASSWORD / SMB_DOMAIN** (optional): SMB credentials/domain used only for hosts in `SEARXNG_WEB_URL_CREDENTIAL_HOSTS`.
59
+ - **IPFS_GATEWAY** (optional): Gateway used for `ipfs://` URLs (default: `https://ipfs.io`).
60
+ - **GEMINI_INSECURE=1** (optional): disables TLS certificate verification for Gemini fetches in `web_url_read`.
35
61
 
36
62
  To fully customize the HTTP client (e.g. custom certs, proxy, timeouts), override `#build_http(uri)` in a subclass, or pass a `configure_http:` callable to the client; it is invoked with the `Net::HTTP` instance and the request URI before each request.
37
63
 
64
+ ## MCP Tools
65
+
66
+ The MCP server provides the following tools:
67
+
68
+ 1. **searxng_web_search** - Web search via SearXNG
69
+ - Parameters: `query` (required), `pageno` (optional), `max_results` (optional, default 10), `time_range` (optional), `language` (optional), `safesearch` (optional). Use `max_results` to limit how many results are returned in one response (reduces token usage); use `pageno` for the next page.
70
+ 2. **web_url_read** - Fetch a URL and return Markdown content
71
+ - Parameters: `url` (required), `startChar`, `maxLength`, `section`, `paragraphRange`, `readHeadings`, `timeoutMs` (all optional).
72
+ - Uses `teplo_core` for HTML/XML to Markdown conversion (with workspace fallback to `textplorer/core-ruby` in development).
73
+ - Supports `http`, `https`, and `ipfs` by default. `ftp`, `sftp`, `smb`, and `gemini` require explicit opt-in through `SEARXNG_WEB_URL_SCHEMES`.
74
+
75
+ The MCP server also exposes resources:
76
+
77
+ - `config://server-config` - current server/environment capabilities
78
+ - `help://usage-guide` - concise usage and configuration guide
79
+
38
80
  ### Cursor IDE Configuration
39
81
 
40
82
  For Cursor IDE, create or update `.cursor/mcp.json` in your project:
@@ -120,37 +162,18 @@ Useful for testing the `searxng_web_search` tool before integrating with Cursor
120
162
  - **MCP Server Integration**: Ready-to-use MCP server with web search tool, compatible with Cursor IDE, Claude Desktop, and other MCP-enabled tools
121
163
  - **Configurable HTTP**: Optional custom CA, verify mode, and `configure_http` hook or `build_http` override for proxy/timeouts/certs
122
164
  - **Basic Auth**: Optional Basic authentication via options or ENV
165
+ - **Fail-fast MCP validation**: MCP startup validates `SEARXNG_URL` and auth variable pairs
166
+ - **User-facing errors**: Clear network/HTTP/configuration error messages
123
167
 
124
- ## Basic Usage
168
+ ## Protocol Support Notes
125
169
 
126
- ### Ruby API
127
-
128
- ```ruby
129
- require "searxng"
130
-
131
- client = Searxng::Client.new
132
- data = client.search("ruby programming", pageno: 1, language: "en")
133
-
134
- data[:results].each do |r|
135
- puts r[:title], r[:url], r[:content]
136
- end
137
- ```
138
-
139
- ### CLI
140
-
141
- ```bash
142
- searxng search "your query"
143
- searxng search "ruby" --page 2 --language en --time-range day --json
144
- ```
145
-
146
- Options: `--url`, `--page`, `--language`, `--time-range` (day|month|year), `--safesearch` (0|1|2), `--json`.
147
-
148
- ## MCP Tools
149
-
150
- The MCP server provides the following tools:
151
-
152
- 1. **searxng_web_search** - Web search via SearXNG
153
- - Parameters: `query` (required), `pageno` (optional), `max_results` (optional, default 10), `time_range` (optional), `language` (optional), `safesearch` (optional). Use `max_results` to limit how many results are returned in one response (reduces token usage); use `pageno` for the next page.
170
+ - `http` / `https`: fully supported.
171
+ - `ftp`: opt-in through `SEARXNG_WEB_URL_SCHEMES` (requires the `net-ftp` runtime gem).
172
+ - `sftp`: opt-in through `SEARXNG_WEB_URL_SCHEMES` (requires the `net-sftp` runtime gem).
173
+ - `smb`: opt-in through `SEARXNG_WEB_URL_SCHEMES` and uses `smbclient` available in `PATH`.
174
+ - `gemini`: opt-in through `SEARXNG_WEB_URL_SCHEMES` (Gemini fetch + Gemtext to Markdown conversion).
175
+ - `ipfs`: supported by `web_url_read` through gateway resolution (`ipfs://` -> `IPFS_GATEWAY`).
176
+ - `.onion` / `.i2p`: not supported by `web_url_read` (destination policy resolves hosts locally before any proxy).
154
177
 
155
178
  ## Examples
156
179
 
@@ -196,6 +219,27 @@ For more information, see [CONTRIBUTING.md](CONTRIBUTING.md).
196
219
 
197
220
  If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for details.
198
221
 
222
+ ## Links
223
+
224
+ - [GitHub](https://github.com/amkisko/searxng.rb)
225
+ - [GitLab](https://gitlab.com/amkisko/searxng.rb)
226
+ - [RubyGems](https://rubygems.org/gems/searxng)
227
+ - [Versions Atom](https://rubygems.org/gems/searxng/versions.atom) (feed id `10648244160064`)
228
+ - [libraries.io](https://libraries.io/rubygems/searxng)
229
+ - [Deps.dev](https://deps.dev/rubygems/searxng)
230
+ - [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_searxng.rb)
231
+ - [Snyk](https://snyk.io/test/github/amkisko/searxng.rb)
232
+ - [Codecov](https://app.codecov.io/github/amkisko/searxng.rb)
233
+ - [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/searxng.rb)
234
+
199
235
  ## License
200
236
 
201
237
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
238
+
239
+ ## Sponsors
240
+
241
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
242
+
243
+ <a href="https://www.kiskolabs.com">
244
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
245
+ </a>
data/bin/searxng CHANGED
@@ -5,13 +5,16 @@ cmd = ARGV.shift
5
5
  case cmd
6
6
  when "serve"
7
7
  unless ENV["DEBUG"]
8
- require "stringio"
9
- $stderr = StringIO.new
10
- $stderr.set_encoding("UTF-8")
8
+ $stderr.reopen(File::NULL, "w")
11
9
  end
12
10
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
13
11
  require "searxng/server"
14
- Searxng::Server.start
12
+ begin
13
+ Searxng::Server.start
14
+ rescue Searxng::ConfigurationError => e
15
+ warn e.message
16
+ exit 1
17
+ end
15
18
  when "search"
16
19
  $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
17
20
  require "searxng"
@@ -2,6 +2,8 @@ require "uri"
2
2
  require "net/http"
3
3
  require "openssl"
4
4
  require "json"
5
+ require "ipaddr"
6
+ require "timeout"
5
7
 
6
8
  module Searxng
7
9
  class Client
@@ -22,9 +24,13 @@ module Searxng
22
24
  user_agent: nil,
23
25
  configure_http: nil
24
26
  )
25
- @base_url = (base_url || ENV["SEARXNG_URL"] || DEFAULT_BASE_URL).to_s.chomp("/")
26
- @user = user || ENV["SEARXNG_USER"]
27
- @password = password || ENV["SEARXNG_PASSWORD"]
27
+ @base_url = if base_url.nil?
28
+ present_string(ENV["SEARXNG_URL"]) || DEFAULT_BASE_URL
29
+ else
30
+ base_url.to_s.strip
31
+ end.chomp("/")
32
+ @user = present_string(user) || present_string(ENV["SEARXNG_USER"]) || present_string(ENV["AUTH_USERNAME"])
33
+ @password = present_string(password) || present_string(ENV["SEARXNG_PASSWORD"]) || present_string(ENV["AUTH_PASSWORD"])
28
34
  @open_timeout = open_timeout
29
35
  @read_timeout = read_timeout
30
36
  @ca_file = ca_file || ENV["SEARXNG_CA_FILE"]
@@ -35,7 +41,7 @@ module Searxng
35
41
  end
36
42
 
37
43
  def search(query, pageno: 1, time_range: nil, language: "all", safesearch: nil)
38
- raise ConfigurationError, "SEARXNG_URL not set" if @base_url.nil? || @base_url.empty?
44
+ raise ConfigurationError, ErrorMessages.configuration_missing_url if @base_url.nil? || @base_url.empty?
39
45
 
40
46
  uri = build_search_uri(query, pageno: pageno, time_range: time_range, language: language, safesearch: safesearch)
41
47
  response = perform_request(uri)
@@ -47,13 +53,30 @@ module Searxng
47
53
  def build_search_uri(query, pageno:, time_range:, language:, safesearch:)
48
54
  uri = URI.join("#{@base_url}/", "search")
49
55
  params = {"q" => query, "format" => "json", "pageno" => pageno.to_s}
50
- params["time_range"] = time_range if time_range && VALID_TIME_RANGES.include?(time_range.to_s)
56
+ if time_range
57
+ range = time_range.to_s
58
+ unless VALID_TIME_RANGES.include?(range)
59
+ raise ConfigurationError, "Invalid time_range: #{time_range.inspect} (allowed: #{VALID_TIME_RANGES.join(", ")})"
60
+ end
61
+ params["time_range"] = range
62
+ end
51
63
  params["language"] = language if language && language != "all"
52
- params["safesearch"] = safesearch.to_s if safesearch && VALID_SAFESEARCH.include?(safesearch.to_i)
64
+ unless safesearch.nil?
65
+ value = Integer(safesearch, exception: false)
66
+ unless value && VALID_SAFESEARCH.include?(value)
67
+ raise ConfigurationError, "Invalid safesearch: #{safesearch.inspect} (allowed: #{VALID_SAFESEARCH.join(", ")})"
68
+ end
69
+ params["safesearch"] = value.to_s
70
+ end
53
71
  uri.query = URI.encode_www_form(params)
54
72
  uri
55
73
  end
56
74
 
75
+ def present_string(value)
76
+ text = value&.to_s&.strip
77
+ text unless text.nil? || text.empty?
78
+ end
79
+
57
80
  def perform_request(uri)
58
81
  http = build_http(uri)
59
82
  @configure_http&.call(http, uri)
@@ -65,13 +88,19 @@ module Searxng
65
88
 
66
89
  http.request(request)
67
90
  rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ECONNRESET, Errno::EPIPE, Timeout::Error, OpenSSL::SSL::SSLError, EOFError => e
68
- raise NetworkError, "Connection failed: #{e.message}"
91
+ raise NetworkError, ErrorMessages.network_error(e, uri: uri)
69
92
  end
70
93
 
71
94
  # Builds and returns a configured Net::HTTP instance. Override in a subclass to customize
72
95
  # timeouts, SSL, proxy, or other options. Called once per request.
73
96
  def build_http(uri)
74
- http = Net::HTTP.new(uri.host, uri.port)
97
+ proxy_uri = proxy_uri_for(uri)
98
+ http_klass = if proxy_uri
99
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
100
+ else
101
+ Net::HTTP
102
+ end
103
+ http = http_klass.new(uri.host, uri.port)
75
104
  http.use_ssl = (uri.scheme == "https")
76
105
  http.open_timeout = @open_timeout
77
106
  http.read_timeout = @read_timeout
@@ -103,7 +132,7 @@ module Searxng
103
132
  }
104
133
  else
105
134
  raise APIError.new(
106
- "SearXNG returned #{response.code} #{response.message}",
135
+ ErrorMessages.api_error(response.code.to_i, response.message),
107
136
  status_code: response.code.to_i,
108
137
  response_data: response.body,
109
138
  uri: uri.to_s
@@ -117,6 +146,51 @@ module Searxng
117
146
  )
118
147
  end
119
148
 
149
+ def proxy_uri_for(uri)
150
+ return nil if no_proxy_match?(uri.host)
151
+
152
+ raw = if uri.scheme == "https"
153
+ ENV["HTTPS_PROXY"] || ENV["https_proxy"] || ENV["HTTP_PROXY"] || ENV["http_proxy"] || ENV["ALL_PROXY"] || ENV["all_proxy"]
154
+ else
155
+ ENV["HTTP_PROXY"] || ENV["http_proxy"] || ENV["ALL_PROXY"] || ENV["all_proxy"]
156
+ end
157
+ return nil if raw.nil? || raw.strip.empty?
158
+
159
+ URI(raw.include?("://") ? raw : "http://#{raw}")
160
+ rescue URI::InvalidURIError, ArgumentError
161
+ raise ConfigurationError, "Proxy URL is invalid: #{raw.inspect}"
162
+ end
163
+
164
+ def no_proxy_match?(host)
165
+ return false if host.nil? || host.empty?
166
+
167
+ raw = ENV["NO_PROXY"] || ENV["no_proxy"]
168
+ return false if raw.nil? || raw.strip.empty?
169
+
170
+ host_ip = begin
171
+ IPAddr.new(host)
172
+ rescue
173
+ nil
174
+ end
175
+ raw.split(",").any? do |entry|
176
+ token = entry.to_s.strip
177
+ next false if token.empty?
178
+ return true if token == "*"
179
+
180
+ cidr = begin
181
+ IPAddr.new(token)
182
+ rescue
183
+ nil
184
+ end
185
+ if cidr && host_ip
186
+ next cidr.include?(host_ip)
187
+ end
188
+
189
+ normalized = token.sub(/\A\./, "")
190
+ host == normalized || host.end_with?(".#{normalized}")
191
+ end
192
+ end
193
+
120
194
  def normalize_results(results)
121
195
  Array(results).map do |r|
122
196
  {
@@ -15,4 +15,68 @@ module Searxng
15
15
  @uri = uri
16
16
  end
17
17
  end
18
+
19
+ module ErrorMessages
20
+ module_function
21
+
22
+ def configuration_missing_url
23
+ "SEARXNG_URL not set. Set it to your SearXNG instance (e.g., http://localhost:8080 or https://search.example.com)"
24
+ end
25
+
26
+ def configuration_invalid_url(url)
27
+ "SEARXNG_URL has invalid format: #{url.inspect}. Use format: http://localhost:8080 or https://search.example.com"
28
+ end
29
+
30
+ def configuration_invalid_protocol(protocol)
31
+ "SEARXNG_URL must use http or https protocol, got: #{protocol.inspect}"
32
+ end
33
+
34
+ def configuration_auth_pair
35
+ "Authentication variables must be set together: SEARXNG_USER with SEARXNG_PASSWORD (or AUTH_USERNAME with AUTH_PASSWORD)"
36
+ end
37
+
38
+ def no_results(query)
39
+ %(No results found for "#{query}". Try different search terms or check if SearXNG search engines are working.)
40
+ end
41
+
42
+ def network_error(exception, uri: nil, target: "SearXNG server")
43
+ case exception
44
+ when SocketError
45
+ host = uri&.host || "unknown host"
46
+ %(DNS Error: Cannot resolve hostname "#{host}".)
47
+ when Errno::ECONNREFUSED
48
+ "Connection Error: #{target} is not responding."
49
+ when Errno::ETIMEDOUT, Timeout::Error
50
+ "Timeout Error: #{target} is too slow to respond."
51
+ when OpenSSL::SSL::SSLError
52
+ "SSL Error: Certificate or TLS problem while connecting to #{target}."
53
+ else
54
+ message = exception&.message.to_s.strip
55
+ "Network Error: #{message.empty? ? "Connection failed" : message}"
56
+ end
57
+ end
58
+
59
+ def api_error(status_code, status_message)
60
+ case status_code.to_i
61
+ when 403
62
+ "SearXNG Error (403): Authentication required or IP blocked."
63
+ when 404
64
+ "SearXNG Error (404): Search endpoint not found."
65
+ when 429
66
+ "SearXNG Error (429): Rate limit exceeded."
67
+ when 500..599
68
+ "SearXNG Error (#{status_code}): Internal server error."
69
+ else
70
+ "SearXNG returned #{status_code} #{status_message}".strip
71
+ end
72
+ end
73
+
74
+ def fetch_http_error(status_code, status_message, location: nil)
75
+ code = status_code.to_i
76
+ base = "Website Error (#{code}): #{status_message}".strip
77
+ return "#{base} Location: #{location}" if location && !location.to_s.strip.empty?
78
+
79
+ base
80
+ end
81
+ end
18
82
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require "socket"
5
+ require "uri"
6
+
7
+ module Searxng
8
+ class NetworkPolicy
9
+ FORBIDDEN_RANGES = %w[
10
+ 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8
11
+ 169.254.0.0/16 172.16.0.0/12 192.0.0.0/24 192.0.2.0/24
12
+ 192.88.99.0/24 192.168.0.0/16 198.18.0.0/15 198.51.100.0/24
13
+ 203.0.113.0/24 224.0.0.0/4 240.0.0.0/4
14
+ ::/128 ::1/128 ::/96 64:ff9b::/96 64:ff9b:1::/48 100::/64
15
+ 2001::/23 2001:db8::/32 2002::/16 3fff::/20
16
+ fc00::/7 fe80::/10 fec0::/10 ff00::/8
17
+ ].map { |range| IPAddr.new(range) }.freeze
18
+
19
+ def self.validate!(url, allowed_schemes:, allow_userinfo: false)
20
+ uri = URI.parse(url.to_s)
21
+ reject! unless allowed_schemes.include?(uri.scheme&.downcase)
22
+ reject! if hostname_of(uri).to_s.empty? || (uri.userinfo && !allow_userinfo)
23
+
24
+ [uri, allowed_ip_address(uri)]
25
+ rescue URI::InvalidURIError
26
+ reject!
27
+ end
28
+
29
+ def self.allowed_ip_address(uri)
30
+ host = hostname_of(uri)
31
+ addresses = Addrinfo.getaddrinfo(host, uri.port, nil, :STREAM)
32
+ .map(&:ip_address)
33
+ .uniq
34
+ reject! if addresses.empty? || addresses.any? { |address| forbidden?(address) }
35
+
36
+ addresses.first
37
+ rescue SocketError, SystemCallError
38
+ reject!
39
+ end
40
+ private_class_method :allowed_ip_address
41
+
42
+ def self.hostname_of(uri)
43
+ uri.respond_to?(:hostname) ? uri.hostname : uri.host.to_s.delete_prefix("[").delete_suffix("]")
44
+ end
45
+ private_class_method :hostname_of
46
+
47
+ def self.forbidden?(address)
48
+ parsed = normalize_address(address)
49
+ return true if FORBIDDEN_RANGES.any? { |range| range.include?(parsed) }
50
+ return forbidden?(parsed.native.to_s) if ipv4_compatible?(parsed)
51
+
52
+ false
53
+ rescue IPAddr::InvalidAddressError
54
+ true
55
+ end
56
+ private_class_method :forbidden?
57
+
58
+ def self.normalize_address(address)
59
+ parsed = IPAddr.new(address.split("%", 2).first)
60
+ parsed.ipv4_mapped? ? parsed.native : parsed
61
+ end
62
+ private_class_method :normalize_address
63
+
64
+ def self.ipv4_compatible?(parsed)
65
+ parsed.ipv6? && parsed.respond_to?(:ipv4_compat?) && parsed.ipv4_compat?
66
+ end
67
+ private_class_method :ipv4_compatible?
68
+
69
+ def self.reject!
70
+ raise ConfigurationError, "URL destination is not allowed"
71
+ end
72
+ private_class_method :reject!
73
+ end
74
+ end