240 lines
6.1 KiB
TypeScript
240 lines
6.1 KiB
TypeScript
const DEFAULT_ALLOWED_METHODS = [
|
|
"GET",
|
|
"POST",
|
|
"PUT",
|
|
"PATCH",
|
|
"DELETE",
|
|
"OPTIONS",
|
|
];
|
|
const DEFAULT_ALLOWED_HEADERS = [
|
|
"Content-Type",
|
|
"Authorization",
|
|
"X-Requested-With",
|
|
];
|
|
const DEFAULT_EXPOSED_HEADERS = ["Content-Length", "Content-Type"];
|
|
const IS_DEVELOPMENT = Deno.env.get("DEVELOPMENT") === "true";
|
|
|
|
export interface CorsOptions {
|
|
allowedOrigins: Set<string>;
|
|
allowCredentials?: boolean;
|
|
allowedMethods?: string[];
|
|
allowedHeaders?: string[];
|
|
exposeHeaders?: string[];
|
|
requestHeaders?: string | null;
|
|
maxAgeSeconds?: number;
|
|
}
|
|
|
|
function normalizeOrigin(raw: string | null | undefined): string | null {
|
|
if (!raw) return null;
|
|
try {
|
|
const trimmed = raw.trim();
|
|
const hasScheme = /^https?:\/\//i.test(trimmed);
|
|
const needsHttp = /^(localhost|127\.0\.0\.1)/i.test(trimmed);
|
|
const candidate = hasScheme
|
|
? trimmed
|
|
: `${needsHttp ? "http" : "https"}://${trimmed}`;
|
|
const url = new URL(candidate);
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
return null;
|
|
}
|
|
return `${url.protocol}//${url.host}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function parseOriginList(raw: string | null | undefined): Set<string> {
|
|
const allowed = new Set<string>();
|
|
if (!raw) return allowed;
|
|
for (const entry of raw.split(",")) {
|
|
const normalized = normalizeOrigin(entry.trim());
|
|
if (normalized) {
|
|
allowed.add(normalized);
|
|
}
|
|
}
|
|
return allowed;
|
|
}
|
|
|
|
function deriveDefaultEdgeOrigins(): Set<string> {
|
|
const allowed = new Set<string>();
|
|
const redirectHome = normalizeOrigin(Deno.env.get("REDIRECT_HOME"));
|
|
if (redirectHome) {
|
|
allowed.add(redirectHome);
|
|
}
|
|
|
|
const hostname = Deno.env.get("HOSTNAME_DENO");
|
|
const port = Deno.env.get("PORT");
|
|
if (hostname && port) {
|
|
const devOrigin = normalizeOrigin(`http://${hostname}:${port}`);
|
|
if (devOrigin) {
|
|
allowed.add(devOrigin);
|
|
}
|
|
}
|
|
|
|
return allowed;
|
|
}
|
|
|
|
const EDGE_ALLOWED_ORIGINS = (() => {
|
|
const fromEnv = parseOriginList(Deno.env.get("EDGE_ALLOWED_ORIGINS"));
|
|
if (fromEnv.size > 0) {
|
|
return fromEnv;
|
|
}
|
|
return deriveDefaultEdgeOrigins();
|
|
})();
|
|
|
|
const ISLAND_ALLOWED_ORIGINS = new Set([
|
|
"https://cyberhybridhub.com",
|
|
"http://cyberhybridhub.com",
|
|
"http://localhost:8080",
|
|
"http://96.3.218.128",
|
|
"https://96.3.218.126",
|
|
]);
|
|
|
|
function mergeVary(current: string | null, incoming: string): string {
|
|
const parts = new Set<string>();
|
|
for (const entry of (current ?? "").split(",")) {
|
|
const trimmed = entry.trim();
|
|
if (trimmed) {
|
|
parts.add(trimmed);
|
|
}
|
|
}
|
|
for (const entry of incoming.split(",")) {
|
|
const trimmed = entry.trim();
|
|
if (trimmed) {
|
|
parts.add(trimmed);
|
|
}
|
|
}
|
|
return Array.from(parts).join(", ");
|
|
}
|
|
|
|
function buildAllowedHeaders(
|
|
baseHeaders: string[],
|
|
requestHeaders?: string | null,
|
|
): string[] {
|
|
const allowed = new Set(baseHeaders.map((header) => header.toLowerCase()));
|
|
if (requestHeaders) {
|
|
for (const header of requestHeaders.split(",")) {
|
|
const trimmed = header.trim();
|
|
if (trimmed) {
|
|
allowed.add(trimmed.toLowerCase());
|
|
}
|
|
}
|
|
}
|
|
return Array.from(allowed).map((header) =>
|
|
header
|
|
.split("-")
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join("-")
|
|
);
|
|
}
|
|
|
|
export function isIslandFetchRequestAllowed(
|
|
url: URL,
|
|
originHeader?: string | null,
|
|
): boolean {
|
|
if (IS_DEVELOPMENT) return true;
|
|
|
|
// Check if the request origin is allowed
|
|
if (originHeader) {
|
|
const normalizedOrigin = normalizeOrigin(originHeader);
|
|
if (normalizedOrigin && ISLAND_ALLOWED_ORIGINS.has(normalizedOrigin)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Fallback to checking the URL origin
|
|
return ISLAND_ALLOWED_ORIGINS.has(url.origin);
|
|
}
|
|
|
|
export function getCorsHeaders(
|
|
origin: string | null,
|
|
options: CorsOptions,
|
|
): Headers {
|
|
const headers = new Headers();
|
|
const normalizedOrigin = normalizeOrigin(origin);
|
|
if (!normalizedOrigin || !options.allowedOrigins.has(normalizedOrigin)) {
|
|
return headers;
|
|
}
|
|
|
|
headers.set("Access-Control-Allow-Origin", normalizedOrigin);
|
|
headers.set(
|
|
"Access-Control-Allow-Methods",
|
|
(options.allowedMethods ?? DEFAULT_ALLOWED_METHODS).join(", "),
|
|
);
|
|
headers.set(
|
|
"Access-Control-Allow-Headers",
|
|
buildAllowedHeaders(
|
|
options.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS,
|
|
options.requestHeaders,
|
|
).join(", "),
|
|
);
|
|
headers.set(
|
|
"Access-Control-Expose-Headers",
|
|
(options.exposeHeaders ?? DEFAULT_EXPOSED_HEADERS).join(", "),
|
|
);
|
|
if (options.allowCredentials ?? true) {
|
|
headers.set("Access-Control-Allow-Credentials", "true");
|
|
}
|
|
if (options.maxAgeSeconds) {
|
|
headers.set("Access-Control-Max-Age", `${options.maxAgeSeconds}`);
|
|
}
|
|
headers.set(
|
|
"Vary",
|
|
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
|
|
);
|
|
return headers;
|
|
}
|
|
|
|
export function getEdgeCorsHeaders(
|
|
origin: string | null,
|
|
requestHeaders?: string | null,
|
|
): Headers {
|
|
return getCorsHeaders(origin, {
|
|
allowedOrigins: EDGE_ALLOWED_ORIGINS,
|
|
requestHeaders,
|
|
});
|
|
}
|
|
|
|
export function getIslandCorsHeaders(
|
|
origin: string | null,
|
|
requestHeaders?: string | null,
|
|
): Headers {
|
|
return getCorsHeaders(origin, {
|
|
allowedOrigins: ISLAND_ALLOWED_ORIGINS,
|
|
requestHeaders,
|
|
});
|
|
}
|
|
|
|
export function withCors(response: Response, corsHeaders: Headers): Response {
|
|
if (!corsHeaders || corsHeaders.values().toArray().length === 0) {
|
|
return response;
|
|
}
|
|
const headers = new Headers(response.headers);
|
|
for (const [key, value] of corsHeaders.entries()) {
|
|
if (key.toLowerCase() === "vary") {
|
|
headers.set("Vary", mergeVary(headers.get("Vary"), value));
|
|
} else {
|
|
headers.set(key, value);
|
|
}
|
|
}
|
|
return new Response(response.body, {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
export function createCorsPreflightResponse(
|
|
_request: Request,
|
|
corsHeaders: Headers,
|
|
): Response {
|
|
if (!corsHeaders || !corsHeaders.get("Access-Control-Allow-Origin")) {
|
|
return new Response("Forbidden", { status: 403 });
|
|
}
|
|
return new Response(null, { status: 204, headers: corsHeaders });
|
|
}
|
|
|
|
export function getEdgeAllowedOrigins(): string[] {
|
|
return Array.from(EDGE_ALLOWED_ORIGINS);
|
|
}
|