Pin camera sources by parsed URL

Camera catalog image URLs were pinned by string prefix, so a URL that only
began with the agency's directory before dot segments or an encoded separator
were resolved still counted as inside it. The pin now parses the URL and
requires the same scheme, the exact host, no credentials, the default port and
a normalized path inside the pinned directory. Redirects on a pinned host must
stay inside the same directory.

Document the response format checks and API response headers alongside the
existing server hardening notes.
This commit is contained in:
Sameh Khamis committed 2026-10-21 15:07:40 -07:00
1 parent 0ad75095a7
commit 3e80afe713
8 files changed
+241 -31

No files matched your search

+40 -28
View File
@@ -149,6 +149,7 @@ import {
cameraDisplayCode,
rowArrayToObject,
prioritizeSources,
pinnedCameraUrl,
} from './normalize.js';
import { directionToHeading } from '../../../src/data/directionText.js';
import {
@@ -310,9 +311,12 @@ export async function loadCaltransSourcesFromOpenData() {
const lon = toFiniteNumber(loc.longitude);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
const imageUrl = String(cctv.imageData?.static?.currentImageURL || '');
// Official-host pin (see JSDoc). Also drops records with no still image.
if (!imageUrl.startsWith('https://cwwp2.dot.ca.gov/')) continue;
const imageUrl = pinnedCameraUrl(
cctv.imageData?.static?.currentImageURL,
'https://cwwp2.dot.ca.gov/',
);
if (!imageUrl) continue;
const locationName = String(loc.locationName || '').trim();
// Leading token of locationName is the stable camera code ("TV102 -- I-580 : …").
@@ -417,8 +421,8 @@ export async function loadTflSourcesFromOpenData() {
const lat = toFiniteNumber(place?.lat);
const lon = toFiniteNumber(place?.lon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
const imageUrl = String(props.imageUrl || '');
if (!imageUrl.startsWith(TFL_IMAGE_ORIGIN)) continue; // official-bucket pin
const imageUrl = pinnedCameraUrl(props.imageUrl, TFL_IMAGE_ORIGIN);
if (!imageUrl) continue; // official-bucket pin
// "JamCams_00002.00865" → "tfl-00002.00865" (provider-stable id).
const rawId = String(place?.id || '').replace(/^JamCams_/, '');
@@ -1127,13 +1131,15 @@ export function loadTallinnSourcesFromCatalog({
// Rough Estonia/Tallinn metro sanity (allows nearby suburbs already in the pack).
if (lat < 59.2 || lat > 59.7 || lon < 24.3 || lon > 25.4) continue;
const imageUrl =
const imageUrl = pinnedCameraUrl(
typeof item.url === 'string'
? item.url.trim()
? item.url
: typeof item.snapshotUrl === 'string'
? item.snapshotUrl.trim()
: '';
if (!imageUrl.startsWith(TALLINN_IMAGE_ORIGIN)) continue;
? item.snapshotUrl
: '',
TALLINN_IMAGE_ORIGIN,
);
if (!imageUrl) continue;
const extractedHeading = toFiniteNumber(item.headingDeg, NaN);
const hasHeading = Number.isFinite(extractedHeading);
@@ -1235,8 +1241,8 @@ export function parseTarkteeDatexImages(xml) {
body,
);
if (!refMatch || !urlMatch) continue;
const url = urlMatch[1].trim();
if (!url.startsWith(TARKTEE_IMAGE_ORIGIN)) continue;
const url = pinnedCameraUrl(urlMatch[1], TARKTEE_IMAGE_ORIGIN);
if (!url) continue;
out.set(refMatch[1], url);
}
return out;
@@ -1422,14 +1428,16 @@ export function loadWarendorfSourcesFromCatalog({
for (const item of rows) {
if (!item || typeof item !== 'object') continue;
const id = typeof item.id === 'string' ? item.id.trim() : '';
const url =
const raw =
typeof item.url === 'string'
? item.url.trim()
? item.url
: typeof item.snapshotUrl === 'string'
? item.snapshotUrl.trim()
? item.snapshotUrl
: '';
if (!id || !WARENDORF_IMAGE_ORIGINS.some((o) => url.startsWith(o)))
continue;
const url = WARENDORF_IMAGE_ORIGINS.map((o) =>
pinnedCameraUrl(raw, o),
).find(Boolean);
if (!id || !url) continue;
const lat = typeof item.lat === 'number' ? item.lat : NaN;
const lon = typeof item.lon === 'number' ? item.lon : NaN;
if (!isPlausibleLatLon(lat, lon)) continue;
@@ -1483,8 +1491,8 @@ export function nswCameraToSource(feature) {
const lat = typeof coords?.[1] === 'number' ? coords[1] : NaN;
if (!isLikelyNswCoordinate(lat, lon)) return null;
const props = feature?.properties || {};
const url = String(props.href || '').trim();
if (!url.startsWith(NSW_IMAGE_ORIGIN)) return null;
const url = pinnedCameraUrl(props.href, NSW_IMAGE_ORIGIN);
if (!url) return null;
// "N-E" -> "NE" for the compass lookup.
const direction = String(props.direction || '')
.trim()
@@ -1578,8 +1586,7 @@ export function normalizeCalgaryImageUrl(raw) {
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
parsed.protocol = 'https:';
const upgraded = parsed.toString();
return upgraded.startsWith(CALGARY_IMAGE_ORIGIN) ? upgraded : null;
return pinnedCameraUrl(parsed.toString(), CALGARY_IMAGE_ORIGIN);
}
/**
@@ -2085,8 +2092,7 @@ export function normalizeHkImageUrl(raw) {
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
parsed.protocol = 'https:';
const upgraded = parsed.toString();
return upgraded.startsWith(HK_IMAGE_ORIGIN) ? upgraded : null;
return pinnedCameraUrl(parsed.toString(), HK_IMAGE_ORIGIN);
}
/**
@@ -2423,10 +2429,16 @@ export async function loadKingCountySourcesFromOpenData() {
// county image origin, so they are skipped.
if (owner === 'WSDOT') continue;
// The handler is served over HTTPS; the feed still writes http://.
const imageUrl = String(attrs.ImageURL || '')
.trim()
.replace(/^http:\/\/info\.kingcounty\.gov\//, KING_COUNTY_IMAGE_ORIGIN);
if (!imageUrl.startsWith(KING_COUNTY_IMAGE_ORIGIN)) continue;
const imageUrl = pinnedCameraUrl(
String(attrs.ImageURL || '')
.trim()
.replace(
/^http:\/\/info\.kingcounty\.gov\//,
KING_COUNTY_IMAGE_ORIGIN,
),
KING_COUNTY_IMAGE_ORIGIN,
);
if (!imageUrl) continue;
const [lon, lat] = Array.isArray(row?.geometry?.coordinates)
? row.geometry.coordinates
: [];
@@ -2586,8 +2598,8 @@ export async function loadWsdotSourcesFromOpenData() {
const attrs = row?.attributes;
const rawId = attrs?.CameraID;
if (!Number.isSafeInteger(rawId) || rawId <= 0) continue;
const imageUrl = String(attrs.ImageURL || '').trim();
if (!imageUrl.startsWith(WSDOT_IMAGE_ORIGIN)) continue;
const imageUrl = pinnedCameraUrl(attrs.ImageURL, WSDOT_IMAGE_ORIGIN);
if (!imageUrl) continue;
const x = row?.geometry?.x;
const y = row?.geometry?.y;
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;