mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-05 16:13:08 +08:00
fix: improve error handling for workspace switch and support --host 0.0.0.0
- Add safeParseJson() helper with content-type validation and proxy error detection - Allow token acquisition from any interface when server binds to 0.0.0.0 or :: - Provide clear error messages when proxy intercepts localhost requests
This commit is contained in:
@@ -17,6 +17,49 @@ interface SwitchWorkspaceResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse JSON response with content-type validation
|
||||
* Provides better error messages when response is not JSON (e.g., proxy errors)
|
||||
*/
|
||||
async function safeParseJson<T>(response: Response, endpoint: string): Promise<T> {
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
// Check if response is JSON
|
||||
if (!contentType?.includes('application/json')) {
|
||||
// Get response text for error message (truncated to avoid huge output)
|
||||
const text = await response.text();
|
||||
const preview = text.substring(0, 200);
|
||||
|
||||
// Detect common proxy errors
|
||||
if (text.includes('APIKEY') || text.includes('api key') || text.includes('apiKey')) {
|
||||
throw new Error(
|
||||
`Request to ${endpoint} was intercepted by a proxy requiring API key. ` +
|
||||
`Check HTTP_PROXY/HTTPS_PROXY environment variables. ` +
|
||||
`Response: ${preview}`
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected response from ${endpoint} (expected JSON, got: ${contentType || 'unknown'}). ` +
|
||||
`This may indicate a proxy or network issue. Response: ${preview}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check for HTTP errors
|
||||
if (!response.ok) {
|
||||
let errorMessage = response.statusText;
|
||||
try {
|
||||
const body = await response.json() as { error?: string; message?: string };
|
||||
errorMessage = body.error || body.message || response.statusText;
|
||||
} catch {
|
||||
// Ignore JSON parse errors for error response
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if server is already running on the specified port
|
||||
* @param {number} port - Port to check
|
||||
@@ -47,15 +90,20 @@ async function isServerRunning(port: number): Promise<boolean> {
|
||||
*/
|
||||
async function switchWorkspace(port: number, path: string): Promise<SwitchWorkspaceResult> {
|
||||
try {
|
||||
// Get auth token with content-type validation
|
||||
const tokenResponse = await fetch(`http://localhost:${port}/api/auth/token`);
|
||||
const tokenData = await tokenResponse.json() as { token?: string };
|
||||
const tokenData = await safeParseJson<{ token?: string; expiresAt?: string }>(
|
||||
tokenResponse,
|
||||
'/api/auth/token'
|
||||
);
|
||||
const token = tokenData.token;
|
||||
|
||||
// Switch workspace with content-type validation
|
||||
const response = await fetch(
|
||||
`http://localhost:${port}/api/switch-path?path=${encodeURIComponent(path)}`,
|
||||
token ? { headers: { Authorization: `Bearer ${token}` } } : undefined
|
||||
);
|
||||
return await response.json() as SwitchWorkspaceResult;
|
||||
return safeParseJson<SwitchWorkspaceResult>(response, '/api/switch-path');
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
return { success: false, error: error.message };
|
||||
|
||||
@@ -47,11 +47,31 @@ export function extractAuthToken(req: IncomingMessage): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isLocalhostRequest(req: IncomingMessage): boolean {
|
||||
/**
|
||||
* Check if request is from localhost
|
||||
* @param req - The incoming request
|
||||
* @param allowAllInterfaces - When true (server bound to 0.0.0.0), allows requests from any interface
|
||||
*/
|
||||
export function isLocalhostRequest(req: IncomingMessage, allowAllInterfaces: boolean = false): boolean {
|
||||
// When server is bound to 0.0.0.0, allow token acquisition from any interface
|
||||
// This is safe because the token endpoint only provides auth for the dashboard
|
||||
if (allowAllInterfaces) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const remote = req.socket?.remoteAddress ?? '';
|
||||
return remote === '127.0.0.1' || remote === '::1' || remote === '::ffff:127.0.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if host binding allows all network interfaces
|
||||
* @param host - The host address the server is bound to
|
||||
*/
|
||||
export function isWildcardHost(host: string | undefined): boolean {
|
||||
if (!host) return false;
|
||||
return host === '0.0.0.0' || host === '::' || host === '';
|
||||
}
|
||||
|
||||
export function setAuthCookie(res: ServerResponse, token: string, expiresAt: Date): void {
|
||||
const maxAgeSeconds = Math.max(0, Math.floor((expiresAt.getTime() - Date.now()) / 1000));
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ import { handleSpecRoutes } from './routes/spec-routes.js';
|
||||
import { handleWebSocketUpgrade, broadcastToClients, extractSessionIdFromPath } from './websocket.js';
|
||||
|
||||
import { getTokenManager } from './auth/token-manager.js';
|
||||
import { authMiddleware, isLocalhostRequest, setAuthCookie } from './auth/middleware.js';
|
||||
import { authMiddleware, isLocalhostRequest, isWildcardHost, setAuthCookie } from './auth/middleware.js';
|
||||
import { getCorsOrigin } from './cors.js';
|
||||
import { csrfValidation } from './auth/csrf-middleware.js';
|
||||
import { getCsrfTokenManager } from './auth/csrf-manager.js';
|
||||
@@ -418,9 +418,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
server
|
||||
};
|
||||
|
||||
// Token acquisition endpoint (localhost-only)
|
||||
// Token acquisition endpoint (localhost-only, or any interface when bound to 0.0.0.0)
|
||||
if (pathname === '/api/auth/token') {
|
||||
if (!isLocalhostRequest(req)) {
|
||||
// Allow from any interface when server is bound to 0.0.0.0 or ::
|
||||
const allowAllInterfaces = isWildcardHost(host);
|
||||
if (!isLocalhostRequest(req, allowAllInterfaces)) {
|
||||
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ error: 'Forbidden' }));
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user