mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-25 12:06:37 +00:00
Cache token for thundering hurd
This commit is contained in:
22
server/lib/tokenCache.ts
Normal file
22
server/lib/tokenCache.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Returns a cached plaintext token from Redis if one exists and decrypts
|
||||||
|
* cleanly, otherwise calls `createSession` to mint a fresh token, stores the
|
||||||
|
* encrypted value in Redis with the given TTL, and returns it.
|
||||||
|
*
|
||||||
|
* Failures at the Redis layer are non-fatal – the function always falls
|
||||||
|
* through to session creation so the caller is never blocked by a Redis outage.
|
||||||
|
*
|
||||||
|
* @param cacheKey Unique Redis key, e.g. `"newt:token_cache:abc123"`
|
||||||
|
* @param secret Server secret used for AES encryption/decryption
|
||||||
|
* @param ttlSeconds Cache TTL in seconds (should match session expiry)
|
||||||
|
* @param createSession Factory that mints a new session and returns its raw token
|
||||||
|
*/
|
||||||
|
export async function getOrCreateCachedToken(
|
||||||
|
cacheKey: string,
|
||||||
|
secret: string,
|
||||||
|
ttlSeconds: number,
|
||||||
|
createSession: () => Promise<string>
|
||||||
|
): Promise<string> {
|
||||||
|
const token = await createSession();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
@@ -1,3 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { redisManager } from "@server/private/lib/redis";
|
import { redisManager } from "@server/private/lib/redis";
|
||||||
|
|||||||
77
server/private/lib/tokenCache.ts
Normal file
77
server/private/lib/tokenCache.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import redisManager from "#dynamic/lib/redis";
|
||||||
|
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cached plaintext token from Redis if one exists and decrypts
|
||||||
|
* cleanly, otherwise calls `createSession` to mint a fresh token, stores the
|
||||||
|
* encrypted value in Redis with the given TTL, and returns it.
|
||||||
|
*
|
||||||
|
* Failures at the Redis layer are non-fatal – the function always falls
|
||||||
|
* through to session creation so the caller is never blocked by a Redis outage.
|
||||||
|
*
|
||||||
|
* @param cacheKey Unique Redis key, e.g. `"newt:token_cache:abc123"`
|
||||||
|
* @param secret Server secret used for AES encryption/decryption
|
||||||
|
* @param ttlSeconds Cache TTL in seconds (should match session expiry)
|
||||||
|
* @param createSession Factory that mints a new session and returns its raw token
|
||||||
|
*/
|
||||||
|
export async function getOrCreateCachedToken(
|
||||||
|
cacheKey: string,
|
||||||
|
secret: string,
|
||||||
|
ttlSeconds: number,
|
||||||
|
createSession: () => Promise<string>
|
||||||
|
): Promise<string> {
|
||||||
|
if (redisManager.isRedisEnabled()) {
|
||||||
|
try {
|
||||||
|
const cached = await redisManager.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
const token = decrypt(cached, secret);
|
||||||
|
if (token) {
|
||||||
|
logger.debug(`Token cache hit for key: ${cacheKey}`);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
// Decryption produced an empty string – treat as a miss
|
||||||
|
logger.warn(
|
||||||
|
`Token cache decryption returned empty string for key: ${cacheKey}, treating as miss`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(
|
||||||
|
`Token cache read/decrypt failed for key ${cacheKey}, falling through to session creation:`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await createSession();
|
||||||
|
|
||||||
|
if (redisManager.isRedisEnabled()) {
|
||||||
|
try {
|
||||||
|
const encrypted = encrypt(token, secret);
|
||||||
|
await redisManager.set(cacheKey, encrypted, ttlSeconds);
|
||||||
|
logger.debug(
|
||||||
|
`Token cached in Redis for key: ${cacheKey} (TTL ${ttlSeconds}s)`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(
|
||||||
|
`Token cache write failed for key ${cacheKey} (session was still created):`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
@@ -23,8 +23,10 @@ import { z } from "zod";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
createRemoteExitNodeSession,
|
createRemoteExitNodeSession,
|
||||||
validateRemoteExitNodeSessionToken
|
validateRemoteExitNodeSessionToken,
|
||||||
|
EXPIRES
|
||||||
} from "#private/auth/sessions/remoteExitNode";
|
} from "#private/auth/sessions/remoteExitNode";
|
||||||
|
import { getOrCreateCachedToken } from "@server/private/lib/tokenCache";
|
||||||
import { verifyPassword } from "@server/auth/password";
|
import { verifyPassword } from "@server/auth/password";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -103,14 +105,23 @@ export async function getRemoteExitNodeToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resToken = generateSessionToken();
|
// Return a cached token if one exists to prevent thundering herd on
|
||||||
await createRemoteExitNodeSession(
|
// simultaneous restarts; falls back to creating a fresh session when
|
||||||
resToken,
|
// Redis is unavailable or the cache has expired.
|
||||||
existingRemoteExitNode.remoteExitNodeId
|
const resToken = await getOrCreateCachedToken(
|
||||||
|
`remote_exit_node:token_cache:${existingRemoteExitNode.remoteExitNodeId}`,
|
||||||
|
config.getRawConfig().server.secret!,
|
||||||
|
Math.floor(EXPIRES / 1000),
|
||||||
|
async () => {
|
||||||
|
const token = generateSessionToken();
|
||||||
|
await createRemoteExitNodeSession(
|
||||||
|
token,
|
||||||
|
existingRemoteExitNode.remoteExitNodeId
|
||||||
|
);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// logger.debug(`Created RemoteExitNode token response: ${JSON.stringify(resToken)}`);
|
|
||||||
|
|
||||||
return response<{ token: string }>(res, {
|
return response<{ token: string }>(res, {
|
||||||
data: {
|
data: {
|
||||||
token: resToken
|
token: resToken
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
import { db, newtSessions } from "@server/db";
|
import { db, newtSessions } from "@server/db";
|
||||||
import { newts } from "@server/db";
|
import { newts } from "@server/db";
|
||||||
|
import { getOrCreateCachedToken } from "#dynamic/lib/tokenCache";
|
||||||
|
import { EXPIRES } from "@server/auth/sessions/newt";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -92,9 +94,19 @@ export async function getNewtToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise generate a new one
|
// Return a cached token if one exists to prevent thundering herd on
|
||||||
const resToken = generateSessionToken();
|
// simultaneous restarts; falls back to creating a fresh session when
|
||||||
await createNewtSession(resToken, existingNewt.newtId);
|
// Redis is unavailable or the cache has expired.
|
||||||
|
const resToken = await getOrCreateCachedToken(
|
||||||
|
`newt:token_cache:${existingNewt.newtId}`,
|
||||||
|
config.getRawConfig().server.secret!,
|
||||||
|
Math.floor(EXPIRES / 1000),
|
||||||
|
async () => {
|
||||||
|
const token = generateSessionToken();
|
||||||
|
await createNewtSession(token, existingNewt.newtId);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return response<{ token: string; serverVersion: string }>(res, {
|
return response<{ token: string; serverVersion: string }>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import { z } from "zod";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
createOlmSession,
|
createOlmSession,
|
||||||
validateOlmSessionToken
|
validateOlmSessionToken,
|
||||||
|
EXPIRES
|
||||||
} from "@server/auth/sessions/olm";
|
} from "@server/auth/sessions/olm";
|
||||||
|
import { getOrCreateCachedToken } from "#dynamic/lib/tokenCache";
|
||||||
import { verifyPassword } from "@server/auth/password";
|
import { verifyPassword } from "@server/auth/password";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -132,8 +134,19 @@ export async function getOlmToken(
|
|||||||
|
|
||||||
logger.debug("Creating new olm session token");
|
logger.debug("Creating new olm session token");
|
||||||
|
|
||||||
const resToken = generateSessionToken();
|
// Return a cached token if one exists to prevent thundering herd on
|
||||||
await createOlmSession(resToken, existingOlm.olmId);
|
// simultaneous restarts; falls back to creating a fresh session when
|
||||||
|
// Redis is unavailable or the cache has expired.
|
||||||
|
const resToken = await getOrCreateCachedToken(
|
||||||
|
`olm:token_cache:${existingOlm.olmId}`,
|
||||||
|
config.getRawConfig().server.secret!,
|
||||||
|
Math.floor(EXPIRES / 1000),
|
||||||
|
async () => {
|
||||||
|
const token = generateSessionToken();
|
||||||
|
await createOlmSession(token, existingOlm.olmId);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let clientIdToUse;
|
let clientIdToUse;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user