Handle token

This commit is contained in:
Owen 2025-08-13 17:30:59 -07:00
parent 2c8bf4f18c
commit dc50190dc3
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
7 changed files with 232 additions and 83 deletions

View file

@ -3,10 +3,11 @@ import express from "express";
import { parse } from "url"; import { parse } from "url";
import logger from "@server/logger"; import logger from "@server/logger";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { WebSocketClient, createWebSocketClient } from "./routers/ws/client"; import { createWebSocketClient } from "./routers/ws/client";
import { addPeer, deletePeer } from "./routers/gerbil/peers"; import { addPeer, deletePeer } from "./routers/gerbil/peers";
import { db, exitNodes } from "./db"; import { db, exitNodes } from "./db";
import { TraefikConfigManager } from "./lib/remoteTraefikConfig"; import { TraefikConfigManager } from "./lib/remoteTraefikConfig";
import { tokenManager } from "./lib/tokenManager";
export async function createHybridClientServer() { export async function createHybridClientServer() {
const monitor = new TraefikConfigManager(); const monitor = new TraefikConfigManager();
@ -21,11 +22,14 @@ export async function createHybridClientServer() {
throw new Error("Hybrid configuration is not defined"); throw new Error("Hybrid configuration is not defined");
} }
// Start the token manager
await tokenManager.start();
const token = await tokenManager.getToken();
// Create client // Create client
const client = createWebSocketClient( const client = createWebSocketClient(
"remoteExitNode", // or 'olm' token,
config.getRawConfig().hybrid!.id!,
config.getRawConfig().hybrid!.secret!,
config.getRawConfig().hybrid!.endpoint!, config.getRawConfig().hybrid!.endpoint!,
{ {
reconnectInterval: 5000, reconnectInterval: 5000,

View file

@ -7,7 +7,7 @@ import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer"; import { createInternalServer } from "./internalServer";
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
import { createIntegrationApiServer } from "./integrationApiServer"; import { createIntegrationApiServer } from "./integrationApiServer";
import { createHybridClientServer } from "./hybridClientServer"; import { createHybridClientServer } from "./privateHybridServer.js";
import config from "@server/lib/config"; import config from "@server/lib/config";
async function startServers() { async function startServers() {

View file

@ -1 +1,2 @@
export * from "./response"; export * from "./response";
export { tokenManager, TokenManager } from "./tokenManager";

View file

@ -5,6 +5,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { tokenManager } from "./tokenManager";
/** /**
* Proxy function that forwards requests to the remote cloud server * Proxy function that forwards requests to the remote cloud server
@ -28,6 +29,7 @@ export const proxyToRemote = async (
data: req.body, data: req.body,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(await tokenManager.getAuthHeader()).headers
}, },
params: req.query, params: req.query,
timeout: 30000, // 30 second timeout timeout: 30000, // 30 second timeout

View file

@ -5,6 +5,7 @@ import logger from "@server/logger";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import axios from "axios"; import axios from "axios";
import { db, exitNodes } from "@server/db"; import { db, exitNodes } from "@server/db";
import { tokenManager } from "./tokenManager";
export class TraefikConfigManager { export class TraefikConfigManager {
private intervalId: NodeJS.Timeout | null = null; private intervalId: NodeJS.Timeout | null = null;
@ -162,7 +163,8 @@ export class TraefikConfigManager {
} | null> { } | null> {
try { try {
const resp = await axios.get( const resp = await axios.get(
`${config.getRawConfig().hybrid?.endpoint}/get-traefik-config` `${config.getRawConfig().hybrid?.endpoint}/traefik-config`,
await tokenManager.getAuthHeader()
); );
if (resp.status !== 200) { if (resp.status !== 200) {

209
server/lib/tokenManager.ts Normal file
View file

@ -0,0 +1,209 @@
import axios from "axios";
import config from "@server/lib/config";
import logger from "@server/logger";
export interface TokenResponse {
success: boolean;
message?: string;
data: {
token: string;
};
}
/**
* Token Manager - Handles automatic token refresh for hybrid server authentication
*
* Usage throughout the application:
* ```typescript
* import { tokenManager } from "@server/lib/tokenManager";
*
* // Get the current valid token
* const token = await tokenManager.getToken();
*
* // Force refresh if needed
* await tokenManager.refreshToken();
* ```
*
* The token manager automatically refreshes tokens every 24 hours by default
* and is started once in the privateHybridServer.ts file.
*/
export class TokenManager {
private token: string | null = null;
private refreshInterval: NodeJS.Timeout | null = null;
private isRefreshing: boolean = false;
private refreshIntervalMs: number;
constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000) {
// Default to 24 hours
this.refreshIntervalMs = refreshIntervalMs;
}
/**
* Start the token manager - gets initial token and sets up refresh interval
*/
async start(): Promise<void> {
try {
await this.refreshToken();
this.setupRefreshInterval();
logger.info("Token manager started successfully");
} catch (error) {
logger.error("Failed to start token manager:", error);
throw error;
}
}
/**
* Stop the token manager and clear refresh interval
*/
stop(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
logger.info("Token manager stopped");
}
/**
* Get the current valid token
*/
async getToken(): Promise<string> {
if (!this.token) {
if (this.isRefreshing) {
// Wait for current refresh to complete
await this.waitForRefresh();
} else {
await this.refreshToken();
}
}
if (!this.token) {
throw new Error("No valid token available");
}
return this.token;
}
async getAuthHeader() {
return {
headers: {
Authorization: `Bearer ${await this.getToken()}`
}
};
}
/**
* Force refresh the token
*/
async refreshToken(): Promise<void> {
if (this.isRefreshing) {
await this.waitForRefresh();
return;
}
this.isRefreshing = true;
try {
const hybridConfig = config.getRawConfig().hybrid;
if (
!hybridConfig?.id ||
!hybridConfig?.secret ||
!hybridConfig?.endpoint
) {
throw new Error("Hybrid configuration is not defined");
}
const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`;
const tokenData = {
remoteExitNodeId: hybridConfig.id,
secret: hybridConfig.secret
};
logger.debug("Requesting new token from server");
const response = await axios.post<TokenResponse>(
tokenEndpoint,
tokenData,
{
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": "x-csrf-protection"
},
timeout: 10000 // 10 second timeout
}
);
if (!response.data.success) {
throw new Error(
`Failed to get token: ${response.data.message}`
);
}
if (!response.data.data.token) {
throw new Error("Received empty token from server");
}
this.token = response.data.data.token;
logger.debug("Token refreshed successfully");
} catch (error) {
logger.error("Failed to refresh token:", error);
if (axios.isAxiosError(error)) {
if (error.response) {
throw new Error(
`Failed to get token with status code: ${error.response.status}`
);
} else if (error.request) {
throw new Error(
"Failed to request new token: No response received"
);
} else {
throw new Error(
`Failed to request new token: ${error.message}`
);
}
} else {
throw new Error(`Failed to get token: ${error}`);
}
} finally {
this.isRefreshing = false;
}
}
/**
* Set up automatic token refresh interval
*/
private setupRefreshInterval(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
this.refreshInterval = setInterval(async () => {
try {
logger.debug("Auto-refreshing token");
await this.refreshToken();
} catch (error) {
logger.error("Failed to auto-refresh token:", error);
}
}, this.refreshIntervalMs);
}
/**
* Wait for current refresh operation to complete
*/
private async waitForRefresh(): Promise<void> {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (!this.isRefreshing) {
clearInterval(checkInterval);
resolve();
}
}, 100);
});
}
}
// Export a singleton instance for use throughout the application
export const tokenManager = new TokenManager();

View file

@ -14,14 +14,6 @@ export interface WSMessage {
data: any; data: any;
} }
export interface TokenResponse {
success: boolean;
message?: string;
data: {
token: string;
};
}
export type MessageHandler = (message: WSMessage) => void; export type MessageHandler = (message: WSMessage) => void;
export interface ClientOptions { export interface ClientOptions {
@ -33,45 +25,32 @@ export interface ClientOptions {
export class WebSocketClient extends EventEmitter { export class WebSocketClient extends EventEmitter {
private conn: WebSocket | null = null; private conn: WebSocket | null = null;
private config: Config;
private baseURL: string; private baseURL: string;
private handlers: Map<string, MessageHandler> = new Map(); private handlers: Map<string, MessageHandler> = new Map();
private reconnectInterval: number; private reconnectInterval: number;
private isConnected: boolean = false; private isConnected: boolean = false;
private pingInterval: number; private pingInterval: number;
private pingTimeout: number; private pingTimeout: number;
private clientType: string;
private shouldReconnect: boolean = true; private shouldReconnect: boolean = true;
private reconnectTimer: NodeJS.Timeout | null = null; private reconnectTimer: NodeJS.Timeout | null = null;
private pingTimer: NodeJS.Timeout | null = null; private pingTimer: NodeJS.Timeout | null = null;
private pingTimeoutTimer: NodeJS.Timeout | null = null; private pingTimeoutTimer: NodeJS.Timeout | null = null;
private token: string;
constructor( constructor(
clientType: string, token: string,
id: string,
secret: string,
endpoint: string, endpoint: string,
options: ClientOptions = {} options: ClientOptions = {}
) { ) {
super(); super();
this.clientType = clientType; this.token = token;
this.config = {
id,
secret,
endpoint
};
this.baseURL = options.baseURL || endpoint; this.baseURL = options.baseURL || endpoint;
this.reconnectInterval = options.reconnectInterval || 3000; this.reconnectInterval = options.reconnectInterval || 3000;
this.pingInterval = options.pingInterval || 30000; this.pingInterval = options.pingInterval || 30000;
this.pingTimeout = options.pingTimeout || 10000; this.pingTimeout = options.pingTimeout || 10000;
} }
public getConfig(): Config {
return this.config;
}
public async connect(): Promise<void> { public async connect(): Promise<void> {
this.shouldReconnect = true; this.shouldReconnect = true;
await this.connectWithRetry(); await this.connectWithRetry();
@ -161,48 +140,6 @@ export class WebSocketClient extends EventEmitter {
return this.isConnected; return this.isConnected;
} }
private async getToken(): Promise<string> {
const baseURL = new URL(this.baseURL);
const tokenEndpoint = `${baseURL.origin}/api/v1/auth/${this.clientType}/get-token`;
const tokenData = this.clientType === 'newt'
? { newtId: this.config.id, secret: this.config.secret }
: { olmId: this.config.id, secret: this.config.secret };
try {
const response = await axios.post<TokenResponse>(tokenEndpoint, tokenData, {
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': 'x-csrf-protection'
},
timeout: 10000 // 10 second timeout
});
if (!response.data.success) {
throw new Error(`Failed to get token: ${response.data.message}`);
}
if (!response.data.data.token) {
throw new Error('Received empty token from server');
}
console.debug(`Received token: ${response.data.data.token}`);
return response.data.data.token;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response) {
throw new Error(`Failed to get token with status code: ${error.response.status}`);
} else if (error.request) {
throw new Error('Failed to request new token: No response received');
} else {
throw new Error(`Failed to request new token: ${error.message}`);
}
} else {
throw new Error(`Failed to get token: ${error}`);
}
}
}
private async connectWithRetry(): Promise<void> { private async connectWithRetry(): Promise<void> {
while (this.shouldReconnect) { while (this.shouldReconnect) {
try { try {
@ -221,18 +158,14 @@ export class WebSocketClient extends EventEmitter {
} }
private async establishConnection(): Promise<void> { private async establishConnection(): Promise<void> {
// Get token for authentication
const token = await this.getToken();
this.emit('tokenUpdate', token);
// Parse the base URL to determine protocol and hostname // Parse the base URL to determine protocol and hostname
const baseURL = new URL(this.baseURL); const baseURL = new URL(this.baseURL);
const wsProtocol = baseURL.protocol === 'https:' ? 'wss' : 'ws'; const wsProtocol = baseURL.protocol === 'https:' ? 'wss' : 'ws';
const wsURL = new URL(`${wsProtocol}://${baseURL.host}/api/v1/ws`); const wsURL = new URL(`${wsProtocol}://${baseURL.host}/api/v1/ws`);
// Add token and client type to query parameters // Add token and client type to query parameters
wsURL.searchParams.set('token', token); wsURL.searchParams.set('token', this.token);
wsURL.searchParams.set('clientType', this.clientType); wsURL.searchParams.set('clientType', "remoteExitNode");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const conn = new WebSocket(wsURL.toString()); const conn = new WebSocket(wsURL.toString());
@ -330,13 +263,11 @@ export class WebSocketClient extends EventEmitter {
// Factory function for easier instantiation // Factory function for easier instantiation
export function createWebSocketClient( export function createWebSocketClient(
clientType: string, token: string,
id: string,
secret: string,
endpoint: string, endpoint: string,
options?: ClientOptions options?: ClientOptions
): WebSocketClient { ): WebSocketClient {
return new WebSocketClient(clientType, id, secret, endpoint, options); return new WebSocketClient(token, endpoint, options);
} }
export default WebSocketClient; export default WebSocketClient;