194 lines
6.2 KiB
TypeScript
Executable File
194 lines
6.2 KiB
TypeScript
Executable File
import { define } from "../utils.ts";
|
|
import { Context } from "fresh";
|
|
import * as Iron from "iron";
|
|
import { deleteStateKey, getStateKeys, setStateKey } from "./userService.ts";
|
|
import { getCookies, setCookie } from "$std/http/cookie.ts";
|
|
import { runStateKeyCleanup } from "./dbCleanup.ts";
|
|
import {
|
|
generatePassword,
|
|
isSecureRequest,
|
|
persistWithEncryptedCookie,
|
|
SessionCookie,
|
|
} from "../routes/auth/callback.ts";
|
|
import { getE2ESession, isE2EEnabled } from "./e2eFixtures.ts";
|
|
|
|
const useDb = true;
|
|
export function generateNonce(length = 32) {
|
|
const array = new Uint8Array(length);
|
|
crypto.getRandomValues(array);
|
|
return btoa(String.fromCharCode(...array)).replace(/[^a-zA-Z0-9]/g, "");
|
|
}
|
|
|
|
export interface SessionCookieParams {
|
|
sessionCookie: string | null;
|
|
stateId: string | null;
|
|
}
|
|
|
|
export function readSessionCookiesFromCookies(
|
|
headers: Headers,
|
|
): SessionCookieParams {
|
|
const cookies: Record<string, string> = getCookies(headers);
|
|
return {
|
|
sessionCookie: cookies["session"] ?? null,
|
|
stateId: cookies["xx_state_id"] ?? null,
|
|
};
|
|
}
|
|
|
|
export async function hashText(text: string): Promise<string> {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(text);
|
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(
|
|
"",
|
|
);
|
|
return hashHex;
|
|
}
|
|
|
|
export function InitiPGSessionMiddleware() {
|
|
return define.middleware(async (ctx: Context<any>) => {
|
|
const url = new URL(ctx.req.url);
|
|
|
|
console.log('path', url.pathname);
|
|
if (isE2EEnabled()) {
|
|
const e2eUser = ctx.req.headers.get("x-e2e-user") ??
|
|
url.searchParams.get("e2e-user");
|
|
const e2eSession = getE2ESession(e2eUser);
|
|
if (e2eSession) {
|
|
ctx.state.photoUrl = "";
|
|
ctx.state.data = e2eSession;
|
|
ctx.state.host = url.href;
|
|
ctx.state.urlRoute = url.pathname;
|
|
ctx.state.stateId = "e2e-state";
|
|
return await ctx.next();
|
|
}
|
|
}
|
|
|
|
if (url.hostname === "cyberhybridhub.com") {
|
|
return Response.redirect("https://portal.cyberhybridhub.com", 302);
|
|
}
|
|
if (
|
|
url.pathname.startsWith("/api/ai-images/file/") ||
|
|
url.pathname.startsWith("/api/audio-blob/") ||
|
|
url.pathname.startsWith("/api/image-blob/") ||
|
|
url.pathname === "/health" || url.pathname.startsWith("/auth/") ||
|
|
url.pathname.startsWith("/node_modules/") ||
|
|
url.pathname.startsWith("/.") ||
|
|
url.pathname.startsWith("/favicon.ico")
|
|
) {
|
|
return await ctx.next();
|
|
}
|
|
|
|
const timestampNow = Date.now();
|
|
let sessionData: SessionCookie | null = null;
|
|
|
|
const sessionCookieParams = readSessionCookiesFromCookies(ctx.req.headers);
|
|
|
|
if (url.searchParams.get("popstate") === "true") {
|
|
console.log("popstate");
|
|
url.searchParams.delete("popstate");
|
|
if (sessionCookieParams.sessionCookie) {
|
|
url.pathname = "/auth/logout";
|
|
return Response.redirect(url.toString(), 302);
|
|
} else {
|
|
return Response.redirect(url.toString(), 302);
|
|
}
|
|
}
|
|
|
|
const xxStateId = sessionCookieParams.stateId;
|
|
|
|
let theStateKey: string | null = null;
|
|
if (xxStateId) {
|
|
if (useDb) {
|
|
const stateKeys = await getStateKeys(xxStateId);
|
|
|
|
stateKeys.sort((a: any, b: any) => b.created_on - a.created_on);
|
|
const timestamp = timestampNow - (1000 * 60 * 60);
|
|
for (const stateKey of stateKeys) {
|
|
if (stateKey.created_on < timestamp) {
|
|
deleteStateKey(stateKey.state_id);
|
|
} else if (sessionCookieParams.sessionCookie) {
|
|
let stateData: SessionCookie | null = null;
|
|
|
|
try {
|
|
stateData = await Iron.unseal(
|
|
sessionCookieParams.sessionCookie,
|
|
stateKey.seal_key,
|
|
Iron.defaults,
|
|
) as SessionCookie;
|
|
} catch (_e) {
|
|
console.log("DEBUG stateMiddleware: error unsealing cookie", _e);
|
|
return new Response(null, {
|
|
status: 302,
|
|
headers: { Location: "/auth/logout" },
|
|
});
|
|
}
|
|
|
|
if (stateData && stateData.user_id) {
|
|
const thisHash = await hashText(JSON.stringify(stateData));
|
|
|
|
if (thisHash === stateKey.cookie_hash) {
|
|
const renewWindow = timestampNow - (5 * 60 * 1000);
|
|
const newKey = generatePassword();
|
|
if (stateKey.created_on < renewWindow) {
|
|
stateData.started_on = timestampNow;
|
|
const cookieHash = await hashText(JSON.stringify(stateData));
|
|
await setStateKey(newKey, xxStateId, cookieHash);
|
|
theStateKey = newKey;
|
|
} else {
|
|
theStateKey = stateKey.seal_key;
|
|
}
|
|
|
|
sessionData = stateData;
|
|
break;
|
|
} else {
|
|
console.log(
|
|
"DEBUG stateMiddleware: hash mismatch, cookie invalid",
|
|
);
|
|
}
|
|
} else {
|
|
console.log(
|
|
"DEBUG stateMiddleware: stateData missing or no user_id",
|
|
{
|
|
hasStateData: !!stateData,
|
|
user_id: stateData?.user_id,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.state.photoUrl = "";
|
|
ctx.state.data = sessionData;
|
|
ctx.state.host = url.href;
|
|
ctx.state.urlRoute = url.pathname;
|
|
ctx.state.stateId = sessionCookieParams.stateId;
|
|
|
|
const preResp = await ctx.next();
|
|
|
|
if (sessionCookieParams.sessionCookie && !sessionData) {
|
|
const expiredDate = new Date(0).toUTCString();
|
|
const isSecure = isSecureRequest(ctx.req);
|
|
const deleteCookie =
|
|
`session=; Expires=${expiredDate}; Path=/; HttpOnly=true; Secure=${isSecure}; SameSite=Lax`;
|
|
preResp.headers.append("Set-Cookie", deleteCookie);
|
|
}
|
|
|
|
runStateKeyCleanup().catch((err) => {
|
|
console.error("State key cleanup error:", err);
|
|
});
|
|
|
|
return theStateKey && sessionData
|
|
? persistWithEncryptedCookie(
|
|
sessionData as SessionCookie,
|
|
theStateKey,
|
|
preResp,
|
|
ctx.req,
|
|
xxStateId || "",
|
|
)
|
|
: preResp;
|
|
});
|
|
}
|