1.0.36
This commit is contained in:
63 files changed
+1324
-281
No files matched your search
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Safe cookie header parsing for WebSocket upgrades.
|
||||
*
|
||||
* Extracted from ws-auth so it can be unit-tested without pulling in the
|
||||
* DB/SvelteKit graph (#1224).
|
||||
*/
|
||||
|
||||
export function safeDecode(v: string): string {
|
||||
// Browsers freely send cookies with stray `%` from third-party trackers and
|
||||
// legacy code. decodeURIComponent throws URIError on `%` not followed by two
|
||||
// hex digits, which would crash the entire WS upgrade — including the
|
||||
// session cookie we actually care about. Fall back to the raw value (#1224).
|
||||
try {
|
||||
return decodeURIComponent(v);
|
||||
} catch {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCookieHeader(header: string | undefined): Record<string, string> {
|
||||
if (!header) return {};
|
||||
const out: Record<string, string> = {};
|
||||
for (const part of header.split(';')) {
|
||||
const eq = part.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const k = part.slice(0, eq).trim();
|
||||
let v = part.slice(eq + 1).trim();
|
||||
if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
|
||||
if (k) out[k] = safeDecode(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Pure date/time formatting helpers — no SvelteKit / store imports.
|
||||
*
|
||||
* settings.ts wraps these with cached preferences and a reactive subscription;
|
||||
* keeping the implementation here lets the formatters be unit-tested without
|
||||
* dragging in $app/environment or svelte/store.
|
||||
*/
|
||||
|
||||
export type TimeFormat = '12h' | '24h';
|
||||
export type DateFormat = 'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY-MM-DD' | 'DD.MM.YYYY';
|
||||
|
||||
export interface DateTimeFormatters {
|
||||
date: Intl.DateTimeFormat;
|
||||
time: Intl.DateTimeFormat;
|
||||
}
|
||||
|
||||
// Intl.DateTimeFormat construction is expensive; the caller caches one pair per
|
||||
// timezone and rebuilds only when the timezone setting changes.
|
||||
export function buildFormatters(timeZone: string | undefined): DateTimeFormatters {
|
||||
const tz = timeZone || undefined;
|
||||
return {
|
||||
date: new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: tz,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}),
|
||||
time: new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: tz,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function pickPart(parts: Intl.DateTimeFormatPart[], type: Intl.DateTimeFormatPartTypes): string {
|
||||
return parts.find((p) => p.type === type)?.value ?? '';
|
||||
}
|
||||
|
||||
export function formatDatePartWith(d: Date, dateFormatter: Intl.DateTimeFormat, dateFormat: DateFormat): string {
|
||||
const parts = dateFormatter.formatToParts(d);
|
||||
const day = pickPart(parts, 'day');
|
||||
const month = pickPart(parts, 'month');
|
||||
const year = pickPart(parts, 'year');
|
||||
|
||||
switch (dateFormat) {
|
||||
case 'MM/DD/YYYY':
|
||||
return `${month}/${day}/${year}`;
|
||||
case 'DD/MM/YYYY':
|
||||
return `${day}/${month}/${year}`;
|
||||
case 'YYYY-MM-DD':
|
||||
return `${year}-${month}-${day}`;
|
||||
case 'DD.MM.YYYY':
|
||||
default:
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTimePartWith(
|
||||
d: Date,
|
||||
timeFormatter: Intl.DateTimeFormat,
|
||||
timeFormat: TimeFormat,
|
||||
includeSeconds = false
|
||||
): string {
|
||||
const parts = timeFormatter.formatToParts(d);
|
||||
const hours = parseInt(pickPart(parts, 'hour'), 10);
|
||||
const minutes = pickPart(parts, 'minute');
|
||||
const seconds = pickPart(parts, 'second');
|
||||
|
||||
if (timeFormat === '12h') {
|
||||
const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
return includeSeconds
|
||||
? `${hour12}:${minutes}:${seconds} ${ampm}`
|
||||
: `${hour12}:${minutes} ${ampm}`;
|
||||
} else {
|
||||
const hour24 = hours.toString().padStart(2, '0');
|
||||
return includeSeconds
|
||||
? `${hour24}:${minutes}:${seconds}`
|
||||
: `${hour24}:${minutes}`;
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@
|
||||
* cause breakage are rejected.
|
||||
*/
|
||||
|
||||
// Allowed characters: ASCII letters/digits + _ . - ( ) space + @
|
||||
// Allowed characters: ASCII letters/digits + _ . - ( ) space + @ '
|
||||
// First char: any allowed except space (no leading whitespace).
|
||||
// Last char: any allowed except space and dot (no trailing whitespace,
|
||||
// no trailing dot to avoid Windows-style "hidden" trailing-dot issues).
|
||||
// Length: 1..64.
|
||||
export const ENV_NAME_RE = /^(?! )[A-Za-z0-9_.\-() +@](?:[A-Za-z0-9_.\-() +@]{0,62}[A-Za-z0-9_\-)+@])?$/;
|
||||
// Apostrophe (#1228): safe — env.name only lands in path.join() and
|
||||
// array-form spawn(), never in a shell-interpolated string.
|
||||
export const ENV_NAME_RE = /^(?! )[A-Za-z0-9_.\-()' +@](?:[A-Za-z0-9_.\-()' +@]{0,62}[A-Za-z0-9_\-)'+@])?$/;
|
||||
|
||||
export const ENV_NAME_MAX_LENGTH = 64;
|
||||
|
||||
@@ -39,7 +41,7 @@ export function validateEnvName(name: unknown): ValidationResult {
|
||||
return {
|
||||
ok: false,
|
||||
reason:
|
||||
'Name may contain letters, digits, spaces, and any of - _ . ( ) + @ (no leading/trailing whitespace, no trailing dot, no slashes, no wildcards)'
|
||||
'Name can\'t contain slashes, wildcards, or other shell-special characters, and can\'t start with whitespace or end with whitespace or a dot'
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
*
|
||||
* dockhand.url labels override this — Pangolin extraction is a fallback,
|
||||
* never a winner over an explicit user-provided URL.
|
||||
*
|
||||
* Earlier v1.0.34 used `pangolin.proxy-resources.*` — that label was never
|
||||
* recognised by Pangolin itself and is no longer accepted here.
|
||||
*/
|
||||
export interface PangolinUrl {
|
||||
url: string;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Path-relative redirect target validation.
|
||||
*
|
||||
* Returns true only for targets that are path-relative URLs ('/path?query#hash').
|
||||
* Rejects absolute URLs ('https://...', 'http://...'), protocol-relative URLs
|
||||
* ('//example.com'), backslash-prefixed forms ('\\example.com' — some browsers
|
||||
* normalize these), and non-string inputs.
|
||||
*/
|
||||
export function isSafeRedirect(target: unknown): target is string {
|
||||
if (typeof target !== 'string' || target.length === 0) return false;
|
||||
if (!target.startsWith('/')) return false;
|
||||
if (target.startsWith('//') || target.startsWith('/\\')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the input when it passes isSafeRedirect, otherwise '/'. Use this on
|
||||
* any post-login / post-callback redirect target derived from user input.
|
||||
*/
|
||||
export function safeRedirectOrRoot(target: unknown): string {
|
||||
return isSafeRedirect(target) ? target : '/';
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Pure scanner override resolver (#1219).
|
||||
*
|
||||
* Decides which networkMode + DNS values to pass to the scanner container,
|
||||
* given (auto-detected, user-settings) inputs. Kept SvelteKit/server-free so
|
||||
* the unit tests can import it without triggering DB initialisation.
|
||||
*/
|
||||
|
||||
export function resolveScannerOverrides(
|
||||
autoDetected: { networkMode?: string; extraHosts?: string[] },
|
||||
userSettings: { networkMode?: string; dns?: string[] }
|
||||
): { networkMode?: string; dns?: string[]; extraHosts?: string[] } {
|
||||
const userNet = userSettings.networkMode?.trim();
|
||||
const networkMode = userNet ? userNet : autoDetected.networkMode || undefined;
|
||||
|
||||
const cleanedDns = (userSettings.dns ?? [])
|
||||
.map((d) => d.trim())
|
||||
.filter((d) => d.length > 0);
|
||||
const dns = cleanedDns.length > 0 ? cleanedDns : undefined;
|
||||
|
||||
const extraHosts =
|
||||
autoDetected.extraHosts && autoDetected.extraHosts.length > 0
|
||||
? autoDetected.extraHosts
|
||||
: undefined;
|
||||
|
||||
return { networkMode, dns, extraHosts };
|
||||
}
|
||||
Reference in new issue
Block a user