mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-22 18:29:19 +02:00
Handle token
This commit is contained in:
parent
2c8bf4f18c
commit
dc50190dc3
7 changed files with 232 additions and 83 deletions
|
@ -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,
|
|
@ -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() {
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export * from "./response";
|
export * from "./response";
|
||||||
|
export { tokenManager, TokenManager } from "./tokenManager";
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
209
server/lib/tokenManager.ts
Normal 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();
|
|
@ -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;
|
Loading…
Add table
Add a link
Reference in a new issue