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 +4 -4
- data/CHANGELOG.md +36 -1
- data/README.md +84 -40
- data/bin/searxng +7 -4
- data/lib/searxng/client.rb +83 -9
- data/lib/searxng/errors.rb +64 -0
- data/lib/searxng/network_policy.rb +74 -0
- data/lib/searxng/server.rb +793 -2
- data/lib/searxng/version.rb +1 -1
- data/sig/searxng.rbs +11 -1
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0752ce85481fa3a6a5846b4326effa0251ff69e77a32912dc046f06e35542832
|
|
4
|
+
data.tar.gz: 1700a1ad55569d5cbb7137c6b3c6cde6583c8b6198508975e6b66030dfd2bc39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e85f85f3363f0468cef0d8dd6b5318f2d2eba8ef7268e6c607fbbe318a5140347e395400b747c33716f4ee73a2fa99e6af54398e7e20e42dbfb2e1a76f60593a
|
|
7
|
+
data.tar.gz: ff609005b3e6993106c5c29855801675e48b25221456d7f4561f43390a31af1002d98a6abc718d407cd3c87bcede1ff8bed6b587cf166c6fdd5dddfb699aa3a8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
#
|
|
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
|
-
[](https://badge.fury.io/rb/searxng) [](https://github.com/amkisko/searxng.rb/actions/workflows/test.yml) [](https://codecov.io/
|
|
3
|
+
[](https://badge.fury.io/rb/searxng) [](https://github.com/amkisko/searxng.rb/actions/workflows/test.yml) [](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.
|
|
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
|
|
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
|
-
##
|
|
168
|
+
## Protocol Support Notes
|
|
125
169
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
data/lib/searxng/client.rb
CHANGED
|
@@ -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 =
|
|
26
|
-
|
|
27
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
{
|
data/lib/searxng/errors.rb
CHANGED
|
@@ -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
|