You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
4.8 KiB
169 lines
4.8 KiB
/**
|
|
* Clock synchronization and skew handling
|
|
*/
|
|
|
|
import { ClockSyncConfig } from './types';
|
|
import { DEFAULT_CONFIG } from './constants';
|
|
|
|
export class ClockSyncManager {
|
|
private config: ClockSyncConfig;
|
|
private lastSyncTime = 0;
|
|
private serverOffset = 0; // Server time - client time
|
|
private syncInterval?: NodeJS.Timeout | undefined;
|
|
|
|
constructor(config: Partial<ClockSyncConfig> = {}) {
|
|
this.config = {
|
|
serverTimeSource: config.serverTimeSource ?? 'ntp',
|
|
ntpServers: config.ntpServers ?? ['pool.ntp.org', 'time.google.com'],
|
|
maxClockSkewSeconds: config.maxClockSkewSeconds ?? DEFAULT_CONFIG.maxClockSkewSeconds,
|
|
skewCheckIntervalMs: config.skewCheckIntervalMs ?? DEFAULT_CONFIG.skewCheckIntervalMs,
|
|
jwtClockSkewTolerance: config.jwtClockSkewTolerance ?? DEFAULT_CONFIG.jwtClockSkewTolerance,
|
|
jwtMaxAge: config.jwtMaxAge ?? DEFAULT_CONFIG.jwtMaxAge
|
|
};
|
|
}
|
|
|
|
async syncWithServer(apiServer: string, jwtToken?: string): Promise<void> {
|
|
try {
|
|
// Get server time from API
|
|
const response = await fetch(`${apiServer}/api/v2/time`, {
|
|
method: 'GET',
|
|
headers: jwtToken ? { 'Authorization': `Bearer ${jwtToken}` } : {}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Clock sync failed: HTTP ${response.status}`);
|
|
}
|
|
|
|
const serverTime = parseInt(response.headers.get('X-Server-Time') || '0');
|
|
const clientTime = Date.now();
|
|
|
|
if (serverTime === 0) {
|
|
throw new Error('Invalid server time response');
|
|
}
|
|
|
|
this.serverOffset = serverTime - clientTime;
|
|
this.lastSyncTime = clientTime;
|
|
|
|
// Validate skew is within tolerance
|
|
if (Math.abs(this.serverOffset) > this.config.maxClockSkewSeconds * 1000) {
|
|
// Large clock skew detected: ${this.serverOffset}ms
|
|
}
|
|
|
|
// Clock sync successful: offset=${this.serverOffset}ms
|
|
|
|
} catch (error) {
|
|
// Clock sync failed: ${error}
|
|
// Continue with client time, but log the issue
|
|
}
|
|
}
|
|
|
|
getServerTime(): number {
|
|
return Date.now() + this.serverOffset;
|
|
}
|
|
|
|
getClientTime(): number {
|
|
return Date.now();
|
|
}
|
|
|
|
getServerOffset(): number {
|
|
return this.serverOffset;
|
|
}
|
|
|
|
getLastSyncTime(): number {
|
|
return this.lastSyncTime;
|
|
}
|
|
|
|
validateJwtTimestamp(jwt: Record<string, unknown>): boolean {
|
|
const now = this.getServerTime();
|
|
const iat = jwt.iat * 1000; // Convert to milliseconds
|
|
const exp = jwt.exp * 1000;
|
|
|
|
// Check if JWT is within valid time window
|
|
const skewTolerance = this.config.jwtClockSkewTolerance * 1000;
|
|
const maxAge = this.config.jwtMaxAge;
|
|
|
|
const isValid = (now >= iat - skewTolerance) &&
|
|
(now <= exp + skewTolerance) &&
|
|
(now - iat <= maxAge);
|
|
|
|
if (!isValid) {
|
|
// JWT timestamp validation failed: ${JSON.stringify({now, iat, exp, skewTolerance, maxAge, serverOffset: this.serverOffset})}
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
isClockSkewExcessive(): boolean {
|
|
return Math.abs(this.serverOffset) > this.config.maxClockSkewSeconds * 1000;
|
|
}
|
|
|
|
needsSync(): boolean {
|
|
const timeSinceLastSync = Date.now() - this.lastSyncTime;
|
|
return timeSinceLastSync > this.config.skewCheckIntervalMs;
|
|
}
|
|
|
|
// Periodic sync
|
|
startPeriodicSync(apiServer: string, jwtToken?: string): void {
|
|
if (this.syncInterval) {
|
|
clearInterval(this.syncInterval);
|
|
}
|
|
|
|
this.syncInterval = setInterval(() => {
|
|
this.syncWithServer(apiServer, jwtToken);
|
|
}, this.config.skewCheckIntervalMs);
|
|
}
|
|
|
|
stopPeriodicSync(): void {
|
|
if (this.syncInterval) {
|
|
clearInterval(this.syncInterval);
|
|
this.syncInterval = undefined;
|
|
}
|
|
}
|
|
|
|
getConfig(): ClockSyncConfig {
|
|
return { ...this.config };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create default clock sync manager
|
|
*/
|
|
export function createDefaultClockSyncManager(): ClockSyncManager {
|
|
return new ClockSyncManager();
|
|
}
|
|
|
|
/**
|
|
* Create clock sync manager with custom config
|
|
*/
|
|
export function createClockSyncManager(config: Partial<ClockSyncConfig>): ClockSyncManager {
|
|
return new ClockSyncManager(config);
|
|
}
|
|
|
|
/**
|
|
* Clock sync configuration presets
|
|
*/
|
|
export const CLOCK_SYNC_PRESETS = {
|
|
// Conservative: Frequent sync, strict tolerance
|
|
conservative: {
|
|
maxClockSkewSeconds: 15,
|
|
skewCheckIntervalMs: 180000, // 3 minutes
|
|
jwtClockSkewTolerance: 15,
|
|
jwtMaxAge: 1800000 // 30 minutes
|
|
},
|
|
|
|
// Balanced: Good balance of sync frequency and tolerance
|
|
balanced: {
|
|
maxClockSkewSeconds: 30,
|
|
skewCheckIntervalMs: 300000, // 5 minutes
|
|
jwtClockSkewTolerance: 30,
|
|
jwtMaxAge: 3600000 // 1 hour
|
|
},
|
|
|
|
// Relaxed: Less frequent sync, more tolerance
|
|
relaxed: {
|
|
maxClockSkewSeconds: 60,
|
|
skewCheckIntervalMs: 600000, // 10 minutes
|
|
jwtClockSkewTolerance: 60,
|
|
jwtMaxAge: 7200000 // 2 hours
|
|
}
|
|
} as const;
|
|
|