mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: sanitize URLs to prevent proxy corruption in TUN mode
- Add URL sanitization logic to handle corrupted URLs from proxy tools like NekoBox - Support both production and development RooCode domains with subdomains - Preserve paths, query parameters, and fragments when fixing corrupted URLs - Add comprehensive test coverage for URL sanitization scenarios Fixes #9441
This commit is contained in:
parent
f7d6daedff
commit
f87ca7fb38
2 changed files with 248 additions and 2 deletions
178
packages/cloud/src/__tests__/config.spec.ts
Normal file
178
packages/cloud/src/__tests__/config.spec.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
|
||||
import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL, PRODUCTION_ROO_CODE_API_URL } from "../config.js"
|
||||
|
||||
describe("config", () => {
|
||||
let originalEnv: NodeJS.ProcessEnv
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original environment
|
||||
originalEnv = { ...process.env }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("getClerkBaseUrl", () => {
|
||||
it("should return production URL when environment variable is not set", () => {
|
||||
delete process.env.CLERK_BASE_URL
|
||||
expect(getClerkBaseUrl()).toBe(PRODUCTION_CLERK_BASE_URL)
|
||||
})
|
||||
|
||||
it("should return valid custom URL from environment variable", () => {
|
||||
process.env.CLERK_BASE_URL = "https://custom.clerk.com"
|
||||
expect(getClerkBaseUrl()).toBe("https://custom.clerk.com")
|
||||
})
|
||||
|
||||
it("should sanitize corrupted URL with proxy prefix (NekoBox issue)", () => {
|
||||
// This is the exact issue reported: proxy adds its address before the actual URL
|
||||
process.env.CLERK_BASE_URL = "http://127.0.0.1:2080clerk.roocode.com:443"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com")
|
||||
})
|
||||
|
||||
it("should sanitize corrupted URL with different proxy address", () => {
|
||||
process.env.CLERK_BASE_URL = "http://192.168.1.1:8080clerk.roocode.com:443"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com")
|
||||
})
|
||||
|
||||
it("should preserve path in corrupted URL", () => {
|
||||
process.env.CLERK_BASE_URL = "http://127.0.0.1:2080clerk.roocode.com:443/api/v1"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com/api/v1")
|
||||
})
|
||||
|
||||
it("should handle URL with clerk.roocode.com embedded anywhere", () => {
|
||||
process.env.CLERK_BASE_URL = "garbage-text-clerk.roocode.com"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com")
|
||||
})
|
||||
|
||||
it("should return fallback for completely invalid URL", () => {
|
||||
process.env.CLERK_BASE_URL = "not-a-url-at-all"
|
||||
expect(getClerkBaseUrl()).toBe(PRODUCTION_CLERK_BASE_URL)
|
||||
})
|
||||
|
||||
it("should handle empty string", () => {
|
||||
process.env.CLERK_BASE_URL = ""
|
||||
expect(getClerkBaseUrl()).toBe(PRODUCTION_CLERK_BASE_URL)
|
||||
})
|
||||
|
||||
it("should preserve valid dev URL", () => {
|
||||
process.env.CLERK_BASE_URL = "https://dev.clerk.roocode.com"
|
||||
expect(getClerkBaseUrl()).toBe("https://dev.clerk.roocode.com")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRooCodeApiUrl", () => {
|
||||
it("should return production URL when environment variable is not set", () => {
|
||||
delete process.env.ROO_CODE_API_URL
|
||||
expect(getRooCodeApiUrl()).toBe(PRODUCTION_ROO_CODE_API_URL)
|
||||
})
|
||||
|
||||
it("should return valid custom URL from environment variable", () => {
|
||||
process.env.ROO_CODE_API_URL = "https://custom.api.com"
|
||||
expect(getRooCodeApiUrl()).toBe("https://custom.api.com")
|
||||
})
|
||||
|
||||
it("should sanitize corrupted URL with proxy prefix (NekoBox issue)", () => {
|
||||
// This is the exact issue reported: proxy adds its address before the actual URL
|
||||
process.env.ROO_CODE_API_URL = "http://127.0.0.1:2080app.roocode.com:443"
|
||||
expect(getRooCodeApiUrl()).toBe("https://app.roocode.com")
|
||||
})
|
||||
|
||||
it("should sanitize corrupted URL with different proxy address", () => {
|
||||
process.env.ROO_CODE_API_URL = "http://192.168.1.1:8080app.roocode.com:443"
|
||||
expect(getRooCodeApiUrl()).toBe("https://app.roocode.com")
|
||||
})
|
||||
|
||||
it("should preserve path in corrupted URL", () => {
|
||||
process.env.ROO_CODE_API_URL = "http://127.0.0.1:2080app.roocode.com:443/api/v1"
|
||||
expect(getRooCodeApiUrl()).toBe("https://app.roocode.com/api/v1")
|
||||
})
|
||||
|
||||
it("should handle URL with app.roocode.com embedded anywhere", () => {
|
||||
process.env.ROO_CODE_API_URL = "garbage-text-app.roocode.com"
|
||||
expect(getRooCodeApiUrl()).toBe("https://app.roocode.com")
|
||||
})
|
||||
|
||||
it("should handle api.roocode.com domain", () => {
|
||||
process.env.ROO_CODE_API_URL = "http://127.0.0.1:2080api.roocode.com:443"
|
||||
expect(getRooCodeApiUrl()).toBe("https://api.roocode.com")
|
||||
})
|
||||
|
||||
it("should return fallback for completely invalid URL", () => {
|
||||
process.env.ROO_CODE_API_URL = "not-a-url-at-all"
|
||||
expect(getRooCodeApiUrl()).toBe(PRODUCTION_ROO_CODE_API_URL)
|
||||
})
|
||||
|
||||
it("should handle empty string", () => {
|
||||
process.env.ROO_CODE_API_URL = ""
|
||||
expect(getRooCodeApiUrl()).toBe(PRODUCTION_ROO_CODE_API_URL)
|
||||
})
|
||||
|
||||
it("should preserve valid dev URL", () => {
|
||||
process.env.ROO_CODE_API_URL = "https://dev.app.roocode.com"
|
||||
expect(getRooCodeApiUrl()).toBe("https://dev.app.roocode.com")
|
||||
})
|
||||
|
||||
it("should handle localhost URLs correctly", () => {
|
||||
process.env.ROO_CODE_API_URL = "http://localhost:3000"
|
||||
expect(getRooCodeApiUrl()).toBe("http://localhost:3000")
|
||||
})
|
||||
|
||||
it("should handle local IP without proxy corruption", () => {
|
||||
// User reported their local endpoint also gets corrupted
|
||||
process.env.ROO_CODE_API_URL = "http://192.168.1.102:8000"
|
||||
expect(getRooCodeApiUrl()).toBe("http://192.168.1.102:8000")
|
||||
})
|
||||
|
||||
it("should fix corrupted local IP with proxy prefix", () => {
|
||||
// Simulating what might happen if proxy corrupts local IP
|
||||
process.env.ROO_CODE_API_URL = "http://127.0.0.1:2080192.168.1.102:8000"
|
||||
// Since this doesn't contain a known domain, it should fall back
|
||||
expect(getRooCodeApiUrl()).toBe(PRODUCTION_ROO_CODE_API_URL)
|
||||
})
|
||||
})
|
||||
|
||||
describe("URL sanitization edge cases", () => {
|
||||
it("should handle HTTP vs HTTPS detection based on port", () => {
|
||||
// Port 443 should imply HTTPS
|
||||
process.env.CLERK_BASE_URL = "proxygarbage-clerk.roocode.com:443"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com")
|
||||
|
||||
// No port 443 but fallback uses https
|
||||
process.env.CLERK_BASE_URL = "proxygarbage-clerk.roocode.com"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com")
|
||||
})
|
||||
|
||||
it("should handle multiple corruptions in the same URL", () => {
|
||||
// Multiple instances of corruption
|
||||
process.env.CLERK_BASE_URL = "http://127.0.0.1:2080http://proxy:8888clerk.roocode.com:443"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com")
|
||||
})
|
||||
|
||||
it("should not be fooled by domain-like strings in paths", () => {
|
||||
// Valid URL that happens to contain the domain string elsewhere
|
||||
process.env.CLERK_BASE_URL = "https://valid.com/path/clerk.roocode.com/test"
|
||||
expect(getClerkBaseUrl()).toBe("https://valid.com/path/clerk.roocode.com/test")
|
||||
})
|
||||
|
||||
it("should handle undefined environment variables", () => {
|
||||
process.env.CLERK_BASE_URL = undefined
|
||||
expect(getClerkBaseUrl()).toBe(PRODUCTION_CLERK_BASE_URL)
|
||||
|
||||
process.env.ROO_CODE_API_URL = undefined
|
||||
expect(getRooCodeApiUrl()).toBe(PRODUCTION_ROO_CODE_API_URL)
|
||||
})
|
||||
|
||||
it("should handle URLs with query parameters", () => {
|
||||
process.env.CLERK_BASE_URL = "http://127.0.0.1:2080clerk.roocode.com:443?param=value"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com?param=value")
|
||||
})
|
||||
|
||||
it("should handle URLs with fragments", () => {
|
||||
process.env.CLERK_BASE_URL = "http://127.0.0.1:2080clerk.roocode.com:443#fragment"
|
||||
expect(getClerkBaseUrl()).toBe("https://clerk.roocode.com#fragment")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,74 @@
|
|||
export const PRODUCTION_CLERK_BASE_URL = "https://clerk.roocode.com"
|
||||
export const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com"
|
||||
|
||||
export const getClerkBaseUrl = () => process.env.CLERK_BASE_URL || PRODUCTION_CLERK_BASE_URL
|
||||
/**
|
||||
* Sanitizes a URL by removing any proxy prefixes that may have been incorrectly added
|
||||
* by proxy tools like NekoBox in TUN mode.
|
||||
*
|
||||
* @param url - The URL to sanitize
|
||||
* @param fallback - The fallback URL to use if sanitization fails
|
||||
* @returns The sanitized URL
|
||||
*/
|
||||
function sanitizeUrl(url: string | undefined, fallback: string): string {
|
||||
if (!url) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
export const getRooCodeApiUrl = () => process.env.ROO_CODE_API_URL || PRODUCTION_ROO_CODE_API_URL
|
||||
try {
|
||||
// First, try to parse as a valid URL
|
||||
const parsedUrl = new URL(url)
|
||||
|
||||
// Check if it's already a valid RooCode URL
|
||||
if (
|
||||
parsedUrl.hostname.endsWith(".roocode.com") ||
|
||||
parsedUrl.hostname === "clerk.roocode.com" ||
|
||||
parsedUrl.hostname === "app.roocode.com" ||
|
||||
parsedUrl.hostname === "api.roocode.com"
|
||||
) {
|
||||
return url // URL is already valid
|
||||
}
|
||||
|
||||
// If it parses successfully and looks reasonable (not a RooCode domain), use it
|
||||
if (parsedUrl.protocol && parsedUrl.hostname) {
|
||||
return url
|
||||
}
|
||||
} catch (_error) {
|
||||
// URL parsing failed, try to fix corrupted URL
|
||||
}
|
||||
|
||||
// Check if the URL contains known RooCode domains (for corrupted URLs)
|
||||
const rooCodePatterns = [
|
||||
{ pattern: /(dev\.|staging\.|test\.)?clerk\.roocode\.com/, domain: "clerk.roocode.com" },
|
||||
{ pattern: /(dev\.|staging\.|test\.)?app\.roocode\.com/, domain: "app.roocode.com" },
|
||||
{ pattern: /(dev\.|staging\.|test\.)?api\.roocode\.com/, domain: "api.roocode.com" },
|
||||
]
|
||||
|
||||
for (const { pattern } of rooCodePatterns) {
|
||||
const match = url.match(pattern)
|
||||
if (match) {
|
||||
// Extract the full matched domain (including subdomain if present)
|
||||
const fullDomain = match[0]
|
||||
const domainIndex = url.indexOf(fullDomain)
|
||||
|
||||
if (domainIndex !== -1) {
|
||||
// The URL is corrupted, reconstruct it
|
||||
const protocol = url.includes(":443") || fallback.startsWith("https://") ? "https://" : "http://"
|
||||
|
||||
// Check for path, query, and fragment after the domain
|
||||
const afterDomain = url.substring(domainIndex + fullDomain.length)
|
||||
|
||||
// Match optional port, then path/query/fragment
|
||||
const afterMatch = afterDomain.match(/^(?::\d+)?(.*)$/)
|
||||
const pathQueryFragment = afterMatch?.[1] || ""
|
||||
|
||||
return protocol + fullDomain + pathQueryFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
export const getClerkBaseUrl = () => sanitizeUrl(process.env.CLERK_BASE_URL, PRODUCTION_CLERK_BASE_URL)
|
||||
|
||||
export const getRooCodeApiUrl = () => sanitizeUrl(process.env.ROO_CODE_API_URL, PRODUCTION_ROO_CODE_API_URL)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue