Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ CODEIUM_API_URL=https://server.self-serve.windsurf.com
DEFAULT_MODEL=claude-4.5-sonnet-thinking
MAX_TOKENS=8192
LOG_LEVEL=info

# ========== Security ==========
# Allow private/internal hosts (e.g., 192.168.x.x, 10.x.x.x, localhost) in proxy tests.
# Set to 1 for local deployments where you need to test proxies on private networks.
# Leave empty or set to 0 for public-facing deployments (default: only public hosts allowed).
ALLOW_PRIVATE_PROXY_HOSTS=
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ In your client's settings for **Custom OpenAI Compatible**:
| `LS_PORT` | `42100` | LS gRPC port. |
| `LS_DATA_DIR` | `/opt/windsurf` | Per-proxy LS data directory root. |
| `DASHBOARD_PASSWORD` | empty | Dashboard password. Leave empty for no password. |
| `ALLOW_PRIVATE_PROXY_HOSTS` | empty | Set to `1` to allow private/internal IPs (e.g., `192.168.x.x`, `10.x.x.x`) in proxy tests and login. Leave empty to only allow public addresses (default). |
| `CASCADE_REUSE_STRICT` | `0` | Set to `1` for strict conversation reuse mode (waits for same fingerprint). |
| `CASCADE_REUSE_STRICT_RETRY_MS` | `60000` | Retry delay in ms for strict reuse mode. |
| `CASCADE_REUSE_HASH_SYSTEM` | `0` | Set to `1` to include system messages in conversation reuse hash. |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ curl http://localhost:3003/v1/messages \
| `LS_BINARY_PATH` | `/opt/windsurf/language_server_linux_x64` | LS 二进制位置 |
| `LS_PORT` | `42100` | LS gRPC 端口 |
| `DASHBOARD_PASSWORD` | 空 | 后台密码 留空不设密码 |
| `ALLOW_PRIVATE_PROXY_HOSTS` | 空 | 设为 `1` 允许在代理测试和登录时使用内网 IP(如 `192.168.x.x`、`10.x.x.x`)。默认留空仅允许公网地址 |

## Dashboard 功能面板

Expand Down
1 change: 1 addition & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ LOG_LEVEL=info
LS_BINARY_PATH=/opt/windsurf/language_server_linux_x64
LS_PORT=42100
DASHBOARD_PASSWORD=
ALLOW_PRIVATE_PROXY_HOSTS=
ENVEOF
echo " Edit .env to set your API_KEY and DASHBOARD_PASSWORD"
else
Expand Down
3 changes: 3 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ export const config = {

// Dashboard
dashboardPassword: process.env.DASHBOARD_PASSWORD || '',

// Proxy testing
allowPrivateProxyHosts: process.env.ALLOW_PRIVATE_PROXY_HOSTS === '1',
};

const levels = { debug: 0, info: 1, warn: 2, error: 3 };
Expand Down
33 changes: 22 additions & 11 deletions src/dashboard/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,28 @@ import { windsurfLogin, refreshFirebaseToken, reRegisterWithCodeium } from './wi
import { getModelAccessConfig, setModelAccessMode, setModelAccessList, addModelToList, removeModelFromList } from './model-access.js';
import { checkMessageRateLimit } from '../windsurf-api.js';
import { assertPublicUrlHost } from '../image.js';
import { validateHostFormat } from '../net-safety.js';

export function parseProxyUrl(proxy) {
const proxyParts = String(proxy).match(/^(?:(\w+):\/\/)?(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/);
if (!proxyParts) return null;
return {
type: proxyParts[1] || 'http',
host: proxyParts[4],
port: parseInt(proxyParts[5]),
username: proxyParts[2] || '',
password: proxyParts[3] || '',
};
}

export function buildBatchProxyBinding(result, proxy) {
const accountId = result?.account?.id || null;
if (!result?.success || !proxy || !accountId) return null;
const proxyParts = String(proxy).match(/^(?:(\w+):\/\/)?(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$/);
if (!proxyParts) return null;
const parsed = parseProxyUrl(proxy);
if (!parsed) return null;
return {
accountId,
proxy: {
type: proxyParts[1] || 'http',
host: proxyParts[4],
port: parseInt(proxyParts[5]),
username: proxyParts[2] || '',
password: proxyParts[3] || '',
},
proxy: parsed,
};
}

Expand Down Expand Up @@ -596,7 +603,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
continue;
}
try {
const loginProxy = proxy ? { host: proxy } : getProxyConfig().global;
const loginProxy = proxy ? parseProxyUrl(proxy) : getProxyConfig().global;
const result = await processWindsurfLogin({ email, password, loginProxy, autoAdd });
const binding = buildBatchProxyBinding(result, proxy);
if (binding) {
Expand Down Expand Up @@ -746,7 +753,11 @@ async function gitStatus() {
}

async function testProxy({ host, port, username, password, type }) {
await assertPublicUrlHost(host);
if (config.allowPrivateProxyHosts) {
await validateHostFormat(host);
} else {
await assertPublicUrlHost(host);
}
const { isSocks, createSocksTunnel } = await import('../socks.js');
const tls = await import('node:tls');
const targetHost = 'api.ipify.org';
Expand Down
12 changes: 12 additions & 0 deletions src/net-safety.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,15 @@ export async function resolvePublicAddresses(hostname, lookupFn = dnsLookup) {
return addrs;
}

export async function validateHostFormat(hostname, lookupFn = dnsLookup) {
const host = String(hostname || '').replace(/^\[|\]$/g, '');
if (!host) throw new Error('ERR_INVALID_HOST');
if (net.isIP(host)) {
return [{ address: host, family: net.isIP(host) }];
}
const result = await new Promise((resolve, reject) => {
lookupFn(host, { all: true }, (err, addrs) => err ? reject(err) : resolve(addrs));
});
return Array.isArray(result) ? result : [result];
}

Loading