fix(tls-sidecar): 修复代理请求标头处理和配置显示空值问题
修复 Cloudflare 403 错误:彻底清理代理和网络特征标头,保持小写形式 修复前端配置显示:正确处理 null/undefined 值,避免显示 "null" 字符串
This commit is contained in:
parent
829fafc651
commit
f22cc4499a
2 changed files with 39 additions and 22 deletions
|
|
@ -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 || '';
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
Loading…
Reference in a new issue