fix(tls-sidecar): 修复代理请求标头处理和配置显示空值问题

修复 Cloudflare 403 错误:彻底清理代理和网络特征标头,保持小写形式
修复前端配置显示:正确处理 null/undefined 值,避免显示 "null" 字符串
This commit is contained in:
hex2077 2026-03-01 22:01:09 +08:00
parent 829fafc651
commit f22cc4499a
2 changed files with 39 additions and 22 deletions

View file

@ -458,7 +458,7 @@ function renderProviderConfig(provider) {
baseFields.forEach(fieldKey => {
const displayLabel = getFieldLabel(fieldKey);
const value = provider[fieldKey];
const displayValue = value !== undefined ? value : '';
const displayValue = (value !== undefined && value !== null) ? value : '';
// 查找字段定义以获取 placeholder
const fieldDef = fieldConfigs.find(f => f.id === fieldKey) || fieldConfigs.find(f => f.id.toUpperCase() === fieldKey.toUpperCase()) || {};
@ -473,7 +473,7 @@ function renderProviderConfig(provider) {
value="${displayValue}"
readonly
data-config-key="${fieldKey}"
data-config-value="${value || ''}"
data-config-value="${(value !== undefined && value !== null) ? value : ''}"
placeholder="${placeholder}">
</div>
`;
@ -502,7 +502,7 @@ function renderProviderConfig(provider) {
value="${displayValue}"
readonly
data-config-key="${fieldKey}"
data-config-value="${value || ''}"
data-config-value="${(value !== undefined && value !== null) ? value : ''}"
placeholder="${placeholder}">
</div>
`;
@ -521,7 +521,7 @@ function renderProviderConfig(provider) {
const field1Value = provider[field1Key];
const field1IsPassword = field1Key.toLowerCase().includes('key') || field1Key.toLowerCase().includes('password');
const field1IsOAuthFilePath = field1Key.includes('OAUTH_CREDS_FILE_PATH');
const field1DisplayValue = field1IsPassword && field1Value ? '••••••••' : (field1Value || '');
const field1DisplayValue = field1IsPassword && field1Value ? '••••••••' : ((field1Value !== undefined && field1Value !== null) ? field1Value : '');
const field1Def = fieldConfigs.find(f => f.id === field1Key) || fieldConfigs.find(f => f.id.toUpperCase() === field1Key.toUpperCase()) || {};
if (field1IsPassword) {
@ -533,7 +533,7 @@ function renderProviderConfig(provider) {
value="${field1DisplayValue}"
readonly
data-config-key="${field1Key}"
data-config-value="${field1Value || ''}"
data-config-value="${(field1Value !== undefined && field1Value !== null) ? field1Value : ''}"
placeholder="${field1Def.placeholder || ''}">
<button type="button" class="password-toggle" data-target="${field1Key}">
<i class="fas fa-eye"></i>
@ -550,10 +550,10 @@ function renderProviderConfig(provider) {
<div class="file-input-group">
<input type="text"
id="edit-${provider.uuid}-${field1Key}"
value="${field1Value || ''}"
value="${(field1Value !== undefined && field1Value !== null) ? field1Value : ''}"
readonly
data-config-key="${field1Key}"
data-config-value="${field1Value || ''}"
data-config-value="${(field1Value !== undefined && field1Value !== null) ? field1Value : ''}"
placeholder="${field1Def.placeholder || ''}">
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field1Key}" aria-label="上传文件" disabled>
<i class="fas fa-upload"></i>
@ -570,7 +570,7 @@ function renderProviderConfig(provider) {
value="${field1DisplayValue}"
readonly
data-config-key="${field1Key}"
data-config-value="${field1Value || ''}"
data-config-value="${(field1Value !== undefined && field1Value !== null) ? field1Value : ''}"
placeholder="${field1Def.placeholder || ''}">
</div>
`;
@ -583,7 +583,7 @@ function renderProviderConfig(provider) {
const field2Value = provider[field2Key];
const field2IsPassword = field2Key.toLowerCase().includes('key') || field2Key.toLowerCase().includes('password');
const field2IsOAuthFilePath = field2Key.includes('OAUTH_CREDS_FILE_PATH');
const field2DisplayValue = field2IsPassword && field2Value ? '••••••••' : (field2Value || '');
const field2DisplayValue = field2IsPassword && field2Value ? '••••••••' : ((field2Value !== undefined && field2Value !== null) ? field2Value : '');
const field2Def = fieldConfigs.find(f => f.id === field2Key) || fieldConfigs.find(f => f.id.toUpperCase() === field2Key.toUpperCase()) || {};
if (field2IsPassword) {
@ -595,7 +595,7 @@ function renderProviderConfig(provider) {
value="${field2DisplayValue}"
readonly
data-config-key="${field2Key}"
data-config-value="${field2Value || ''}"
data-config-value="${(field2Value !== undefined && field2Value !== null) ? field2Value : ''}"
placeholder="${field2Def.placeholder || ''}">
<button type="button" class="password-toggle" data-target="${field2Key}">
<i class="fas fa-eye"></i>
@ -612,10 +612,10 @@ function renderProviderConfig(provider) {
<div class="file-input-group">
<input type="text"
id="edit-${provider.uuid}-${field2Key}"
value="${field2Value || ''}"
value="${(field2Value !== undefined && field2Value !== null) ? field2Value : ''}"
readonly
data-config-key="${field2Key}"
data-config-value="${field2Value || ''}"
data-config-value="${(field2Value !== undefined && field2Value !== null) ? field2Value : ''}"
placeholder="${field2Def.placeholder || ''}">
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field2Key}" aria-label="上传文件" disabled>
<i class="fas fa-upload"></i>
@ -632,7 +632,7 @@ function renderProviderConfig(provider) {
value="${field2DisplayValue}"
readonly
data-config-key="${field2Key}"
data-config-value="${field2Value || ''}"
data-config-value="${(field2Value !== undefined && field2Value !== null) ? field2Value : ''}"
placeholder="${field2Def.placeholder || ''}">
</div>
`;
@ -831,10 +831,12 @@ function cancelEdit(uuid, event) {
// 恢复输入框为只读状态
configInputs.forEach(input => {
input.readOnly = true;
// 恢复显示为密码格式(如果有的话)
const originalValue = input.dataset.configValue;
// 恢复原始值
if (input.type === 'password') {
const actualValue = input.dataset.configValue;
input.value = actualValue ? '••••••••' : '';
input.value = originalValue ? '••••••••' : '';
} else {
input.value = originalValue || '';
}
});

View file

@ -121,7 +121,11 @@ func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
if alpn == "h2" {
// HTTP/2: 创建 H2 ClientConn
cc, err := (&http2.Transport{}).NewClientConn(conn)
t2 := &http2.Transport{
StrictMaxConcurrentStreams: true,
AllowHTTP: false,
}
cc, err := t2.NewClientConn(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("h2 client conn: %w", err)
@ -240,21 +244,31 @@ func handleProxy(w http.ResponseWriter, r *http.Request) {
}
// Copy headers (skip internal + hop-by-hop)
// 403 关键修复:彻底清理所有非浏览器标头,严格保持小写
for key, vals := range r.Header {
lk := strings.ToLower(key)
if lk == strings.ToLower(headerTarget) || lk == strings.ToLower(headerProxy) {
continue
}
// 移除所有代理、本地网络特征标头,防止 Cloudflare 识别
if lk == "connection" || lk == "keep-alive" || lk == "transfer-encoding" ||
lk == "te" || lk == "trailer" || lk == "upgrade" || lk == "host" {
lk == "te" || lk == "trailer" || lk == "upgrade" || lk == "host" ||
lk == "x-forwarded-for" || lk == "x-real-ip" || lk == "x-forwarded-proto" ||
lk == "x-forwarded-host" || lk == "via" || lk == "proxy-connection" ||
lk == "cf-connecting-ip" || lk == "true-client-ip" {
continue
}
for _, v := range vals {
outReq.Header.Add(key, v)
}
// 直接通过 map 赋值,确保 Go 的 http2 栈能识别并以原始(小写)形式发出
outReq.Header[key] = vals
}
outReq.Host = parsed.Host
// 针对 Grok 的特殊处理:如果 Accept-Encoding 包含 br 且环境可能存在压缩协商问题
// 强制设置为标准的浏览器组合
if ae := outReq.Header["Accept-Encoding"]; len(ae) > 0 {
outReq.Header["Accept-Encoding"] = []string{"gzip, deflate, br, zstd"}
}
// Execute via uTLS RoundTripper
rt := getOrCreateRT(proxyURL)
resp, err := rt.RoundTrip(outReq)
@ -316,7 +330,8 @@ func dialUTLS(ctx context.Context, network, addr string, proxyURL string) (*utls
return nil, fmt.Errorf("tcp dial failed: %w", err)
}
// uTLS 握手 — Chrome 最新指纹 (HelloChrome_Auto 跟随 utls 库更新)
// uTLS 握手 — 使用 Chrome 最新自动指纹
// 403 错误通过保持标头小写和清理转发标头来解决
tlsConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
NextProtos: []string{"h2", "http/1.1"},