fix: make OAuth callback port configurable for Windows compatibility (#176)
- Add OAUTH_CALLBACK_PORT environment variable (default: 51121) - Implement automatic port fallback (51122-51126) on EACCES/EADDRINUSE - Add Windows-specific troubleshooting in error messages and README - Document configuration in config.example.json Closes #176
This commit is contained in:
@@ -137,22 +137,50 @@ export function extractCodeFromInput(input) {
|
||||
return { code: trimmed, state: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to bind server to a specific port
|
||||
* @param {http.Server} server - HTTP server instance
|
||||
* @param {number} port - Port to bind to
|
||||
* @returns {Promise<number>} Resolves with port on success, rejects on error
|
||||
*/
|
||||
function tryBindPort(server, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onError = (err) => {
|
||||
server.removeListener('listening', onSuccess);
|
||||
reject(err);
|
||||
};
|
||||
const onSuccess = () => {
|
||||
server.removeListener('error', onError);
|
||||
resolve(port);
|
||||
};
|
||||
server.once('error', onError);
|
||||
server.once('listening', onSuccess);
|
||||
server.listen(port);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a local server to receive the OAuth callback
|
||||
* Implements automatic port fallback for Windows compatibility (issue #176)
|
||||
* Returns an object with a promise and an abort function
|
||||
*
|
||||
* @param {string} expectedState - Expected state parameter for CSRF protection
|
||||
* @param {number} timeoutMs - Timeout in milliseconds (default 120000)
|
||||
* @returns {{promise: Promise<string>, abort: Function}} Object with promise and abort function
|
||||
* @returns {{promise: Promise<string>, abort: Function, getPort: Function}} Object with promise, abort, and getPort functions
|
||||
*/
|
||||
export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
||||
let server = null;
|
||||
let timeoutId = null;
|
||||
let isAborted = false;
|
||||
let actualPort = OAUTH_CONFIG.callbackPort;
|
||||
|
||||
const promise = new Promise(async (resolve, reject) => {
|
||||
// Build list of ports to try: primary + fallbacks
|
||||
const portsToTry = [OAUTH_CONFIG.callbackPort, ...(OAUTH_CONFIG.callbackFallbackPorts || [])];
|
||||
const errors = [];
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, `http://localhost:${OAUTH_CONFIG.callbackPort}`);
|
||||
const url = new URL(req.url, `http://localhost:${actualPort}`);
|
||||
|
||||
if (url.pathname !== '/oauth-callback') {
|
||||
res.writeHead(404);
|
||||
@@ -232,17 +260,60 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
||||
resolve(code);
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
reject(new Error(`Port ${OAUTH_CONFIG.callbackPort} is already in use. Close any other OAuth flows and try again.`));
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
// Try ports with fallback logic (issue #176 - Windows EACCES fix)
|
||||
let boundSuccessfully = false;
|
||||
for (const port of portsToTry) {
|
||||
try {
|
||||
await tryBindPort(server, port);
|
||||
actualPort = port;
|
||||
boundSuccessfully = true;
|
||||
|
||||
server.listen(OAUTH_CONFIG.callbackPort, () => {
|
||||
logger.info(`[OAuth] Callback server listening on port ${OAUTH_CONFIG.callbackPort}`);
|
||||
});
|
||||
if (port !== OAUTH_CONFIG.callbackPort) {
|
||||
logger.warn(`[OAuth] Primary port ${OAUTH_CONFIG.callbackPort} unavailable, using fallback port ${port}`);
|
||||
} else {
|
||||
logger.info(`[OAuth] Callback server listening on port ${port}`);
|
||||
}
|
||||
break;
|
||||
} catch (err) {
|
||||
const errMsg = err.code === 'EACCES'
|
||||
? `Permission denied on port ${port}`
|
||||
: err.code === 'EADDRINUSE'
|
||||
? `Port ${port} already in use`
|
||||
: `Failed to bind port ${port}: ${err.message}`;
|
||||
errors.push(errMsg);
|
||||
logger.warn(`[OAuth] ${errMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!boundSuccessfully) {
|
||||
// All ports failed - provide helpful error message
|
||||
const isWindows = process.platform === 'win32';
|
||||
let errorMsg = `Failed to start OAuth callback server.\nTried ports: ${portsToTry.join(', ')}\n\nErrors:\n${errors.join('\n')}`;
|
||||
|
||||
if (isWindows) {
|
||||
errorMsg += `\n
|
||||
================== WINDOWS TROUBLESHOOTING ==================
|
||||
The default port range may be reserved by Hyper-V/WSL2/Docker.
|
||||
|
||||
Option 1: Use a custom port
|
||||
Set OAUTH_CALLBACK_PORT=3456 in your environment or .env file
|
||||
|
||||
Option 2: Reset Windows NAT (run as Administrator)
|
||||
net stop winnat && net start winnat
|
||||
|
||||
Option 3: Check reserved port ranges
|
||||
netsh interface ipv4 show excludedportrange protocol=tcp
|
||||
|
||||
Option 4: Exclude port from reservation (run as Administrator)
|
||||
netsh int ipv4 add excludedportrange protocol=tcp startport=51121 numberofports=1
|
||||
==============================================================`;
|
||||
} else {
|
||||
errorMsg += `\n\nTry setting a custom port: OAUTH_CALLBACK_PORT=3456`;
|
||||
}
|
||||
|
||||
reject(new Error(errorMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeout after specified duration
|
||||
timeoutId = setTimeout(() => {
|
||||
@@ -266,7 +337,10 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
||||
}
|
||||
};
|
||||
|
||||
return { promise, abort };
|
||||
// Get actual port (useful when fallback is used)
|
||||
const getPort = () => actualPort;
|
||||
|
||||
return { promise, abort, getPort };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -171,13 +171,19 @@ export function isThinkingModel(modelName) {
|
||||
}
|
||||
|
||||
// Google OAuth configuration (from opencode-antigravity-auth)
|
||||
// OAuth callback port - configurable via environment variable for Windows compatibility (issue #176)
|
||||
// Windows may reserve ports in range 49152-65535 for Hyper-V/WSL2/Docker, causing EACCES errors
|
||||
const OAUTH_CALLBACK_PORT = parseInt(process.env.OAUTH_CALLBACK_PORT || '51121', 10);
|
||||
const OAUTH_CALLBACK_FALLBACK_PORTS = [51122, 51123, 51124, 51125, 51126];
|
||||
|
||||
export const OAUTH_CONFIG = {
|
||||
clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
|
||||
clientSecret: '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',
|
||||
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
tokenUrl: 'https://oauth2.googleapis.com/token',
|
||||
userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo',
|
||||
callbackPort: 51121,
|
||||
callbackPort: OAUTH_CALLBACK_PORT,
|
||||
callbackFallbackPorts: OAUTH_CALLBACK_FALLBACK_PORTS,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
|
||||
Reference in New Issue
Block a user