mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
The router reads an absolute-form target's path without resolving dot segments, so a target such as http://host/api/../x still reaches /api middleware. Take that path the same way when deciding whether the API response headers apply. The Street View fallback now treats any forwarding header as forwarded, even an empty one, before it spends the key.
115 lines
4.1 KiB
JavaScript
115 lines
4.1 KiB
JavaScript
// Camera catalogs are pinned to their agency's image directory. The pin is a
|
|
// parsed-URL check, not a string prefix, so a path that only starts with the
|
|
// directory before dot segments, an encoded separator, credentials or another
|
|
// port are resolved never counts as inside it, and a redirect on a pinned host
|
|
// cannot leave the directory either.
|
|
//
|
|
// Run with: npm test (node --test)
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { loadTflSourcesFromOpenData } from '../../server/providers/cctv/sources.js';
|
|
import { fetchWithinHost } from '../../server/providers/cctv/media.js';
|
|
import { pinnedCameraUrl } from '../../server/providers/cctv/normalize.js';
|
|
|
|
const BUCKET = 'https://s3-eu-west-1.amazonaws.com/jamcams.tfl.gov.uk/';
|
|
|
|
/** One available JamCam whose frame is at `imageUrl`. */
|
|
const jamCam = (n, imageUrl) => ({
|
|
id: `JamCams_00001.0000${n}`,
|
|
commonName: `JamCam ${n}`,
|
|
lat: 51.5072,
|
|
lon: -0.1276,
|
|
additionalProperties: [
|
|
{ key: 'available', value: 'true' },
|
|
{ key: 'imageUrl', value: imageUrl },
|
|
],
|
|
});
|
|
|
|
test('TfL JamCams keep only frames inside the official bucket, by parsed URL', async (t) => {
|
|
t.mock.method(console, 'log', () => {});
|
|
t.mock.method(console, 'warn', () => {});
|
|
const accepted = [
|
|
[`${BUCKET}00001.00001.jpg`, `${BUCKET}00001.00001.jpg`],
|
|
[`${BUCKET}00001.00002.jpg?t=1`, `${BUCKET}00001.00002.jpg?t=1`],
|
|
[
|
|
'https://s3-eu-west-1.amazonaws.com:443/jamcams.tfl.gov.uk/00001.00003.jpg',
|
|
`${BUCKET}00001.00003.jpg`,
|
|
],
|
|
];
|
|
const rejected = [
|
|
`${BUCKET}../other/x.jpg`,
|
|
`${BUCKET}%2e%2e/other/x.jpg`,
|
|
`${BUCKET}%2E%2E/other/x.jpg`,
|
|
`${BUCKET}.%2e/other/x.jpg`,
|
|
`${BUCKET}..%2fother/x.jpg`,
|
|
`${BUCKET}..%5Cother/x.jpg`,
|
|
`${BUCKET}..\\other\\x.jpg`,
|
|
'https://s3-eu-west-1.amazonaws.com/jamcams.tfl.gov.uk.other/x.jpg',
|
|
'https://s3-eu-west-1.amazonaws.com/jamcams.tfl.gov.uk-other/x.jpg',
|
|
'https://s3-eu-west-1.amazonaws.com/other/jamcams.tfl.gov.uk/x.jpg',
|
|
'https://user:[email protected]/jamcams.tfl.gov.uk/x.jpg',
|
|
'https://s3-eu-west-1.amazonaws.com:8443/jamcams.tfl.gov.uk/x.jpg',
|
|
'http://s3-eu-west-1.amazonaws.com/jamcams.tfl.gov.uk/x.jpg',
|
|
'https://s3-eu-west-1.amazonaws.com.other.example/jamcams.tfl.gov.uk/x.jpg',
|
|
'not a url',
|
|
];
|
|
t.mock.method(globalThis, 'fetch', async () =>
|
|
Response.json(
|
|
[...accepted.map(([url]) => url), ...rejected].map((url, i) =>
|
|
jamCam(i + 1, url),
|
|
),
|
|
),
|
|
);
|
|
const cameras = await loadTflSourcesFromOpenData();
|
|
assert.deepEqual(
|
|
cameras.map((camera) => camera.url),
|
|
accepted.map(([, normalized]) => normalized),
|
|
);
|
|
});
|
|
|
|
test('a redirect on a pinned camera host stays inside the pinned directory', async () => {
|
|
const redirectTo = (location) => async (url) =>
|
|
url === `${BUCKET}00001.00001.jpg`
|
|
? new Response(null, { status: 302, headers: { location } })
|
|
: new Response('frame', { headers: { 'content-type': 'image/jpeg' } });
|
|
for (const location of [
|
|
'/jamcams.tfl.gov.uk.other/x.jpg',
|
|
'/other/x.jpg',
|
|
'/jamcams.tfl.gov.uk/../other/x.jpg',
|
|
'/jamcams.tfl.gov.uk/%2e%2e/other/x.jpg',
|
|
'/jamcams.tfl.gov.uk/..%2fother/x.jpg',
|
|
])
|
|
assert.equal(
|
|
await fetchWithinHost(
|
|
`${BUCKET}00001.00001.jpg`,
|
|
{},
|
|
redirectTo(location),
|
|
),
|
|
null,
|
|
location,
|
|
);
|
|
const followed = await fetchWithinHost(
|
|
`${BUCKET}00001.00001.jpg`,
|
|
{},
|
|
redirectTo('/jamcams.tfl.gov.uk/00001.00002.jpg'),
|
|
);
|
|
assert.equal(await followed.text(), 'frame');
|
|
// A host with no pinned directory keeps the same-origin rule alone.
|
|
const unpinned = await fetchWithinHost(
|
|
'https://cams.example/a/frame.jpg',
|
|
{},
|
|
async (url) =>
|
|
url === 'https://cams.example/a/frame.jpg'
|
|
? new Response(null, { status: 302, headers: { location: '/b/x.jpg' } })
|
|
: new Response('frame'),
|
|
);
|
|
assert.equal(await unpinned.text(), 'frame');
|
|
});
|
|
|
|
test('a pin prefix without a trailing slash still names a directory', () => {
|
|
assert.equal(
|
|
pinnedCameraUrl('https://h.example/camsX/a.jpg', 'https://h.example/cams'),
|
|
null,
|
|
);
|
|
});
|