mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-21 12:23:32 +02:00
fix(seerr): re-encode ingress query strings for the OpenAPI validator (#2917)
* fix(seerr): re-encode ingress query strings for the OpenAPI validator
Searches through ingress fail with a 400 from the Seerr API, which the UI
reports as "500 Internal Server Error". The same searches succeed on the
directly published port 5055.
Supervisor proxies ingress traffic with `params=request.query`
(supervisor/api/ingress.py) - an already-decoded MultiDict - so aiohttp/yarl
re-encodes the query string on the way to the add-on. yarl's safe set is far
wider than the one express-openapi-validator accepts: it emits a space as "+"
and forwards ":", "/", "@", "!", "$", "'", "(", ")", "*" and "," bare, while
the validator tests the raw, still-encoded value against
RESERVED_CHARS = /[\:\/\?#\[\]@!\$&\'()\*\+,;=]/
and rejects the request. That breaks most real titles - "Monsters, Inc.",
"Ocean's Eleven", "Mission: Impossible", "Mamma Mia!".
An njs handler now re-encodes exactly those characters before proxying. This
is lossless: yarl only emits them bare when they were literal characters of
the value, since anything ambiguous arrives already encoded ("+" as %2B, "&"
as %26, "=" as %3D). "&" and "=" are left alone as the query string's own
separators, and the path is forwarded byte-for-byte.
Verified against a real express-openapi-validator over all 19 characters in
RESERVED_CHARS, and diffed byte-for-byte against the previous config across
representative traffic: only query-string encoding changes.
Fixes #2906
Fixes #2646
* fix(seerr): ship the njs load_module snippet and encode "?" and ";"
Addresses two review findings.
1. The njs module never loaded. .templates/ha_automatic_packages.sh moves the
rootfs /etc/nginx aside to /etc/nginx2 before installing nginx, then does
`rm -r /etc/nginx` and restores the saved tree. That deletes the
load_module snippet nginx-mod-http-js installs, so nginx aborted with
`unknown directive "js_import"` and the service's finish hook would have
shut the add-on down - all ingress dead, not just search. The image build
still passed CI because it never starts nginx. The snippet now ships in the
rootfs so it survives the swap, under the package's own filename so the two
can never both be present and double-load the module.
2. "?" arrives bare from yarl and was not encoded. It slipped through testing
because the validator strips one occurrence with `qs.replace('?', '')`
before checking, so a single "?" passes by accident and only a second one
("Who? What?") returned 400.
NEEDS_ENCODING is now derived from the validator's RESERVED_CHARS minus the
"&" and "=" separators, rather than from the characters yarl happens to emit
bare today, so it stays correct if either side changes its safe set. The added
characters are a no-op for current traffic: yarl already percent-encodes
"# [ ] ;", so Seerr receives them encoded regardless.
Verified by replaying the build-time /etc/nginx2 swap and starting nginx, which
fails without the snippet and serves correctly with it; by sending every
RESERVED_CHAR bare; and by diffing forwarded bytes against master, unchanged at
2/20 with all query-parser edge cases identical.
* style(seerr): satisfy Codacy - double quotes in njs, changelog blank lines
Clears the 11 new Info-level Codacy findings: 9x ESLint 'quotes' in
njs/ingress.js and 2x markdownlint MD022/MD032 on the changelog entry.
No behaviour change; re-verified through the build-time /etc/nginx2 swap.
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
## 3.3.0.1 (2026-07-28)
|
||||
|
||||
- Fixed searches failing through ingress with `500 Internal Server Error` (#2906, #2646). Home Assistant's ingress proxy re-encodes the query string and passes a space as `+`, along with a bare `:` `/` `?` `@` `!` `$` `'` `(` `)` `*` `,` - all of which Seerr's OpenAPI validator rejects as reserved characters. Nginx now re-encodes them before proxying, so titles such as `Monsters, Inc.`, `Ocean's Eleven`, `Mission: Impossible` and `Who? What?` search correctly. Only ingress was affected; the directly published port 5055 always worked.
|
||||
|
||||
## 3.3.0 (2026-06-05)
|
||||
- Update to latest version from seerr-team/seerr (changelog : https://github.com/seerr-team/seerr/releases)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ COPY ha_automodules.sh /ha_automodules.sh
|
||||
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
|
||||
|
||||
# Manual apps
|
||||
ENV PACKAGES="nginx"
|
||||
ENV PACKAGES="nginx nginx-mod-http-js"
|
||||
|
||||
# Automatic apps & bashio
|
||||
COPY ha_autoapps.sh /ha_autoapps.sh
|
||||
|
||||
@@ -96,4 +96,4 @@ schema:
|
||||
slug: seerr
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/seerr
|
||||
version: "3.3.0"
|
||||
version: "3.3.0.1"
|
||||
|
||||
14
seerr/rootfs/etc/nginx/modules/10_http_js.conf
Normal file
14
seerr/rootfs/etc/nginx/modules/10_http_js.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
# Load the njs module used by njs/ingress.js.
|
||||
#
|
||||
# The nginx-mod-http-js package ships this same file, but it cannot be relied
|
||||
# on: .templates/ha_automatic_packages.sh moves the rootfs /etc/nginx aside to
|
||||
# /etc/nginx2 before installing nginx, then does `rm -r /etc/nginx` and restores
|
||||
# the saved tree afterwards. That deletes anything the package placed under
|
||||
# /etc/nginx, including its own load_module snippet, and nginx would then fail
|
||||
# to start with "unknown directive js_import".
|
||||
#
|
||||
# Shipping it in the rootfs makes it survive that swap. The filename matches the
|
||||
# package's on purpose: if the package's copy is ever kept instead, it overwrites
|
||||
# this one rather than adding a second load_module for the same module, which
|
||||
# nginx rejects as a duplicate.
|
||||
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
|
||||
@@ -49,6 +49,11 @@ http {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# Repair the query string mangled by Supervisor's ingress proxy before it
|
||||
# reaches Seerr's OpenAPI validator. See njs/ingress.js for the full story.
|
||||
js_import ingress from /etc/nginx/njs/ingress.js;
|
||||
js_set $ingress_uri ingress.uri;
|
||||
|
||||
include /etc/nginx/includes/resolver.conf;
|
||||
include /etc/nginx/includes/upstream.conf;
|
||||
|
||||
|
||||
83
seerr/rootfs/etc/nginx/njs/ingress.js
Normal file
83
seerr/rootfs/etc/nginx/njs/ingress.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Repair the query string of a Home Assistant ingress request.
|
||||
*
|
||||
* Supervisor proxies ingress traffic with `params=request.query`
|
||||
* (supervisor/api/ingress.py), so aiohttp/yarl re-encodes an already-decoded
|
||||
* query string on the way to this add-on. yarl's "safe" set is much wider than
|
||||
* the one Seerr's express-openapi-validator will accept: yarl emits a space as
|
||||
* "+" and passes ":", "/", "?", "@", "!", "$", "'", "(", ")", "*" and ","
|
||||
* through bare, while the validator checks the raw, still-encoded value against
|
||||
*
|
||||
* RESERVED_CHARS = /[\:\/\?#\[\]@!\$&\'()\*\+,;=]/
|
||||
*
|
||||
* and answers 400 "Parameter '<name>' must be url encoded". Every search for a
|
||||
* title containing a space or punctuation therefore fails - "Monsters, Inc.",
|
||||
* "Ocean's Eleven", "Mission: Impossible" - which Seerr's UI reports as a
|
||||
* 500. The same requests succeed on the directly published port 5055, which
|
||||
* does not pass through Supervisor.
|
||||
*
|
||||
* "?" deserves a note: the validator strips one with `qs.replace('?', '')`
|
||||
* before testing, so a single bare "?" slips through by accident and only a
|
||||
* second one ("Who? What?") produces the 400. It is encoded here regardless.
|
||||
*
|
||||
* Re-encoding those characters here is lossless, because yarl only ever emits
|
||||
* them bare when they were literal characters of the value: anything the user
|
||||
* actually typed that is ambiguous comes through already percent-encoded
|
||||
* (a typed "+" arrives as "%2B", "&" as "%26", "=" as "%3D").
|
||||
*
|
||||
* "&" and "=" are deliberately NOT re-encoded: they are the query string's own
|
||||
* separators, so a bare one is always structural.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Every character of the validator's RESERVED_CHARS except "&" and "=", which
|
||||
* are the query string's own separators and are handled above. Deriving the
|
||||
* set from what the validator rejects - rather than from what yarl currently
|
||||
* emits bare - keeps this correct if either side changes its safe set.
|
||||
*/
|
||||
var NEEDS_ENCODING = /[:\/?#\[\]@!$'()*,;]/g;
|
||||
|
||||
function encodePart(part) {
|
||||
return part
|
||||
/* yarl encodes a space as "+"; a literal "+" arrives as "%2B". */
|
||||
.replace(/\+/g, "%20")
|
||||
.replace(NEEDS_ENCODING, function (c) {
|
||||
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the request URI with the path untouched byte-for-byte and only the
|
||||
* query string repaired. Used as the proxy_pass target.
|
||||
*/
|
||||
function uri(r) {
|
||||
var raw = r.variables.request_uri;
|
||||
var split = raw.indexOf("?");
|
||||
|
||||
if (split < 0) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
var path = raw.substring(0, split);
|
||||
var args = raw.substring(split + 1);
|
||||
|
||||
/* A bare trailing "?" is forwarded as-is, so the URI stays byte-for-byte. */
|
||||
if (args === "") {
|
||||
return raw;
|
||||
}
|
||||
|
||||
var repaired = args
|
||||
.split("&")
|
||||
.map(function (pair) {
|
||||
var eq = pair.indexOf("=");
|
||||
if (eq < 0) {
|
||||
return encodePart(pair);
|
||||
}
|
||||
return encodePart(pair.substring(0, eq)) + "=" + encodePart(pair.substring(eq + 1));
|
||||
})
|
||||
.join("&");
|
||||
|
||||
return path + "?" + repaired;
|
||||
}
|
||||
|
||||
export default { uri };
|
||||
@@ -10,9 +10,11 @@ server {
|
||||
location ^~ / {
|
||||
set $app '%%ingress_entry%%';
|
||||
|
||||
# Forward the raw request URI exactly as received by this nginx.
|
||||
# This is the safest way to preserve query-string encoding.
|
||||
proxy_pass http://127.0.0.1:5055$request_uri;
|
||||
# Forward the request URI with the path byte-for-byte as received, and
|
||||
# the query string re-encoded so the characters Supervisor's ingress
|
||||
# proxy passes through bare (a space as "+", plus ":/@!$'()*,") do not
|
||||
# trip Seerr's OpenAPI validator. See njs/ingress.js for the details.
|
||||
proxy_pass http://127.0.0.1:5055$ingress_uri;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Referer $http_referer;
|
||||
|
||||
Reference in New Issue
Block a user