Files
vliaudatandClaude Opus 5 eae3f89aca fix: let the panel reach the API over plain HTTP, deliberately
The e-ink firmware carries a certificate-authority bundle fixed when it
was built, so it cannot validate a chain rooted in an authority created
afterwards. Let's Encrypt's ISRG Root YR was issued in May 2026 and is
not even in an up-to-date Ubuntu CA bundle yet; the kit's firmware
predates it. The handshake fails before a request is ever sent, which is
why neither Traefik nor the application saw anything at all while the
device reported "API connection cannot be established".

Ruled out first, with evidence: TLS 1.2 and the ECDHE-RSA-AES-GCM suites
an ESP32 needs are both offered, and the intermediate is not
cross-signed by an older root, so no alternate path exists in what is
served.

A Traefik router now serves four device paths over :80, ahead of the
entrypoint-wide redirect. The administration stays on TLS. The device
token travels in clear; it is used for nothing else and is revocable
from the settings page, and the image URL is an unguessable content hash.

DEVICE_ALLOW_HTTP existed but was never read — a setting that does
nothing misrepresents what it protects. The device routes now refuse an
unencrypted request unless it is set, so opening this door is a written
decision rather than the silent consequence of a proxy change.

DEPLOY.md records the whole diagnosis, including the commands that
distinguish a TLS failure from an application one, and what to do the
day the firmware learns the new roots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-21 22:17:16 +02:00

51 lines
1.8 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest';
import { checkTransport } from './transport';
const original = process.env.DEVICE_ALLOW_HTTP;
afterEach(() => {
process.env.DEVICE_ALLOW_HTTP = original;
});
function request(headers: Record<string, string>, url = 'https://horaires.test/api/display') {
return new Request(url, { headers });
}
describe('checkTransport', () => {
it('accepts a request the proxy says came over TLS', () => {
process.env.DEVICE_ALLOW_HTTP = 'false';
expect(checkTransport(request({ 'x-forwarded-proto': 'https' })).ok).toBe(true);
});
it('refuses a plain request when plain is not allowed', () => {
process.env.DEVICE_ALLOW_HTTP = 'false';
const result = checkTransport(request({ 'x-forwarded-proto': 'http' }));
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.response.status).toBe(403);
}
});
it('accepts a plain request once it has been allowed deliberately', () => {
// The panel's certificate bundle is fixed at build time; when it cannot
// trust the chain, this is the recorded decision to let it through.
process.env.DEVICE_ALLOW_HTTP = 'true';
expect(checkTransport(request({ 'x-forwarded-proto': 'http' })).ok).toBe(true);
});
it('falls back to the request URL when no proxy header is present', () => {
process.env.DEVICE_ALLOW_HTTP = 'false';
expect(checkTransport(request({})).ok).toBe(true);
expect(checkTransport(request({}, 'http://horaires.test/api/display')).ok).toBe(false);
});
it('treats any value other than "true" as a refusal', () => {
// A half-set variable must not quietly open the door.
for (const value of ['', 'yes', '1', 'TRUE', 'oui']) {
process.env.DEVICE_ALLOW_HTTP = value;
expect(checkTransport(request({ 'x-forwarded-proto': 'http' })).ok).toBe(false);
}
});
});