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:
58
README.md
58
README.md
@@ -427,6 +427,64 @@ npm run test:strategies # Account selection strategies
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Windows: OAuth Port Error (EACCES)
|
||||||
|
|
||||||
|
On Windows, the default OAuth callback port (51121) may be reserved by Hyper-V, WSL2, or Docker. If you see:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: listen EACCES: permission denied 0.0.0.0:51121
|
||||||
|
```
|
||||||
|
|
||||||
|
The proxy will automatically try fallback ports (51122-51126). If all ports fail, try these solutions:
|
||||||
|
|
||||||
|
#### Option 1: Use a Custom Port (Recommended)
|
||||||
|
|
||||||
|
Set a custom port outside the reserved range:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows PowerShell
|
||||||
|
$env:OAUTH_CALLBACK_PORT = "3456"
|
||||||
|
antigravity-claude-proxy start
|
||||||
|
|
||||||
|
# Windows CMD
|
||||||
|
set OAUTH_CALLBACK_PORT=3456
|
||||||
|
antigravity-claude-proxy start
|
||||||
|
|
||||||
|
# Or add to your .env file
|
||||||
|
OAUTH_CALLBACK_PORT=3456
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 2: Reset Windows NAT
|
||||||
|
|
||||||
|
Run as Administrator:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
net stop winnat
|
||||||
|
net start winnat
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 3: Check Reserved Ports
|
||||||
|
|
||||||
|
See which ports are reserved:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
netsh interface ipv4 show excludedportrange protocol=tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
If 51121 is in a reserved range, use Option 1 with a port outside those ranges.
|
||||||
|
|
||||||
|
#### Option 4: Permanently Exclude Port (Admin)
|
||||||
|
|
||||||
|
Reserve the port before Hyper-V claims it (run as Administrator):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
netsh int ipv4 add excludedportrange protocol=tcp startport=51121 numberofports=1
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** The server automatically tries fallback ports (51122-51126) if the primary port fails.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### "Could not extract token from Antigravity"
|
### "Could not extract token from Antigravity"
|
||||||
|
|
||||||
If using single-account mode with Antigravity:
|
If using single-account mode with Antigravity:
|
||||||
|
|||||||
@@ -36,6 +36,9 @@
|
|||||||
"maxAccounts": 10,
|
"maxAccounts": 10,
|
||||||
"_maxAccounts_comment": "Maximum number of Google accounts allowed (1-100). Default: 10.",
|
"_maxAccounts_comment": "Maximum number of Google accounts allowed (1-100). Default: 10.",
|
||||||
|
|
||||||
|
"_oauthCallbackPort_comment": "OAuth callback server port. Change if you get EACCES errors on Windows. Can also use OAUTH_CALLBACK_PORT env var. Default: 51121.",
|
||||||
|
"_oauthCallbackPort_env": "OAUTH_CALLBACK_PORT=3456",
|
||||||
|
|
||||||
"_profiles": {
|
"_profiles": {
|
||||||
"development": {
|
"development": {
|
||||||
"debug": true,
|
"debug": true,
|
||||||
|
|||||||
@@ -137,22 +137,50 @@ export function extractCodeFromInput(input) {
|
|||||||
return { code: trimmed, state: null };
|
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
|
* 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
|
* Returns an object with a promise and an abort function
|
||||||
*
|
*
|
||||||
* @param {string} expectedState - Expected state parameter for CSRF protection
|
* @param {string} expectedState - Expected state parameter for CSRF protection
|
||||||
* @param {number} timeoutMs - Timeout in milliseconds (default 120000)
|
* @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) {
|
export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
||||||
let server = null;
|
let server = null;
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
let isAborted = false;
|
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) => {
|
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') {
|
if (url.pathname !== '/oauth-callback') {
|
||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
@@ -232,17 +260,60 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
|||||||
resolve(code);
|
resolve(code);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('error', (err) => {
|
// Try ports with fallback logic (issue #176 - Windows EACCES fix)
|
||||||
if (err.code === 'EADDRINUSE') {
|
let boundSuccessfully = false;
|
||||||
reject(new Error(`Port ${OAUTH_CONFIG.callbackPort} is already in use. Close any other OAuth flows and try again.`));
|
for (const port of portsToTry) {
|
||||||
} else {
|
try {
|
||||||
reject(err);
|
await tryBindPort(server, port);
|
||||||
}
|
actualPort = port;
|
||||||
});
|
boundSuccessfully = true;
|
||||||
|
|
||||||
server.listen(OAUTH_CONFIG.callbackPort, () => {
|
if (port !== OAUTH_CONFIG.callbackPort) {
|
||||||
logger.info(`[OAuth] Callback server listening on 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
|
// Timeout after specified duration
|
||||||
timeoutId = setTimeout(() => {
|
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)
|
// 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 = {
|
export const OAUTH_CONFIG = {
|
||||||
clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
|
clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
|
||||||
clientSecret: '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',
|
clientSecret: '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',
|
||||||
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||||
tokenUrl: 'https://oauth2.googleapis.com/token',
|
tokenUrl: 'https://oauth2.googleapis.com/token',
|
||||||
userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo',
|
userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo',
|
||||||
callbackPort: 51121,
|
callbackPort: OAUTH_CALLBACK_PORT,
|
||||||
|
callbackFallbackPorts: OAUTH_CALLBACK_FALLBACK_PORTS,
|
||||||
scopes: [
|
scopes: [
|
||||||
'https://www.googleapis.com/auth/cloud-platform',
|
'https://www.googleapis.com/auth/cloud-platform',
|
||||||
'https://www.googleapis.com/auth/userinfo.email',
|
'https://www.googleapis.com/auth/userinfo.email',
|
||||||
|
|||||||
Reference in New Issue
Block a user