Record why the camera player cannot reach a gateway directly
Measured against go2rtc 1.9.14: it sends no Access-Control-Allow-Origin
on any response, auth or not; the SDP POST needs a preflight anyway
because application/sdp is not a CORS-safelisted Content-Type; and
go2rtc checks basic auth before CORS, so the preflight - which browsers
always send without credentials - gets a 401.
So this component has never worked from a browser on another origin, and
no credential handed to the frontend could fix it. The gateway's API now
requires a password, which closes the hole where anything on the
workshop LAN could watch, and changes the symptom here from a CORS error
to a 401. Both are the same missing piece: the relay.
Also stops the two most misleading errors in this path. A blocked
cross-origin request rejects as an opaque TypeError ("Failed to fetch")
that reads as the gateway being down, and a 401 reads as a wrong URL or
a missing camera. Both now say what they are.
This commit is contained in:
@@ -16,9 +16,28 @@ const error = ref<string | null>(null);
|
|||||||
*
|
*
|
||||||
* Note what is deliberately absent: no `iceServers`. Both ends are on the
|
* Note what is deliberately absent: no `iceServers`. Both ends are on the
|
||||||
* same LAN - the gateway is a box in the workshop - so host candidates are
|
* same LAN - the gateway is a box in the workshop - so host candidates are
|
||||||
* enough and a STUN round-trip would only add latency. The same fact is why
|
* enough and a STUN round-trip would only add latency.
|
||||||
* this component cannot work from outside that network at all; there is no
|
*
|
||||||
* relay, and the backend's /printers/:id/stream says so too.
|
* ## This cannot work from a browser on another origin, and that is measured
|
||||||
|
*
|
||||||
|
* Not a caveat about reachability - a flat blocker, against go2rtc 1.9.14:
|
||||||
|
*
|
||||||
|
* - go2rtc sends **no** `Access-Control-Allow-Origin` on any response, with
|
||||||
|
* or without auth configured. A cross-origin response is unreadable.
|
||||||
|
* - The `POST` below needs a preflight regardless, because `application/sdp`
|
||||||
|
* is not a CORS-safelisted `Content-Type`.
|
||||||
|
* - go2rtc checks basic auth *before* CORS, so the preflight - which a
|
||||||
|
* browser always sends without credentials - gets a 401. Sending the
|
||||||
|
* gateway's password from here could not fix it even if the password were
|
||||||
|
* worth handing to every viewer.
|
||||||
|
*
|
||||||
|
* So this component works when it is same-origin with go2rtc, or through a
|
||||||
|
* relay that proxies server-to-server. The gateway's API now requires a
|
||||||
|
* password (continuum-proxy's camera.rs), which closes the hole where
|
||||||
|
* anything on the workshop LAN could watch - and which also means a browser
|
||||||
|
* pointed straight at a gateway now gets a 401 rather than a CORS error.
|
||||||
|
* Both are the same missing piece. The relay design is in PLAN.md; note it
|
||||||
|
* cannot carry WebRTC, so whatever lands there will replace this transport.
|
||||||
*/
|
*/
|
||||||
async function connect() {
|
async function connect() {
|
||||||
status.value = "connecting";
|
status.value = "connecting";
|
||||||
@@ -53,6 +72,15 @@ async function connect() {
|
|||||||
// whoever is debugging it looking at the wrong layer, when the real
|
// whoever is debugging it looking at the wrong layer, when the real
|
||||||
// answer is that this printer is not in go2rtc's config.
|
// answer is that this printer is not in go2rtc's config.
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
// 401 has one meaning and it is worth naming: go2rtc's API is
|
||||||
|
// password-protected, and a browser cannot send that password across
|
||||||
|
// origins (see above). It is not a wrong URL or a missing camera.
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error(
|
||||||
|
"the gateway requires a password for camera access, which a browser cannot " +
|
||||||
|
"supply across origins - this needs the relay",
|
||||||
|
);
|
||||||
|
}
|
||||||
throw new Error(`the gateway refused the stream request (HTTP ${response.status})`);
|
throw new Error(`the gateway refused the stream request (HTTP ${response.status})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +104,18 @@ watch(
|
|||||||
() => {
|
() => {
|
||||||
connect().catch((err: unknown) => {
|
connect().catch((err: unknown) => {
|
||||||
status.value = "failed";
|
status.value = "failed";
|
||||||
error.value = err instanceof Error ? err.message : String(err);
|
// A blocked cross-origin request rejects as an opaque TypeError
|
||||||
|
// ("Failed to fetch") with nothing about CORS in it, which is the
|
||||||
|
// single most misleading error in this path - it reads as the gateway
|
||||||
|
// being down. Say the likely cause instead of repeating the browser.
|
||||||
|
error.value =
|
||||||
|
err instanceof TypeError
|
||||||
|
? "could not reach the gateway's camera API from this origin - either it is " +
|
||||||
|
"unreachable from here, or the browser blocked the request (see this " +
|
||||||
|
"component's notes)"
|
||||||
|
: err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: String(err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user