Merge pull request #187 from quocthai0404/fix/issue-176-windows-callback-port
fix: Make OAuth callback port configurable for Windows compatibility (#176)
This commit is contained in:
58
README.md
58
README.md
@@ -435,6 +435,64 @@ npm run test:cache-control # Cache control field stripping
|
||||
|
||||
## 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"
|
||||
|
||||
If using single-account mode with Antigravity:
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"maxAccounts": 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": {
|
||||
"development": {
|
||||
"debug": true,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -188,13 +188,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