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 => {
|
baseFields.forEach(fieldKey => {
|
||||||
const displayLabel = getFieldLabel(fieldKey);
|
const displayLabel = getFieldLabel(fieldKey);
|
||||||
const value = provider[fieldKey];
|
const value = provider[fieldKey];
|
||||||
const displayValue = value !== undefined ? value : '';
|
const displayValue = (value !== undefined && value !== null) ? value : '';
|
||||||
|
|
||||||
// 查找字段定义以获取 placeholder
|
// 查找字段定义以获取 placeholder
|
||||||
const fieldDef = fieldConfigs.find(f => f.id === fieldKey) || fieldConfigs.find(f => f.id.toUpperCase() === fieldKey.toUpperCase()) || {};
|
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}"
|
value="${displayValue}"
|
||||||
readonly
|
readonly
|
||||||
data-config-key="${fieldKey}"
|
data-config-key="${fieldKey}"
|
||||||
data-config-value="${value || ''}"
|
data-config-value="${(value !== undefined && value !== null) ? value : ''}"
|
||||||
placeholder="${placeholder}">
|
placeholder="${placeholder}">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -502,7 +502,7 @@ function renderProviderConfig(provider) {
|
||||||
value="${displayValue}"
|
value="${displayValue}"
|
||||||
readonly
|
readonly
|
||||||
data-config-key="${fieldKey}"
|
data-config-key="${fieldKey}"
|
||||||
data-config-value="${value || ''}"
|
data-config-value="${(value !== undefined && value !== null) ? value : ''}"
|
||||||
placeholder="${placeholder}">
|
placeholder="${placeholder}">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -521,7 +521,7 @@ function renderProviderConfig(provider) {
|
||||||
const field1Value = provider[field1Key];
|
const field1Value = provider[field1Key];
|
||||||
const field1IsPassword = field1Key.toLowerCase().includes('key') || field1Key.toLowerCase().includes('password');
|
const field1IsPassword = field1Key.toLowerCase().includes('key') || field1Key.toLowerCase().includes('password');
|
||||||
const field1IsOAuthFilePath = field1Key.includes('OAUTH_CREDS_FILE_PATH');
|
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()) || {};
|
const field1Def = fieldConfigs.find(f => f.id === field1Key) || fieldConfigs.find(f => f.id.toUpperCase() === field1Key.toUpperCase()) || {};
|
||||||
|
|
||||||
if (field1IsPassword) {
|
if (field1IsPassword) {
|
||||||
|
|
@ -533,7 +533,7 @@ function renderProviderConfig(provider) {
|
||||||
value="${field1DisplayValue}"
|
value="${field1DisplayValue}"
|
||||||
readonly
|
readonly
|
||||||
data-config-key="${field1Key}"
|
data-config-key="${field1Key}"
|
||||||
data-config-value="${field1Value || ''}"
|
data-config-value="${(field1Value !== undefined && field1Value !== null) ? field1Value : ''}"
|
||||||
placeholder="${field1Def.placeholder || ''}">
|
placeholder="${field1Def.placeholder || ''}">
|
||||||
<button type="button" class="password-toggle" data-target="${field1Key}">
|
<button type="button" class="password-toggle" data-target="${field1Key}">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
|
|
@ -550,10 +550,10 @@ function renderProviderConfig(provider) {
|
||||||
<div class="file-input-group">
|
<div class="file-input-group">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="edit-${provider.uuid}-${field1Key}"
|
id="edit-${provider.uuid}-${field1Key}"
|
||||||
value="${field1Value || ''}"
|
value="${(field1Value !== undefined && field1Value !== null) ? field1Value : ''}"
|
||||||
readonly
|
readonly
|
||||||
data-config-key="${field1Key}"
|
data-config-key="${field1Key}"
|
||||||
data-config-value="${field1Value || ''}"
|
data-config-value="${(field1Value !== undefined && field1Value !== null) ? field1Value : ''}"
|
||||||
placeholder="${field1Def.placeholder || ''}">
|
placeholder="${field1Def.placeholder || ''}">
|
||||||
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field1Key}" aria-label="上传文件" disabled>
|
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field1Key}" aria-label="上传文件" disabled>
|
||||||
<i class="fas fa-upload"></i>
|
<i class="fas fa-upload"></i>
|
||||||
|
|
@ -570,7 +570,7 @@ function renderProviderConfig(provider) {
|
||||||
value="${field1DisplayValue}"
|
value="${field1DisplayValue}"
|
||||||
readonly
|
readonly
|
||||||
data-config-key="${field1Key}"
|
data-config-key="${field1Key}"
|
||||||
data-config-value="${field1Value || ''}"
|
data-config-value="${(field1Value !== undefined && field1Value !== null) ? field1Value : ''}"
|
||||||
placeholder="${field1Def.placeholder || ''}">
|
placeholder="${field1Def.placeholder || ''}">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -583,7 +583,7 @@ function renderProviderConfig(provider) {
|
||||||
const field2Value = provider[field2Key];
|
const field2Value = provider[field2Key];
|
||||||
const field2IsPassword = field2Key.toLowerCase().includes('key') || field2Key.toLowerCase().includes('password');
|
const field2IsPassword = field2Key.toLowerCase().includes('key') || field2Key.toLowerCase().includes('password');
|
||||||
const field2IsOAuthFilePath = field2Key.includes('OAUTH_CREDS_FILE_PATH');
|
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()) || {};
|
const field2Def = fieldConfigs.find(f => f.id === field2Key) || fieldConfigs.find(f => f.id.toUpperCase() === field2Key.toUpperCase()) || {};
|
||||||
|
|
||||||
if (field2IsPassword) {
|
if (field2IsPassword) {
|
||||||
|
|
@ -595,7 +595,7 @@ function renderProviderConfig(provider) {
|
||||||
value="${field2DisplayValue}"
|
value="${field2DisplayValue}"
|
||||||
readonly
|
readonly
|
||||||
data-config-key="${field2Key}"
|
data-config-key="${field2Key}"
|
||||||
data-config-value="${field2Value || ''}"
|
data-config-value="${(field2Value !== undefined && field2Value !== null) ? field2Value : ''}"
|
||||||
placeholder="${field2Def.placeholder || ''}">
|
placeholder="${field2Def.placeholder || ''}">
|
||||||
<button type="button" class="password-toggle" data-target="${field2Key}">
|
<button type="button" class="password-toggle" data-target="${field2Key}">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
|
|
@ -612,10 +612,10 @@ function renderProviderConfig(provider) {
|
||||||
<div class="file-input-group">
|
<div class="file-input-group">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="edit-${provider.uuid}-${field2Key}"
|
id="edit-${provider.uuid}-${field2Key}"
|
||||||
value="${field2Value || ''}"
|
value="${(field2Value !== undefined && field2Value !== null) ? field2Value : ''}"
|
||||||
readonly
|
readonly
|
||||||
data-config-key="${field2Key}"
|
data-config-key="${field2Key}"
|
||||||
data-config-value="${field2Value || ''}"
|
data-config-value="${(field2Value !== undefined && field2Value !== null) ? field2Value : ''}"
|
||||||
placeholder="${field2Def.placeholder || ''}">
|
placeholder="${field2Def.placeholder || ''}">
|
||||||
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field2Key}" aria-label="上传文件" disabled>
|
<button type="button" class="btn btn-outline upload-btn" data-target="edit-${provider.uuid}-${field2Key}" aria-label="上传文件" disabled>
|
||||||
<i class="fas fa-upload"></i>
|
<i class="fas fa-upload"></i>
|
||||||
|
|
@ -632,7 +632,7 @@ function renderProviderConfig(provider) {
|
||||||
value="${field2DisplayValue}"
|
value="${field2DisplayValue}"
|
||||||
readonly
|
readonly
|
||||||
data-config-key="${field2Key}"
|
data-config-key="${field2Key}"
|
||||||
data-config-value="${field2Value || ''}"
|
data-config-value="${(field2Value !== undefined && field2Value !== null) ? field2Value : ''}"
|
||||||
placeholder="${field2Def.placeholder || ''}">
|
placeholder="${field2Def.placeholder || ''}">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -831,10 +831,12 @@ function cancelEdit(uuid, event) {
|
||||||
// 恢复输入框为只读状态
|
// 恢复输入框为只读状态
|
||||||
configInputs.forEach(input => {
|
configInputs.forEach(input => {
|
||||||
input.readOnly = true;
|
input.readOnly = true;
|
||||||
// 恢复显示为密码格式(如果有的话)
|
const originalValue = input.dataset.configValue;
|
||||||
|
// 恢复原始值
|
||||||
if (input.type === 'password') {
|
if (input.type === 'password') {
|
||||||
const actualValue = input.dataset.configValue;
|
input.value = originalValue ? '••••••••' : '';
|
||||||
input.value = actualValue ? '••••••••' : '';
|
} else {
|
||||||
|
input.value = originalValue || '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,11 @@ func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
|
||||||
|
|
||||||
if alpn == "h2" {
|
if alpn == "h2" {
|
||||||
// HTTP/2: 创建 H2 ClientConn
|
// 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 {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, fmt.Errorf("h2 client conn: %w", err)
|
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)
|
// Copy headers (skip internal + hop-by-hop)
|
||||||
|
// 403 关键修复:彻底清理所有非浏览器标头,严格保持小写
|
||||||
for key, vals := range r.Header {
|
for key, vals := range r.Header {
|
||||||
lk := strings.ToLower(key)
|
lk := strings.ToLower(key)
|
||||||
if lk == strings.ToLower(headerTarget) || lk == strings.ToLower(headerProxy) {
|
if lk == strings.ToLower(headerTarget) || lk == strings.ToLower(headerProxy) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 移除所有代理、本地网络特征标头,防止 Cloudflare 识别
|
||||||
if lk == "connection" || lk == "keep-alive" || lk == "transfer-encoding" ||
|
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
|
continue
|
||||||
}
|
}
|
||||||
for _, v := range vals {
|
// 直接通过 map 赋值,确保 Go 的 http2 栈能识别并以原始(小写)形式发出
|
||||||
outReq.Header.Add(key, v)
|
outReq.Header[key] = vals
|
||||||
}
|
|
||||||
}
|
}
|
||||||
outReq.Host = parsed.Host
|
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
|
// Execute via uTLS RoundTripper
|
||||||
rt := getOrCreateRT(proxyURL)
|
rt := getOrCreateRT(proxyURL)
|
||||||
resp, err := rt.RoundTrip(outReq)
|
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)
|
return nil, fmt.Errorf("tcp dial failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// uTLS 握手 — Chrome 最新指纹 (HelloChrome_Auto 跟随 utls 库更新)
|
// uTLS 握手 — 使用 Chrome 最新自动指纹
|
||||||
|
// 403 错误通过保持标头小写和清理转发标头来解决
|
||||||
tlsConn := utls.UClient(rawConn, &utls.Config{
|
tlsConn := utls.UClient(rawConn, &utls.Config{
|
||||||
ServerName: host,
|
ServerName: host,
|
||||||
NextProtos: []string{"h2", "http/1.1"},
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue