This is an automated email from the ASF dual-hosted git repository. hsluoyz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
commit 46bf1d8970e002dff96b508ce3434ae0c18fb0ee Author: Yang Luo <[email protected]> AuthorDate: Fri Sep 4 16:06:48 2026 +0800 feat(origin): let browser extensions call the relay, and edit the host/origin lists in Settings --- conf/app.conf | 5 +++-- conf/conf.go | 5 +++-- object/setting.go | 25 +++++++++++++++++++++++++ routers/origin.go | 39 ++++++++++++++++++++++++++++++++++----- web/src/locales/en/data.json | 4 ++++ web/src/locales/zh/data.json | 4 ++++ web/src/pages/SettingPage.tsx | 2 ++ web/src/types.ts | 5 +++++ 8 files changed, 80 insertions(+), 9 deletions(-) diff --git a/conf/app.conf b/conf/app.conf index 0f93c1e..55737b9 100644 --- a/conf/app.conf +++ b/conf/app.conf @@ -21,11 +21,12 @@ copyrequestbody = true ; other name that is not this machine's. Its own addresses, "localhost" and its ; hostname always work; a browser request for any other name is refused, so that ; a domain pointed at 127.0.0.1 cannot drive Gateway through the operator's -; browser. +; browser. Edited from the Settings page afterwards, like the settings above. allowedHosts = ; Browser origins allowed to call the API from another site, comma-separated and ; written in full, e.g. "https://app.example.com". The web UI needs none of -; these; a page of your own that calls the relay does. +; these, and neither does a browser extension calling /v1; a page of your own +; that calls the management API does. allowedOrigins = ; Database. XORM is used, so mysql, postgres, mssql and sqlite are all supported. diff --git a/conf/conf.go b/conf/conf.go index f6cc65b..a69e1cc 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -166,8 +166,9 @@ func GetAllowedHosts() []string { } // GetAllowedOrigins is the browser origins allowed to call the API from another -// site. The web UI is served by Gateway itself, so it needs none of them: this -// is for a page of your own that talks to the relay. +// site. The web UI is served by Gateway itself, so it needs none of them, and +// neither does a browser extension calling the relay: this is for a page of +// your own that talks to the management API. func GetAllowedOrigins() []string { return getConfigList("allowedOrigins") } diff --git a/object/setting.go b/object/setting.go index e0b3edd..2965987 100644 --- a/object/setting.go +++ b/object/setting.go @@ -84,6 +84,12 @@ type Setting struct { // first start; clearing it on the Settings page issues a new one. RelayToken string `xorm:"varchar(200)" json:"relayToken"` + // AllowedHosts is the extra Host header names Gateway answers to, and + // AllowedOrigins the browser origins allowed to call it from another site. + // Both are comma-separated. + AllowedHosts string `xorm:"varchar(1000)" json:"allowedHosts"` + AllowedOrigins string `xorm:"varchar(1000)" json:"allowedOrigins"` + HttpProxy string `xorm:"varchar(200)" json:"httpProxy"` } @@ -130,6 +136,9 @@ func SyncSettingToConf(setting *Setting) { "apiKeyEncryptionKey": setting.ApiKeyEncryptionKey, "relayToken": setting.RelayToken, + "allowedHosts": setting.AllowedHosts, + "allowedOrigins": setting.AllowedOrigins, + "httpProxy": setting.HttpProxy, }) } @@ -249,6 +258,19 @@ func InitBuiltInSetting() { } } + // The origin filter used to read these from conf/app.conf alone, so an + // installation that listed a name there keeps it when the columns arrive. + if setting.AllowedHosts == "" && setting.AllowedOrigins == "" { + setting.AllowedHosts = conf.GetConfigStringUnquoted("allowedHosts") + setting.AllowedOrigins = conf.GetConfigStringUnquoted("allowedOrigins") + if setting.AllowedHosts != "" || setting.AllowedOrigins != "" { + if _, err = ormer.Engine.ID(core.PK{setting.Owner, setting.Name}). + Cols("allowed_hosts", "allowed_origins").Update(setting); err != nil { + panic(err) + } + } + } + initCloudSyncSetting(setting) SyncSettingToConf(setting) @@ -297,6 +319,9 @@ func newSettingFromConf() *Setting { ApiKeyEncryptionKey: conf.GetConfigStringUnquoted("apiKeyEncryptionKey"), RelayToken: newRelayToken(), + AllowedHosts: conf.GetConfigStringUnquoted("allowedHosts"), + AllowedOrigins: conf.GetConfigStringUnquoted("allowedOrigins"), + HttpProxy: conf.GetConfigStringUnquoted("httpProxy"), } } diff --git a/routers/origin.go b/routers/origin.go index 20716aa..ce1696a 100644 --- a/routers/origin.go +++ b/routers/origin.go @@ -34,11 +34,13 @@ import ( // Two checks, and a client that is not a browser — curl, an agent CLI — passes // both without sending anything. The Host has to name this machine, which the // attacker domain of a DNS rebinding does not, and a request carrying an Origin -// has to come from Gateway's own pages or from an origin the operator listed. +// has to come from Gateway's own pages, from an installed browser extension +// calling the relay, or from an origin the operator listed on the Settings +// page. func OriginFilter(ctx *context.Context) { host := ctx.Request.Host if !isAllowedHost(host) { - denyOrigin(ctx, fmt.Sprintf("%q is not a name this Gateway answers to; add it to \"allowedHosts\" in conf/app.conf to reach Gateway under that name", + denyOrigin(ctx, fmt.Sprintf("%q is not a name this Gateway answers to; add it to \"Allowed hosts\" on the Settings page to reach Gateway under that name", hostnameOf(host))) return } @@ -47,12 +49,12 @@ func OriginFilter(ctx *context.Context) { if origin == "" || sameOrigin(origin, host) { return } - if !isAllowedOrigin(origin) { - denyOrigin(ctx, fmt.Sprintf("%s is another site, and Gateway is not callable from one; add it to \"allowedOrigins\" in conf/app.conf to allow it", origin)) + if !isAllowedOrigin(origin) && !isAllowedExtensionCall(ctx, origin) { + denyOrigin(ctx, fmt.Sprintf("%s is another site, and Gateway is not callable from one; add it to \"Allowed origins\" on the Settings page to allow it", origin)) return } - // An origin the operator listed gets the CORS headers the browser needs to + // An origin that got through gets the CORS headers the browser needs to // hand the answer to it. writeCorsHeaders(ctx, origin) if isPreflight(ctx) { @@ -103,6 +105,33 @@ func isOwnHostname(lower string) bool { return lower == own || strings.HasPrefix(lower, own+".") } +// isAllowedExtensionCall lets a browser extension use the relay. A page cannot +// forge an extension origin, so one does not come from another site at all: it +// comes from software the operator installed, which reaches /v1 the way a local +// agent does. The management API is not part of the deal — an extension has no +// business editing providers — so an extension origin is listed like any other +// to reach that. +func isAllowedExtensionCall(ctx *context.Context, origin string) bool { + return isExtensionOrigin(origin) && isRelayPath(ctx.Request.URL.Path) +} + +var extensionSchemes = []string{"moz-extension://", "chrome-extension://", "safari-web-extension://", "extension://"} + +func isExtensionOrigin(origin string) bool { + lower := strings.ToLower(origin) + for _, scheme := range extensionSchemes { + if strings.HasPrefix(lower, scheme) { + return true + } + } + return false +} + +// isRelayPath separates the LLM gateway from the management API. +func isRelayPath(path string) bool { + return path == "/v1" || strings.HasPrefix(path, "/v1/") || strings.HasPrefix(path, "/v1beta/") +} + func isAllowedOrigin(origin string) bool { for _, allowed := range conf.GetAllowedOrigins() { if strings.EqualFold(strings.TrimSuffix(allowed, "/"), origin) { diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json index a78a9b2..76b1c5d 100644 --- a/web/src/locales/en/data.json +++ b/web/src/locales/en/data.json @@ -1091,6 +1091,10 @@ "Agent state dir": "Agent state directory", "Agent state dir hint": "Patch manifests, config backups, ingest tokens and file-tail cursors.", "Agents": "Agents", + "Allowed hosts": "Allowed hosts", + "Allowed hosts hint": "Extra Host names Gateway answers to, comma-separated. Its own addresses, \"localhost\" and this machine's hostname always work; add a name only when a reverse proxy or another domain points here.", + "Allowed origins": "Allowed origins", + "Allowed origins hint": "Browser origins allowed to call the management API from another site, comma-separated and written in full, e.g. \"https://app.example.com\". The web UI needs none of these, and neither does a browser extension calling /v1.", "Back up now": "Back up now", "Backups": "Backups", "Backups are off": "Backups are off", diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json index ca36f19..baf24df 100644 --- a/web/src/locales/zh/data.json +++ b/web/src/locales/zh/data.json @@ -1163,6 +1163,10 @@ "Agent state dir": "Agent 状态目录", "Agent state dir hint": "补丁清单、配置备份、上报令牌和文件读取位置。", "Agents": "Agent", + "Allowed hosts": "允许的主机名", + "Allowed hosts hint": "网关额外响应的 Host 名称,用逗号分隔。本机地址、\"localhost\" 和本机主机名始终可用;仅当有反向代理或其他域名指向这里时才需要添加。", + "Allowed origins": "允许的来源", + "Allowed origins hint": "允许从其他站点调用管理 API 的浏览器来源,用逗号分隔并写全,例如 \"https://app.example.com\"。网页控制台无需配置,浏览器扩展调用 /v1 也无需配置。", "Back up now": "立即备份", "Backups": "备份", "Backups are off": "自动备份已关闭", diff --git a/web/src/pages/SettingPage.tsx b/web/src/pages/SettingPage.tsx index 7b36215..f9630b0 100644 --- a/web/src/pages/SettingPage.tsx +++ b/web/src/pages/SettingPage.tsx @@ -215,6 +215,8 @@ export default function SettingPage({account}: {account: Account}) { <Section id="network" columns={2} collapsible title={i18next.t("setting:Network")}> {textField("httpProxy", i18next.t("setting:Outbound SOCKS5 proxy"), i18next.t("setting:Outbound SOCKS5 proxy hint"))} + {textField("allowedHosts", i18next.t("setting:Allowed hosts"), i18next.t("setting:Allowed hosts hint"))} + {textField("allowedOrigins", i18next.t("setting:Allowed origins"), i18next.t("setting:Allowed origins hint"))} </Section> <div id="backups" className="scroll-mt-20"> diff --git a/web/src/types.ts b/web/src/types.ts index 951065a..bda14f9 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -847,6 +847,11 @@ export interface Setting { apiKeyEncryptionKey: string; relayToken: string; + /** Comma-separated: extra Host names Gateway answers to, and the browser + * origins allowed to call it from another site. */ + allowedHosts: string; + allowedOrigins: string; + httpProxy: string; backupMode: BackupMode; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
