Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
5ff96de4
authored
Feb 05, 2026
by
Seefs
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add header passthrough
parent
b564194f
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
191 additions
and
2 deletions
+191
-2
relay/channel/api_request.go
+158
-1
web/src/components/table/channels/modals/EditChannelModal.jsx
+21
-1
web/src/i18n/locales/en.json
+2
-0
web/src/i18n/locales/fr.json
+2
-0
web/src/i18n/locales/ja.json
+2
-0
web/src/i18n/locales/ru.json
+2
-0
web/src/i18n/locales/vi.json
+2
-0
web/src/i18n/locales/zh.json
+2
-0
No files found.
relay/channel/api_request.go
View file @
5ff96de4
...
@@ -6,6 +6,7 @@ import (
...
@@ -6,6 +6,7 @@ import (
"fmt"
"fmt"
"io"
"io"
"net/http"
"net/http"
"regexp"
"strings"
"strings"
"sync"
"sync"
"time"
"time"
...
@@ -40,6 +41,86 @@ func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Hea
...
@@ -40,6 +41,86 @@ func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Hea
const
clientHeaderPlaceholderPrefix
=
"{client_header:"
const
clientHeaderPlaceholderPrefix
=
"{client_header:"
const
(
headerPassthroughAllKey
=
"*"
headerPassthroughRegexPrefix
=
"re:"
headerPassthroughRegexPrefixV2
=
"regex:"
)
var
passthroughSkipHeaderNamesLower
=
map
[
string
]
struct
{}{
// RFC 7230 hop-by-hop headers.
"connection"
:
{},
"keep-alive"
:
{},
"proxy-authenticate"
:
{},
"proxy-authorization"
:
{},
"te"
:
{},
"trailer"
:
{},
"transfer-encoding"
:
{},
"upgrade"
:
{},
// Additional headers that should not be forwarded by name-matching passthrough rules.
"host"
:
{},
"content-length"
:
{},
// Do not passthrough credentials by wildcard/regex.
"authorization"
:
{},
"x-api-key"
:
{},
"x-goog-api-key"
:
{},
// WebSocket handshake headers are generated by the client/dialer.
"sec-websocket-key"
:
{},
"sec-websocket-version"
:
{},
"sec-websocket-extensions"
:
{},
}
var
headerPassthroughRegexCache
sync
.
Map
// map[string]*regexp.Regexp
func
getHeaderPassthroughRegex
(
pattern
string
)
(
*
regexp
.
Regexp
,
error
)
{
pattern
=
strings
.
TrimSpace
(
pattern
)
if
pattern
==
""
{
return
nil
,
errors
.
New
(
"empty regex pattern"
)
}
if
v
,
ok
:=
headerPassthroughRegexCache
.
Load
(
pattern
);
ok
{
if
re
,
ok
:=
v
.
(
*
regexp
.
Regexp
);
ok
{
return
re
,
nil
}
headerPassthroughRegexCache
.
Delete
(
pattern
)
}
compiled
,
err
:=
regexp
.
Compile
(
pattern
)
if
err
!=
nil
{
return
nil
,
err
}
actual
,
_
:=
headerPassthroughRegexCache
.
LoadOrStore
(
pattern
,
compiled
)
if
re
,
ok
:=
actual
.
(
*
regexp
.
Regexp
);
ok
{
return
re
,
nil
}
return
compiled
,
nil
}
func
isHeaderPassthroughRuleKey
(
key
string
)
bool
{
key
=
strings
.
TrimSpace
(
key
)
if
key
==
""
{
return
false
}
if
key
==
headerPassthroughAllKey
{
return
true
}
lower
:=
strings
.
ToLower
(
key
)
return
strings
.
HasPrefix
(
lower
,
headerPassthroughRegexPrefix
)
||
strings
.
HasPrefix
(
lower
,
headerPassthroughRegexPrefixV2
)
}
func
shouldSkipPassthroughHeader
(
name
string
)
bool
{
name
=
strings
.
TrimSpace
(
name
)
if
name
==
""
{
return
true
}
lower
:=
strings
.
ToLower
(
name
)
if
_
,
ok
:=
passthroughSkipHeaderNamesLower
[
lower
];
ok
{
return
true
}
return
false
}
func
applyHeaderOverridePlaceholders
(
template
string
,
c
*
gin
.
Context
,
apiKey
string
)
(
string
,
bool
,
error
)
{
func
applyHeaderOverridePlaceholders
(
template
string
,
c
*
gin
.
Context
,
apiKey
string
)
(
string
,
bool
,
error
)
{
trimmed
:=
strings
.
TrimSpace
(
template
)
trimmed
:=
strings
.
TrimSpace
(
template
)
if
strings
.
HasPrefix
(
trimmed
,
clientHeaderPlaceholderPrefix
)
{
if
strings
.
HasPrefix
(
trimmed
,
clientHeaderPlaceholderPrefix
)
{
...
@@ -77,9 +158,85 @@ func applyHeaderOverridePlaceholders(template string, c *gin.Context, apiKey str
...
@@ -77,9 +158,85 @@ func applyHeaderOverridePlaceholders(template string, c *gin.Context, apiKey str
// Supported placeholders:
// Supported placeholders:
// - {api_key}: resolved to the channel API key
// - {api_key}: resolved to the channel API key
// - {client_header:<name>}: resolved to the incoming request header value
// - {client_header:<name>}: resolved to the incoming request header value
//
// Header passthrough rules (keys only; values are ignored):
// - "*": passthrough all incoming headers by name (excluding unsafe headers)
// - "re:<regex>" / "regex:<regex>": passthrough headers whose names match the regex (Go regexp)
//
// Passthrough rules are applied first, then normal overrides are applied, so explicit overrides win.
func
processHeaderOverride
(
info
*
common
.
RelayInfo
,
c
*
gin
.
Context
)
(
map
[
string
]
string
,
error
)
{
func
processHeaderOverride
(
info
*
common
.
RelayInfo
,
c
*
gin
.
Context
)
(
map
[
string
]
string
,
error
)
{
headerOverride
:=
make
(
map
[
string
]
string
)
headerOverride
:=
make
(
map
[
string
]
string
)
passAll
:=
false
var
passthroughRegex
[]
*
regexp
.
Regexp
for
k
:=
range
info
.
HeadersOverride
{
key
:=
strings
.
TrimSpace
(
k
)
if
key
==
""
{
continue
}
if
key
==
headerPassthroughAllKey
{
passAll
=
true
continue
}
lower
:=
strings
.
ToLower
(
key
)
var
pattern
string
switch
{
case
strings
.
HasPrefix
(
lower
,
headerPassthroughRegexPrefix
)
:
pattern
=
strings
.
TrimSpace
(
key
[
len
(
headerPassthroughRegexPrefix
)
:
])
case
strings
.
HasPrefix
(
lower
,
headerPassthroughRegexPrefixV2
)
:
pattern
=
strings
.
TrimSpace
(
key
[
len
(
headerPassthroughRegexPrefixV2
)
:
])
default
:
continue
}
if
pattern
==
""
{
return
nil
,
types
.
NewError
(
fmt
.
Errorf
(
"header passthrough regex pattern is empty: %q"
,
k
),
types
.
ErrorCodeChannelHeaderOverrideInvalid
)
}
compiled
,
err
:=
getHeaderPassthroughRegex
(
pattern
)
if
err
!=
nil
{
return
nil
,
types
.
NewError
(
err
,
types
.
ErrorCodeChannelHeaderOverrideInvalid
)
}
passthroughRegex
=
append
(
passthroughRegex
,
compiled
)
}
if
passAll
||
len
(
passthroughRegex
)
>
0
{
if
c
==
nil
||
c
.
Request
==
nil
{
return
nil
,
types
.
NewError
(
fmt
.
Errorf
(
"missing request context for header passthrough"
),
types
.
ErrorCodeChannelHeaderOverrideInvalid
)
}
for
name
:=
range
c
.
Request
.
Header
{
if
shouldSkipPassthroughHeader
(
name
)
{
continue
}
if
!
passAll
{
matched
:=
false
for
_
,
re
:=
range
passthroughRegex
{
if
re
.
MatchString
(
name
)
{
matched
=
true
break
}
}
if
!
matched
{
continue
}
}
value
:=
strings
.
TrimSpace
(
c
.
Request
.
Header
.
Get
(
name
))
if
value
==
""
{
continue
}
headerOverride
[
name
]
=
value
}
}
for
k
,
v
:=
range
info
.
HeadersOverride
{
for
k
,
v
:=
range
info
.
HeadersOverride
{
if
isHeaderPassthroughRuleKey
(
k
)
{
continue
}
key
:=
strings
.
TrimSpace
(
k
)
if
key
==
""
{
continue
}
str
,
ok
:=
v
.
(
string
)
str
,
ok
:=
v
.
(
string
)
if
!
ok
{
if
!
ok
{
return
nil
,
types
.
NewError
(
nil
,
types
.
ErrorCodeChannelHeaderOverrideInvalid
)
return
nil
,
types
.
NewError
(
nil
,
types
.
ErrorCodeChannelHeaderOverrideInvalid
)
...
@@ -93,7 +250,7 @@ func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]s
...
@@ -93,7 +250,7 @@ func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]s
continue
continue
}
}
headerOverride
[
k
]
=
value
headerOverride
[
k
ey
]
=
value
}
}
return
headerOverride
,
nil
return
headerOverride
,
nil
}
}
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
5ff96de4
...
@@ -3120,9 +3120,12 @@ const EditChannelModal = (props) => {
...
@@ -3120,9 +3120,12 @@ const EditChannelModal = (props) => {
'header_override'
,
'header_override'
,
JSON
.
stringify
(
JSON
.
stringify
(
{
{
'*'
:
true
,
're:^X-Trace-.*$'
:
true
,
'X-Foo'
:
'
{
client_header
:
X
-
Foo
}
',
Authorization: '
Bearer
{
api_key
}
',
'
User
-
Agent
':
'
User
-
Agent
':
'
Mozilla
/
5.0
(
Windows
NT
10.0
;
Win64
;
x64
)
AppleWebKit
/
537.36
(
KHTML
,
like
Gecko
)
Chrome
/
139.0
.
0.0
Safari
/
537.36
Edg
/
139.0
.
0.0
'
,
'
Mozilla
/
5.0
(
Windows
NT
10.0
;
Win64
;
x64
)
AppleWebKit
/
537.36
(
KHTML
,
like
Gecko
)
Chrome
/
139.0
.
0.0
Safari
/
537.36
Edg
/
139.0
.
0.0
'
,
Authorization
:
'
Bearer
{
api_key
}
'
,
},
},
null
,
null
,
2
,
2
,
...
@@ -3134,6 +3137,23 @@ const EditChannelModal = (props) => {
...
@@ -3134,6 +3137,23 @@ const EditChannelModal = (props) => {
</
Text
>
</
Text
>
<
Text
<
Text
className=
'!text-semi-color-primary cursor-pointer'
className=
'!text-semi-color-primary cursor-pointer'
onClick=
{
()
=>
handleInputChange
(
'header_override'
,
JSON
.
stringify
(
{
'*'
:
true
,
},
null
,
2
,
),
)
}
>
{
t
(
'填入透传模版'
)
}
</
Text
>
<
Text
className=
'!text-semi-color-primary cursor-pointer'
onClick=
{
()
=>
formatJsonField
(
'header_override'
)
}
onClick=
{
()
=>
formatJsonField
(
'header_override'
)
}
>
>
{
t
(
'格式化'
)
}
{
t
(
'格式化'
)
}
...
...
web/src/i18n/locales/en.json
View file @
5ff96de4
...
@@ -739,6 +739,8 @@
...
@@ -739,6 +739,8 @@
"填入"
:
"Fill"
,
"填入"
:
"Fill"
,
"填入所有模型"
:
"Fill in all models"
,
"填入所有模型"
:
"Fill in all models"
,
"填入模板"
:
"Fill Template"
,
"填入模板"
:
"Fill Template"
,
"填入透传模版"
:
"Fill Passthrough Template"
,
"填入透传完整模版"
:
"Fill Full Passthrough Template"
,
"填入相关模型"
:
"Fill Related Models"
,
"填入相关模型"
:
"Fill Related Models"
,
"填写Gotify服务器的完整URL地址"
:
"Fill in the complete URL address of the Gotify server"
,
"填写Gotify服务器的完整URL地址"
:
"Fill in the complete URL address of the Gotify server"
,
"填写带https的域名,逗号分隔"
:
"Fill in domains with https, separated by commas"
,
"填写带https的域名,逗号分隔"
:
"Fill in domains with https, separated by commas"
,
...
...
web/src/i18n/locales/fr.json
View file @
5ff96de4
...
@@ -744,6 +744,8 @@
...
@@ -744,6 +744,8 @@
"填入"
:
"Remplir"
,
"填入"
:
"Remplir"
,
"填入所有模型"
:
"Remplir tous les modèles"
,
"填入所有模型"
:
"Remplir tous les modèles"
,
"填入模板"
:
"Remplir le modèle"
,
"填入模板"
:
"Remplir le modèle"
,
"填入透传模版"
:
"Remplir le modèle passthrough"
,
"填入透传完整模版"
:
"Remplir le modèle passthrough complet"
,
"填入相关模型"
:
"Remplir les modèles associés"
,
"填入相关模型"
:
"Remplir les modèles associés"
,
"填写Gotify服务器的完整URL地址"
:
"Remplir l'adresse URL complète du serveur Gotify"
,
"填写Gotify服务器的完整URL地址"
:
"Remplir l'adresse URL complète du serveur Gotify"
,
"填写带https的域名,逗号分隔"
:
"Saisir les domaines avec https, séparés par des virgules"
,
"填写带https的域名,逗号分隔"
:
"Saisir les domaines avec https, séparés par des virgules"
,
...
...
web/src/i18n/locales/ja.json
View file @
5ff96de4
...
@@ -735,6 +735,8 @@
...
@@ -735,6 +735,8 @@
"填入"
:
"入力"
,
"填入"
:
"入力"
,
"填入所有模型"
:
"すべてのモデルを入力"
,
"填入所有模型"
:
"すべてのモデルを入力"
,
"填入模板"
:
"テンプレートを入力"
,
"填入模板"
:
"テンプレートを入力"
,
"填入透传模版"
:
"パススルーテンプレートを入力"
,
"填入透传完整模版"
:
"完全なパススルーテンプレートを入力"
,
"填入相关模型"
:
"関連モデルを入力"
,
"填入相关模型"
:
"関連モデルを入力"
,
"填写Gotify服务器的完整URL地址"
:
"Gotifyサーバーの完全なURLを入力してください"
,
"填写Gotify服务器的完整URL地址"
:
"Gotifyサーバーの完全なURLを入力してください"
,
"填写带https的域名,逗号分隔"
:
"https://を含むドメインをカンマ区切りで入力してください"
,
"填写带https的域名,逗号分隔"
:
"https://を含むドメインをカンマ区切りで入力してください"
,
...
...
web/src/i18n/locales/ru.json
View file @
5ff96de4
...
@@ -750,6 +750,8 @@
...
@@ -750,6 +750,8 @@
"填入"
:
"Заполнить"
,
"填入"
:
"Заполнить"
,
"填入所有模型"
:
"Заполнить все модели"
,
"填入所有模型"
:
"Заполнить все модели"
,
"填入模板"
:
"Заполнить шаблон"
,
"填入模板"
:
"Заполнить шаблон"
,
"填入透传模版"
:
"Заполнить шаблон passthrough"
,
"填入透传完整模版"
:
"Заполнить полный шаблон passthrough"
,
"填入相关模型"
:
"Заполнить связанные модели"
,
"填入相关模型"
:
"Заполнить связанные модели"
,
"填写Gotify服务器的完整URL地址"
:
"Введите полный URL-адрес сервера Gotify"
,
"填写Gotify服务器的完整URL地址"
:
"Введите полный URL-адрес сервера Gotify"
,
"填写带https的域名,逗号分隔"
:
"Введите домены с https, разделённые запятыми"
,
"填写带https的域名,逗号分隔"
:
"Введите домены с https, разделённые запятыми"
,
...
...
web/src/i18n/locales/vi.json
View file @
5ff96de4
...
@@ -736,6 +736,8 @@
...
@@ -736,6 +736,8 @@
"填入"
:
"Điền"
,
"填入"
:
"Điền"
,
"填入所有模型"
:
"Điền tất cả mô hình"
,
"填入所有模型"
:
"Điền tất cả mô hình"
,
"填入模板"
:
"Điền mẫu"
,
"填入模板"
:
"Điền mẫu"
,
"填入透传模版"
:
"Điền mẫu truyền qua"
,
"填入透传完整模版"
:
"Điền mẫu truyền qua đầy đủ"
,
"填入相关模型"
:
"Điền mô hình liên quan"
,
"填入相关模型"
:
"Điền mô hình liên quan"
,
"填写Gotify服务器的完整URL地址"
:
"Điền địa chỉ URL đầy đủ của máy chủ Gotify"
,
"填写Gotify服务器的完整URL地址"
:
"Điền địa chỉ URL đầy đủ của máy chủ Gotify"
,
"填写带https的域名,逗号分隔"
:
"Điền tên miền có https, phân tách bằng dấu phẩy"
,
"填写带https的域名,逗号分隔"
:
"Điền tên miền có https, phân tách bằng dấu phẩy"
,
...
...
web/src/i18n/locales/zh.json
View file @
5ff96de4
...
@@ -735,6 +735,8 @@
...
@@ -735,6 +735,8 @@
"填入"
:
"填入"
,
"填入"
:
"填入"
,
"填入所有模型"
:
"填入所有模型"
,
"填入所有模型"
:
"填入所有模型"
,
"填入模板"
:
"填入模板"
,
"填入模板"
:
"填入模板"
,
"填入透传模版"
:
"填入透传模版"
,
"填入透传完整模版"
:
"填入透传完整模版"
,
"填入相关模型"
:
"填入相关模型"
,
"填入相关模型"
:
"填入相关模型"
,
"填写Gotify服务器的完整URL地址"
:
"填写Gotify服务器的完整URL地址"
,
"填写Gotify服务器的完整URL地址"
:
"填写Gotify服务器的完整URL地址"
,
"填写带https的域名,逗号分隔"
:
"填写带https的域名,逗号分隔"
,
"填写带https的域名,逗号分隔"
:
"填写带https的域名,逗号分隔"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment