Proxy kiwix-serve through this app instead of linking to it directly
map.priesol.net sits behind a reverse proxy that only forwards to maptool itself, so the previous approach (publish kiwix-serve's port, link the browser straight at it) had no address a real browser could ever reach — not just a Docker-internal hostname as first assumed, but no host/port combination at all works from outside. GET /kiwix/* now proxies to kiwix-serve (Kiwix.proxy in lib/maptool/kiwix.rb, a plain Net::HTTP passthrough), and the kiwix service runs with --urlRootLocation=/kiwix so every link/asset it emits already carries that same prefix and needs no rewriting (confirmed against libkiwix/internalServer.cpp, which strips this exact prefix off incoming requests). Drops the port-publishing approach from the previous commit entirely.
This commit is contained in:
1 parent
9997a8a8c0
commit
5ff9b113d9
7 files changed
+107
-51
No files matched your search
@@ -14,7 +14,7 @@ independence from connectivity.
|
|||||||
* [ ] Contour lines from OSM for hikes and mountain passes
|
* [ ] Contour lines from OSM for hikes and mountain passes
|
||||||
* [x] Overpass/OSM scripts to extract POIs (drinking water, dump stations, LPG, public toilets, laundromats, shops, springs) — `rake overpass:pois`, see docs/development.adoc; writes a `.geojson` to drop in `data/gpx/` or upload, not called by the running app itself
|
* [x] Overpass/OSM scripts to extract POIs (drinking water, dump stations, LPG, public toilets, laundromats, shops, springs) — `rake overpass:pois`, see docs/development.adoc; writes a `.geojson` to drop in `data/gpx/` or upload, not called by the running app itself
|
||||||
* [ ] Import Park4Night / iOverlander / Freecampsites data
|
* [ ] Import Park4Night / iOverlander / Freecampsites data
|
||||||
* [x] Kiwix/ZIM offline Wikipedia linked to POI GPS coordinates — a "Nearby on Wikipedia" popup link straight to kiwix-serve's own geo search (`GET /search?latitude=&longitude=&distance=`, confirmed from its actual source, `libkiwix/internalServer.cpp`), not proxied/embedded — it already has a full search UI. `KIWIX_URL` + `GET /api/kiwix` reachability check, same optional-integration pattern as offline.pmtiles/terrain.pmtiles; `docker-compose.yaml` now runs a `kiwix` service, ZIM not included (see docs/development.adoc for where to get one). Not live-tested against a real ZIM (none available here) — verified via a stand-in local HTTP server + real popup-link generation
|
* [x] Kiwix/ZIM offline Wikipedia linked to POI GPS coordinates — a "Nearby on Wikipedia" popup link to kiwix-serve's own geo search (`GET /kiwix/search?latitude=&longitude=&distance=`), proxied through this app (`GET /kiwix/*` in app.rb) rather than a direct link to kiwix-serve — production sits behind a reverse proxy that only forwards to this app, so no address exists for a browser to reach kiwix-serve at directly; kiwix-serve runs with `--urlRootLocation=/kiwix` so its own emitted links already match. `KIWIX_URL` + `GET /api/kiwix` reachability check, same optional-integration pattern as offline.pmtiles/terrain.pmtiles; `docker-compose.yaml` now runs a `kiwix` service, ZIM not included (see docs/development.adoc for where to get one)
|
||||||
* [x] Cache weather forecast when temporarily connected — Open-Meteo 3-day forecast in the Radar flyout, last successful fetch cached in localStorage as a fallback when offline
|
* [x] Cache weather forecast when temporarily connected — Open-Meteo 3-day forecast in the Radar flyout, last successful fetch cached in localStorage as a fallback when offline
|
||||||
** [x] Cache the radar tile *images* too — `public/sw.js`, network-falling-back-to-cache for RainViewer tile requests, registered from layout.erb
|
** [x] Cache the radar tile *images* too — `public/sw.js`, network-falling-back-to-cache for RainViewer tile requests, registered from layout.erb
|
||||||
|
|
||||||
|
|||||||
@@ -73,21 +73,13 @@ module Maptool
|
|||||||
# An external kiwix-serve instance (see docs/development.adoc), not
|
# An external kiwix-serve instance (see docs/development.adoc), not
|
||||||
# something this app runs itself — blank disables the "Nearby on
|
# something this app runs itself — blank disables the "Nearby on
|
||||||
# Wikipedia" popup link the same way the other optional-integration
|
# Wikipedia" popup link the same way the other optional-integration
|
||||||
# blanks above do. This is reached server-side only (the reachability
|
# blanks above do. Only ever reached server-side (the Docker-network
|
||||||
# check below, same-Docker-network service name in production, e.g.
|
# service name in production, e.g. "http://kiwix:8080") — production
|
||||||
# "http://kiwix:8080") — it is NOT what gets handed to the browser, see
|
# sits behind a reverse proxy (map.priesol.net) that only forwards to
|
||||||
# KIWIX_PUBLIC_PORT.
|
# this app, so a browser can't reach kiwix-serve directly by any
|
||||||
|
# hostname/port of its own. GET /kiwix/* below proxies to it instead.
|
||||||
KIWIX_URL = ENV.fetch("KIWIX_URL", "")
|
KIWIX_URL = ENV.fetch("KIWIX_URL", "")
|
||||||
|
|
||||||
# The port kiwix-serve is published on for browsers to reach directly
|
|
||||||
# (docker-compose.yaml's kiwix service ports: mapping) — KIWIX_URL above
|
|
||||||
# is a Docker-network-internal hostname a real browser can't resolve at
|
|
||||||
# all, so the "Nearby on Wikipedia" link is instead built from whatever
|
|
||||||
# host the visitor already used to reach maptool itself
|
|
||||||
# (request.host) plus this port. Blank disables the link, same as a
|
|
||||||
# blank KIWIX_URL.
|
|
||||||
KIWIX_PUBLIC_PORT = ENV.fetch("KIWIX_PUBLIC_PORT", "")
|
|
||||||
|
|
||||||
# Login delegates to the Gitea instance the user already has an account
|
# Login delegates to the Gitea instance the user already has an account
|
||||||
# on, rather than maintaining a local username/password — see
|
# on, rather than maintaining a local username/password — see
|
||||||
# docs/architecture.adoc. Only users listed in ALLOWED_GITEA_USERS may
|
# docs/architecture.adoc. Only users listed in ALLOWED_GITEA_USERS may
|
||||||
@@ -218,16 +210,29 @@ module Maptool
|
|||||||
send_file TERRAIN_PMTILES_PATH, type: "application/octet-stream"
|
send_file TERRAIN_PMTILES_PATH, type: "application/octet-stream"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Pinged once per page load (public/js/map.js) — url is handed back so
|
# Pinged once per page load (public/js/map.js) — url is a same-origin
|
||||||
# the frontend can build the geo-search link itself; this server never
|
# path (not kiwix-serve's own address, which the browser can't reach
|
||||||
# proxies to kiwix-serve, just says whether it's there. url is built
|
# directly behind the reverse proxy in front of this app), proxied by
|
||||||
# from request.host (whatever hostname/IP the visitor already used to
|
# GET /kiwix/* below.
|
||||||
# reach maptool itself) + KIWIX_PUBLIC_PORT, not KIWIX_URL — that's a
|
|
||||||
# Docker-network-internal address only this server can resolve.
|
|
||||||
get "/api/kiwix" do
|
get "/api/kiwix" do
|
||||||
reachable = Kiwix.reachable?(KIWIX_URL)
|
reachable = Kiwix.reachable?(KIWIX_URL)
|
||||||
url = KIWIX_PUBLIC_PORT.empty? ? "" : "http://#{request.host}:#{KIWIX_PUBLIC_PORT}"
|
json available: reachable, url: (reachable ? "/kiwix" : "")
|
||||||
json available: reachable && !url.empty?, url: url
|
end
|
||||||
|
|
||||||
|
# Proxies kiwix-serve's own search UI and content pages through this
|
||||||
|
# app instead of linking the browser to it directly (see KIWIX_URL) —
|
||||||
|
# kiwix-serve is started with --urlRootLocation=/kiwix
|
||||||
|
# (docker-compose.yaml), so every link/asset it emits already carries
|
||||||
|
# this same /kiwix prefix and Just Works through this passthrough. GET
|
||||||
|
# only: browsing a read-only ZIM archive never needs to write anything.
|
||||||
|
get "/kiwix/*" do
|
||||||
|
halt 404 if KIWIX_URL.empty?
|
||||||
|
response = Kiwix.proxy(KIWIX_URL, "/kiwix/#{params[:splat].first}", request.query_string)
|
||||||
|
halt 502 unless response
|
||||||
|
|
||||||
|
content_type response["content-type"] || "application/octet-stream"
|
||||||
|
status response.code.to_i
|
||||||
|
body response.body
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/gpx/tree" do
|
get "/api/gpx/tree" do
|
||||||
|
|||||||
+12
-13
@@ -37,14 +37,10 @@ services:
|
|||||||
# actually being present: GET /api/kiwix pings it and just reports
|
# actually being present: GET /api/kiwix pings it and just reports
|
||||||
# unavailable if the container isn't up/reachable, same story as
|
# unavailable if the container isn't up/reachable, same story as
|
||||||
# every other optional-local-file feature (offline.pmtiles, etc).
|
# every other optional-local-file feature (offline.pmtiles, etc).
|
||||||
# Server-side reachability check only — a browser on the LAN can't
|
# Reached server-side only — production sits behind a reverse proxy
|
||||||
# resolve this hostname, see KIWIX_PUBLIC_PORT below.
|
# that only forwards to maptool itself, so app.rb's GET /kiwix/*
|
||||||
|
# proxies to this instead of handing the browser this address.
|
||||||
KIWIX_URL: "http://kiwix:8080"
|
KIWIX_URL: "http://kiwix:8080"
|
||||||
# The host port kiwix's own ports: mapping below publishes it on —
|
|
||||||
# app.rb builds the browser-facing "Nearby on Wikipedia" link from
|
|
||||||
# whatever host the visitor used to reach maptool itself plus this
|
|
||||||
# port, since KIWIX_URL above is Docker-internal only.
|
|
||||||
KIWIX_PUBLIC_PORT: "8090"
|
|
||||||
volumes:
|
volumes:
|
||||||
# Uploaded GPX files and the offline PMTiles extract — kept outside
|
# Uploaded GPX files and the offline PMTiles extract — kept outside
|
||||||
# the image so a rebuild/redeploy doesn't wipe them (see
|
# the image so a rebuild/redeploy doesn't wipe them (see
|
||||||
@@ -93,11 +89,14 @@ services:
|
|||||||
# --port=8080 (its default) before appending this command array, so
|
# --port=8080 (its default) before appending this command array, so
|
||||||
# passing it again produced "Unexpected argument: --port=8080,
|
# passing it again produced "Unexpected argument: --port=8080,
|
||||||
# --port=8080, /data/wikipedia.zim" and kiwix-serve refused to start.
|
# --port=8080, /data/wikipedia.zim" and kiwix-serve refused to start.
|
||||||
command: ["/data/wikipedia.zim"]
|
#
|
||||||
# Published so browsers on the LAN can reach kiwix-serve's own search
|
# --urlRootLocation=/kiwix makes every link/asset kiwix-serve itself
|
||||||
# UI directly (see maptool's KIWIX_PUBLIC_PORT above) — 8090, not 8080,
|
# emits (search results, article pages, its CSS/JS) carry a /kiwix
|
||||||
# since 8080 is already taken by another container on this host.
|
# prefix, matching where app.rb's GET /kiwix/* proxies requests in
|
||||||
ports:
|
# from (confirmed against libkiwix's internalServer.cpp: it strips
|
||||||
- "8090:8080"
|
# this same prefix off incoming request paths, so both ends agree on
|
||||||
|
# it) — no ports: mapping needed, nothing outside the compose network
|
||||||
|
# ever talks to this container directly.
|
||||||
|
command: ["--urlRootLocation=/kiwix", "/data/wikipedia.zim"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/kiwix/wikipedia.zim:/data/wikipedia.zim
|
- ./data/kiwix/wikipedia.zim:/data/wikipedia.zim
|
||||||
+11
-10
@@ -80,12 +80,12 @@ Wikipedia https://library.kiwix.org[ZIM] loaded — set `KIWIX_URL` to
|
|||||||
wherever *the server* can reach it (`docker-compose.yaml` already runs
|
wherever *the server* can reach it (`docker-compose.yaml` already runs
|
||||||
a `kiwix` service pointed at `data/kiwix/wikipedia.zim` and wires
|
a `kiwix` service pointed at `data/kiwix/wikipedia.zim` and wires
|
||||||
`KIWIX_URL` to it via its compose service name; blank/unset disables
|
`KIWIX_URL` to it via its compose service name; blank/unset disables
|
||||||
the link, same as every other optional integration here). This is
|
the link, same as every other optional integration here). Used only
|
||||||
used only for the server-side reachability check — a browser can't
|
server-side: production sits behind a reverse proxy that forwards only
|
||||||
resolve a Docker-internal hostname like `kiwix`, so the link itself is
|
to this app, so there's no address a browser could reach kiwix-serve
|
||||||
built from `KIWIX_PUBLIC_PORT` (the host port kiwix-serve is
|
at directly — `GET /kiwix/*` (`app.rb`) proxies to it instead, and the
|
||||||
`ports:`-published on, `8090` in `docker-compose.yaml`) plus whatever
|
`kiwix` service runs with `--urlRootLocation=/kiwix` so its own
|
||||||
host the visitor already used to reach maptool itself. Not part of the
|
emitted links/assets already carry that same prefix. Not part of the
|
||||||
repo — a ZIM is a large, self-contained archive, obtain one locally:
|
repo — a ZIM is a large, self-contained archive, obtain one locally:
|
||||||
|
|
||||||
* https://library.kiwix.org/#lang=eng&category=wikipedia[Kiwix's own
|
* https://library.kiwix.org/#lang=eng&category=wikipedia[Kiwix's own
|
||||||
@@ -105,10 +105,11 @@ repo — a ZIM is a large, self-contained archive, obtain one locally:
|
|||||||
|
|
||||||
Once it's there, `docker compose up -d kiwix` (or a full restart)
|
Once it's there, `docker compose up -d kiwix` (or a full restart)
|
||||||
picks it up — `GET /api/kiwix` reports `available: true` and the
|
picks it up — `GET /api/kiwix` reports `available: true` and the
|
||||||
"Nearby on Wikipedia" link appears. kiwix-serve's own geo search
|
"Nearby on Wikipedia" link appears, pointed at kiwix-serve's own geo
|
||||||
(`GET /search?pattern=&latitude=&longitude=&distance=`,
|
search (`GET /kiwix/search?pattern=&latitude=&longitude=&distance=`)
|
||||||
`lib/maptool/kiwix.rb`'s docstring) is linked to directly, not
|
through this app's own proxy rather than at kiwix-serve directly — it
|
||||||
proxied/embedded — it already has a full search UI of its own.
|
already has a full search UI of its own, this just makes it reachable
|
||||||
|
through the same origin/reverse proxy as the rest of the app.
|
||||||
|
|
||||||
== GNSS position (gpsd)
|
== GNSS position (gpsd)
|
||||||
|
|
||||||
|
|||||||
+23
-6
@@ -1,12 +1,14 @@
|
|||||||
require "net/http"
|
require "net/http"
|
||||||
|
|
||||||
module Maptool
|
module Maptool
|
||||||
# A thin reachability check for an external kiwix-serve instance serving
|
# A reachability check plus a passthrough proxy for an external
|
||||||
# an offline Wikipedia ZIM (see docs/development.adoc for how to set one
|
# kiwix-serve instance serving an offline Wikipedia ZIM (see
|
||||||
# up) — the app itself never talks to it beyond this ping. Linking a POI
|
# docs/development.adoc for how to set one up). A POI popup's "Nearby on
|
||||||
# popup to a geo search there (public/js/gpx.js) is a plain browser <a
|
# Wikipedia" link (public/js/gpx.js) used to point straight at
|
||||||
# href> straight to kiwix-serve, not something this server proxies —
|
# kiwix-serve itself — fine on a LAN, broken once this app started
|
||||||
# kiwix-serve already has its own full search UI, no reason to rebuild it.
|
# running behind a reverse proxy (map.priesol.net) that only forwards to
|
||||||
|
# *this* app, leaving no address a browser could reach kiwix-serve at
|
||||||
|
# directly. GET /kiwix/* in app.rb calls .proxy below instead.
|
||||||
module Kiwix
|
module Kiwix
|
||||||
def self.reachable?(url, open_timeout: 1, read_timeout: 2)
|
def self.reachable?(url, open_timeout: 1, read_timeout: 2)
|
||||||
return false if url.to_s.strip.empty?
|
return false if url.to_s.strip.empty?
|
||||||
@@ -17,5 +19,20 @@ module Maptool
|
|||||||
rescue StandardError
|
rescue StandardError
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# path already carries the /kiwix prefix kiwix-serve itself expects
|
||||||
|
# (started with --urlRootLocation=/kiwix, see docker-compose.yaml) —
|
||||||
|
# this is a plain passthrough, not a rewrite, so its emitted links stay
|
||||||
|
# correct without this having to touch response bodies at all. Returns
|
||||||
|
# nil (halted as a 502 by the caller) rather than raising, same
|
||||||
|
# not-reachable-isn't-exceptional stance as .reachable? above.
|
||||||
|
def self.proxy(base_url, path, query_string, open_timeout: 2, read_timeout: 10)
|
||||||
|
uri = URI(base_url)
|
||||||
|
uri.path = path
|
||||||
|
uri.query = query_string unless query_string.to_s.empty?
|
||||||
|
Net::HTTP.start(uri.host, uri.port, open_timeout: open_timeout, read_timeout: read_timeout) { |http| http.request(Net::HTTP::Get.new(uri)) }
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -28,6 +28,10 @@ class AppTest < Minitest::Test
|
|||||||
assert_equal({ "available" => false, "url" => "" }, json_body(response))
|
assert_equal({ "available" => false, "url" => "" }, json_body(response))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_kiwix_proxy_404s_when_no_url_configured
|
||||||
|
assert_equal 404, request.get("/kiwix/search").status
|
||||||
|
end
|
||||||
|
|
||||||
def test_gnss_position_unavailable_with_no_fix
|
def test_gnss_position_unavailable_with_no_fix
|
||||||
response = request.get("/api/gnss/position", **logged_in_env)
|
response = request.get("/api/gnss/position", **logged_in_env)
|
||||||
assert_equal({ "available" => false }, json_body(response))
|
assert_equal({ "available" => false }, json_body(response))
|
||||||
|
|||||||
@@ -15,4 +15,34 @@ class KiwixTest < Minitest::Test
|
|||||||
def test_reachable_is_false_when_nothing_is_listening
|
def test_reachable_is_false_when_nothing_is_listening
|
||||||
refute Maptool::Kiwix.reachable?("http://127.0.0.1:1", open_timeout: 1)
|
refute Maptool::Kiwix.reachable?("http://127.0.0.1:1", open_timeout: 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# A bare TCPServer stub (not WEBrick — not a project dependency, and this
|
||||||
|
# only needs to echo back what request line it received) confirms .proxy
|
||||||
|
# actually builds the target URL from path + query_string correctly, not
|
||||||
|
# just that it can complete *a* request (already covered by nothing here
|
||||||
|
# exercising a wrong path/query, unlike .reachable? which only ever hits
|
||||||
|
# "/").
|
||||||
|
def test_proxy_forwards_path_and_query_and_returns_the_response
|
||||||
|
server = TCPServer.new("127.0.0.1", 0)
|
||||||
|
port = server.addr[1]
|
||||||
|
request_line = nil
|
||||||
|
thread = Thread.new do
|
||||||
|
client = server.accept
|
||||||
|
request_line = client.gets
|
||||||
|
client.print("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nhi")
|
||||||
|
client.close
|
||||||
|
end
|
||||||
|
|
||||||
|
response = Maptool::Kiwix.proxy("http://127.0.0.1:#{port}", "/kiwix/search", "pattern=&latitude=1")
|
||||||
|
thread.join
|
||||||
|
server.close
|
||||||
|
|
||||||
|
assert_equal "GET /kiwix/search?pattern=&latitude=1 HTTP/1.1\r\n", request_line
|
||||||
|
assert_equal "200", response.code
|
||||||
|
assert_equal "hi", response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_proxy_returns_nil_when_nothing_is_listening
|
||||||
|
assert_nil Maptool::Kiwix.proxy("http://127.0.0.1:1", "/kiwix/search", "", open_timeout: 1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Reference in new issue
Block a user