diff --git a/src/dashboard/i18n/en.json b/src/dashboard/i18n/en.json index 21d72627..d7ad044e 100644 --- a/src/dashboard/i18n/en.json +++ b/src/dashboard/i18n/en.json @@ -862,6 +862,7 @@ "ERR_IDTOKEN_REQUIRED": "ID token is required", "ERR_PROXY_PRIVATE_IP": "Proxy cannot point to private/local address", "ERR_PROXY_HTTP_ERROR": "Proxy returned HTTP error", + "ERR_PROXY_PRIVATE_HOST": "Private/local host is not allowed", "ERR_CONNECTION_FAILED": "Connection failed", "ERR_TIMEOUT": "Timeout (10s)", "ERR_TLS_TUNNEL_ERROR": "TLS tunnel established but returned abnormal content", diff --git a/src/dashboard/i18n/zh-CN.json b/src/dashboard/i18n/zh-CN.json index 2e966cc5..24f19173 100644 --- a/src/dashboard/i18n/zh-CN.json +++ b/src/dashboard/i18n/zh-CN.json @@ -862,6 +862,7 @@ "ERR_IDTOKEN_REQUIRED": "缺少 idToken", "ERR_PROXY_PRIVATE_IP": "代理地址不能指向内网/本机", "ERR_PROXY_HTTP_ERROR": "代理返回 HTTP 错误", + "ERR_PROXY_PRIVATE_HOST": "内网/本地主机不允许", "ERR_CONNECTION_FAILED": "连接失败", "ERR_TIMEOUT": "超时(10s)", "ERR_TLS_TUNNEL_ERROR": "TLS 隧道建立但返回内容异常", diff --git a/src/dashboard/index.html b/src/dashboard/index.html index 942b256e..20033ade 100644 --- a/src/dashboard/index.html +++ b/src/dashboard/index.html @@ -1521,8 +1521,8 @@ `); return; } + const errKey = r.error ? `error.${r.error}` : null; + const errMsg = (errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : (r.error || I18n.t('error.unknown')); this.showWindsurfLoginResult(`
${I18n.t('loginResult.fail')}
-

${this.esc(r.error || I18n.t('error.unknown'))}

${this.getWindsurfLoginFailActions(r)}
`); +

${this.esc(errMsg)}

${this.getWindsurfLoginFailActions(r)}
`); }, renderBatchWindsurfLoginResult(results, autoAdd) { @@ -2808,7 +2829,7 @@

控制台登录

${r.success ? I18n.t('batch.success') : I18n.t('batch.fail')} ${r.success ? `${this.esc(r.apiKey_masked || (r.apiKey ? r.apiKey.slice(0, 16) + '...' : '-'))}${r.account ? ` · ${I18n.t('table.header.account')} ${this.esc(r.account.id)}` : ''}` - : `${this.esc(r.error || I18n.t('error.unknown'))}`} + : (() => { const errKey = r.error ? `error.${r.error}` : null; return `${this.esc((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : (r.error || I18n.t('error.unknown')))}`; })()} `).join('')} @@ -2880,11 +2901,13 @@

控制台登录

} else { entry.status = 'error: ' + (r.error || 'unknown'); this.renderSingleWindsurfLoginResult(r, autoAdd); - this.toast(r.error || I18n.t('toast.loginFailed'), 'error'); + const errKey = r.error ? `error.${r.error}` : null; + this.toast((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : (r.error || I18n.t('toast.loginFailed')), 'error'); } } catch (err) { entry.status = 'error: ' + err.message; - this.toast(err.message, 'error'); + const errKey = err.message ? `error.${err.message}` : null; + this.toast((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : err.message, 'error'); } this.pushLoginHistory(entry); @@ -2912,12 +2935,14 @@

控制台登录

try { const r = await this.api('POST', '/batch-import', { text: input, autoAdd }); if (!r.success || !Array.isArray(r.results)) { + const errKey = r.error ? `error.${r.error}` : null; this.showWindsurfLoginResult(`
${I18n.t('loginResult.fail')}
-

${this.esc(r.error || I18n.t('error.unknown'))}

`); - return this.toast(r.error || I18n.t('batch.importFailed'), 'error'); +

${this.esc((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : (r.error || I18n.t('error.unknown')))}

`); + const translatedErr = errKey && I18n.t(errKey) !== errKey ? I18n.t(errKey) : r.error; + return this.toast(translatedErr || I18n.t('batch.importFailed'), 'error'); } this.renderBatchWindsurfLoginResult(r.results, autoAdd); @@ -2934,12 +2959,14 @@

控制台登录

document.getElementById('wl-batch-input').value = ''; } } catch (err) { + const errKey = err.message ? `error.${err.message}` : null; this.showWindsurfLoginResult(`
${I18n.t('loginResult.fail')}
-

${this.esc(err.message)}

`); - this.toast(err.message, 'error'); +

${this.esc((errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : err.message)}

`); + const translatedErr = (errKey && I18n.t(errKey) !== errKey) ? I18n.t(errKey) : err.message; + this.toast(translatedErr, 'error'); } finally { btn.disabled = false; btn.innerHTML = ' ' + I18n.t('action.batchImport'); @@ -2956,7 +2983,7 @@

控制台登录

${this.esc(h.email)} ${ok ? I18n.t('batch.success') : I18n.t('batch.fail')} - ${!ok ? `
${this.esc((h.status||'').replace('error: ',''))}
` : ''} + ${!ok ? (() => { const errCode = (h.status||'').replace('error: ',''); const errKey = `error.${errCode}`; const errMsg = I18n.t(errKey) !== errKey ? I18n.t(errKey) : errCode; return `
${this.esc(errMsg)}
`; })() : ''} ${this.esc(h.proxy || '-')} diff --git a/src/net-safety.js b/src/net-safety.js index 3332fc13..4984bc7c 100644 --- a/src/net-safety.js +++ b/src/net-safety.js @@ -88,9 +88,9 @@ export function isPrivateIp(address) { export async function resolvePublicAddresses(hostname, lookupFn = dnsLookup) { const host = String(hostname || '').replace(/^\[|\]$/g, ''); - if (!host || host.toLowerCase() === 'localhost') throw new Error('ERR_PRIVATE_HOST'); + if (!host || host.toLowerCase() === 'localhost') throw new Error('ERR_PROXY_PRIVATE_HOST'); if (net.isIP(host)) { - if (isPrivateIp(host)) throw new Error('ERR_PRIVATE_IP'); + if (isPrivateIp(host)) throw new Error('ERR_PROXY_PRIVATE_IP'); return [{ address: host, family: net.isIP(host) }]; } const result = await new Promise((resolve, reject) => { @@ -98,7 +98,7 @@ export async function resolvePublicAddresses(hostname, lookupFn = dnsLookup) { }); const addrs = Array.isArray(result) ? result : [result]; for (const a of addrs) { - if (isPrivateIp(a.address)) throw new Error('ERR_PRIVATE_IP'); + if (isPrivateIp(a.address)) throw new Error('ERR_PROXY_PRIVATE_IP'); } return addrs; } diff --git a/test/ssrf.test.js b/test/ssrf.test.js index f4b5f8eb..3c87c418 100644 --- a/test/ssrf.test.js +++ b/test/ssrf.test.js @@ -20,7 +20,7 @@ describe('SSRF private address detection', () => { it('rejects hostnames after DNS resolution to private IPs', async () => { const lookup = (host, opts, cb) => cb(null, [{ address: '127.0.0.1', family: 4 }]); - await assert.rejects(() => resolvePublicAddresses('evil.example', lookup), /ERR_PRIVATE_IP/); + await assert.rejects(() => resolvePublicAddresses('evil.example', lookup), /ERR_PROXY_PRIVATE_IP/); }); it('rejects oversized generic data URLs', () => { @@ -28,4 +28,3 @@ describe('SSRF private address detection', () => { assert.throws(() => parseGenericDataUrl(tooLarge), /Data URL exceeds/); }); }); -