most of the implementation looking ok but sharing the token the wrong way
This commit is contained in:
parent
743b6b207f
commit
412b5d9486
6 changed files with 364 additions and 62 deletions
181
src/lib/coordination.ts
Normal file
181
src/lib/coordination.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
import { checkLockfile, createLockfile, deleteLockfile, getConfigFilePath, LockfileData } from './mcp-auth-config'
|
||||
import { EventEmitter } from 'events'
|
||||
import { Server } from 'http'
|
||||
import express from 'express'
|
||||
import { AddressInfo } from 'net'
|
||||
import { log, setupOAuthCallbackServerWithLongPoll } from './utils'
|
||||
|
||||
/**
|
||||
* Checks if a process with the given PID is running
|
||||
* @param pid The process ID to check
|
||||
* @returns True if the process is running, false otherwise
|
||||
*/
|
||||
export async function isPidRunning(pid: number): Promise<boolean> {
|
||||
try {
|
||||
process.kill(pid, 0) // Doesn't kill the process, just checks if it exists
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a lockfile is valid (process running and endpoint accessible)
|
||||
* @param lockData The lockfile data
|
||||
* @returns True if the lockfile is valid, false otherwise
|
||||
*/
|
||||
export async function isLockValid(lockData: LockfileData): Promise<boolean> {
|
||||
// Check if the lockfile is too old (over 30 minutes)
|
||||
const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes
|
||||
if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) {
|
||||
log('Lockfile is too old')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the process is still running
|
||||
if (!(await isPidRunning(lockData.pid))) {
|
||||
log('Process from lockfile is not running')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the endpoint is accessible
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 1000)
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${lockData.port}/wait-for-auth?poll=false`, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
clearTimeout(timeout)
|
||||
return response.status === 200 || response.status === 202
|
||||
} catch (error) {
|
||||
log(`Error connecting to auth server: ${(error as Error).message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for authentication from another server instance
|
||||
* @param port The port to connect to
|
||||
* @returns The auth code if successful, false otherwise
|
||||
*/
|
||||
export async function waitForAuthentication(port: number): Promise<string | false> {
|
||||
log(`Waiting for authentication from the server on port ${port}...`)
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const url = `http://127.0.0.1:${port}/wait-for-auth`
|
||||
log(`Querying: ${url}`)
|
||||
const response = await fetch(url)
|
||||
|
||||
if (response.status === 200) {
|
||||
const code = await response.text()
|
||||
log(`Received code: ${code}`)
|
||||
return code // Return the auth code
|
||||
} else if (response.status === 202) {
|
||||
// do nothing, loop
|
||||
} else {
|
||||
log(`Unexpected response status: ${response.status}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Error waiting for authentication: ${(error as Error).message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinates authentication between multiple instances of the client/proxy
|
||||
* @param serverUrlHash The hash of the server URL
|
||||
* @param callbackPort The port to use for the callback server
|
||||
* @param events The event emitter to use for signaling
|
||||
* @returns An object with the server, waitForAuthCode function, and a flag indicating if browser auth can be skipped
|
||||
*/
|
||||
export async function coordinateAuth(
|
||||
serverUrlHash: string,
|
||||
callbackPort: number,
|
||||
events: EventEmitter,
|
||||
): Promise<{ server: Server; waitForAuthCode: () => Promise<string>; skipBrowserAuth: boolean; authCode?: string }> {
|
||||
// Check for a lockfile
|
||||
const lockData = await checkLockfile(serverUrlHash)
|
||||
|
||||
// If there's a valid lockfile, try to use the existing auth process
|
||||
if (lockData && (await isLockValid(lockData))) {
|
||||
log(`Another instance is handling authentication on port ${lockData.port}`)
|
||||
|
||||
try {
|
||||
// Try to wait for the authentication to complete
|
||||
const code = await waitForAuthentication(lockData.port)
|
||||
if (code) {
|
||||
log('Authentication completed by another instance')
|
||||
|
||||
// Setup a dummy server and return a pre-resolved promise for the auth code
|
||||
const dummyServer = express().listen(0) // Listen on any available port
|
||||
const dummyWaitForAuthCode = () => Promise.resolve(code)
|
||||
|
||||
return {
|
||||
server: dummyServer,
|
||||
waitForAuthCode: dummyWaitForAuthCode,
|
||||
skipBrowserAuth: true,
|
||||
authCode: code,
|
||||
}
|
||||
} else {
|
||||
log('Taking over authentication process...')
|
||||
}
|
||||
} catch (error) {
|
||||
log(`Error waiting for authentication: ${error}`)
|
||||
}
|
||||
|
||||
// If we get here, the other process didn't complete auth successfully
|
||||
await deleteLockfile(serverUrlHash)
|
||||
} else if (lockData) {
|
||||
// Invalid lockfile, delete its
|
||||
log('Found invalid lockfile, deleting it')
|
||||
await deleteLockfile(serverUrlHash)
|
||||
}
|
||||
|
||||
// Create our own lockfile
|
||||
const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll({
|
||||
port: callbackPort,
|
||||
path: '/oauth/callback',
|
||||
events,
|
||||
})
|
||||
|
||||
// Get the actual port the server is running on
|
||||
const address = server.address() as AddressInfo
|
||||
const actualPort = address.port
|
||||
|
||||
log(`Creating lockfile for server ${serverUrlHash} with process ${process.pid} on port ${actualPort}`)
|
||||
await createLockfile(serverUrlHash, process.pid, actualPort)
|
||||
|
||||
// Make sure lockfile is deleted on process exit
|
||||
const cleanupHandler = async () => {
|
||||
try {
|
||||
log(`Cleaning up lockfile for server ${serverUrlHash}`)
|
||||
await deleteLockfile(serverUrlHash)
|
||||
} catch (error) {
|
||||
log(`Error cleaning up lockfile: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
process.once('exit', () => {
|
||||
try {
|
||||
// Synchronous version for 'exit' event since we can't use async here
|
||||
const configPath = getConfigFilePath(serverUrlHash, 'lock.json')
|
||||
require('fs').unlinkSync(configPath)
|
||||
} catch {}
|
||||
})
|
||||
|
||||
// Also handle SIGINT separately
|
||||
process.once('SIGINT', async () => {
|
||||
await cleanupHandler()
|
||||
})
|
||||
|
||||
return {
|
||||
server,
|
||||
waitForAuthCode,
|
||||
skipBrowserAuth: false,
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue