diff --git a/packages/core/basic/src/utils/util.request.test.ts b/packages/core/basic/src/utils/util.request.test.ts new file mode 100644 index 000000000..cf66ef4cb --- /dev/null +++ b/packages/core/basic/src/utils/util.request.test.ts @@ -0,0 +1,53 @@ +import { expect } from "chai"; +import { createAxiosService, HttpClient, setGlobalHeaders } from "./util.request.js"; +import { ILogger } from "./util.log.js"; + +const testLogger = { + info() {}, + error() {}, +} as unknown as ILogger; + +describe("util.request", () => { + afterEach(() => { + setGlobalHeaders({}); + }); + + it("should merge global headers without overriding request headers", async () => { + setGlobalHeaders({ + "X-Common": "common", + "X-Override": "global", + }); + + const http = createAxiosService({ logger: testLogger }) as HttpClient; + const res = await http.request({ + url: "http://example.com", + method: "get", + logReq: false, + logRes: false, + headers: { + "X-Override": "request", + "X-Request": "request", + }, + adapter: async config => { + const headers = config.headers; + return { + config, + data: { + common: headers.get("X-Common"), + override: headers.get("X-Override"), + request: headers.get("X-Request"), + }, + headers: {}, + status: 200, + statusText: "OK", + }; + }, + }); + + expect(res).to.deep.equal({ + common: "common", + override: "request", + request: "request", + }); + }); +}); diff --git a/packages/core/basic/src/utils/util.request.ts b/packages/core/basic/src/utils/util.request.ts index df1edddb7..5c3d81be3 100644 --- a/packages/core/basic/src/utils/util.request.ts +++ b/packages/core/basic/src/utils/util.request.ts @@ -82,6 +82,7 @@ export class HttpError extends Error { export const HttpCommonError = HttpError; let defaultAgents = createAgent(); +let defaultHeaders: Record = {}; export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) { logger.info("setGlobalProxy:", opts); @@ -92,6 +93,15 @@ export function getGlobalAgents() { return defaultAgents; } +export function setGlobalHeaders(headers: Record = {}) { + logger.info("setGlobalHeaders:", Object.keys(headers)); + defaultHeaders = { ...headers }; +} + +export function getGlobalHeaders() { + return defaultHeaders; +} + /** * @description 创建请求实例 */ @@ -148,6 +158,12 @@ export function createAxiosService({ logger }: { logger: ILogger }) { config.httpsAgent = agents.httpsAgent; config.httpAgent = agents.httpAgent; + if (Object.keys(defaultHeaders).length > 0) { + const headers = AxiosHeaders.from(defaultHeaders); + headers.set(config.headers || {}); + config.headers = headers; + } + // const agent = new https.Agent({ // rejectUnauthorized: false // 允许自签名证书 // }); diff --git a/packages/libs/lib-server/src/system/settings/service/models.ts b/packages/libs/lib-server/src/system/settings/service/models.ts index 563a3df7d..89e96760f 100644 --- a/packages/libs/lib-server/src/system/settings/service/models.ts +++ b/packages/libs/lib-server/src/system/settings/service/models.ts @@ -80,6 +80,7 @@ export class SysPrivateSettings extends BaseSettings { httpsProxy? = ''; httpProxy? = ''; + commonHeaders?: string = ''; reverseProxies?: Record = {}; diff --git a/packages/libs/lib-server/src/system/settings/service/sys-settings-service.ts b/packages/libs/lib-server/src/system/settings/service/sys-settings-service.ts index 86a16e4ed..7fa835a9c 100644 --- a/packages/libs/lib-server/src/system/settings/service/sys-settings-service.ts +++ b/packages/libs/lib-server/src/system/settings/service/sys-settings-service.ts @@ -5,7 +5,7 @@ import { SysSettingsEntity } from '../entity/sys-settings.js'; import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js'; import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client'; -import { cache, logger, mergeUtils, setGlobalProxy } from '@certd/basic'; +import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from '@certd/basic'; import { isPlus } from '@certd/plus-core'; import * as dns from 'node:dns'; import { BaseService, setAdminMode } from '../../../basic/index.js'; @@ -167,6 +167,7 @@ export class SysSettingsService extends BaseService { httpsProxy: privateSetting.httpsProxy, }; setGlobalProxy(opts); + setGlobalHeaders(this.parseKeyValueText(privateSetting.commonHeaders)); if (privateSetting.dnsResultOrder) { dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any); @@ -185,12 +186,12 @@ export class SysSettingsService extends BaseService { } - setEnvironmentVars(vars: string) { - const envVars = {} - if (typeof vars !== 'string') { - vars = "" + parseKeyValueText(text: string) { + const values = {}; + if (typeof text !== 'string') { + text = ""; } - vars.split('\n').forEach(line => { + text.split('\n').forEach(line => { line = line.trim(); if (!line || line.startsWith('#')) { return @@ -204,11 +205,18 @@ export class SysSettingsService extends BaseService { return } - const [key, value] = line.split('='); + const eqIndex = line.indexOf('='); + const key = line.substring(0, eqIndex).trim(); + const value = line.substring(eqIndex + 1).trim(); if (key && value) { - envVars[key.trim()] = value.trim(); + values[key] = value; } }); + return values; + } + + setEnvironmentVars(vars: string) { + const envVars = this.parseKeyValueText(vars); //先删除旧环境变量 if (lastSaveEnvVars) { for (const key in lastSaveEnvVars) { diff --git a/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-settings.ts b/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-settings.ts index b8e62dc17..cb0089c0f 100644 --- a/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-settings.ts +++ b/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-settings.ts @@ -91,6 +91,8 @@ export default { reverseProxyEmpty: "No reverse proxy list configured", environmentVars: "Environment Variables", environmentVarsHelper: "configure the runtime environment variables, one per line, format: KEY=VALUE", + commonHeaders: "Common Headers", + commonHeadersHelper: "Common headers automatically added to server-side HTTP requests, one per line, format: KEY=VALUE. Existing request headers with the same name are not overwritten.", bindUrl: "Bind URL", bindUrlHelper: "Bind URL, used as your site URL in notifications", diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-settings.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-settings.ts index 2e01e5354..2d1f30028 100644 --- a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-settings.ts +++ b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-settings.ts @@ -89,6 +89,8 @@ export default { reverseProxyEmpty: "未配置反向代理", environmentVars: "环境变量", environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE", + commonHeaders: "公共请求头", + commonHeadersHelper: "服务端发起 HTTP 请求时自动附加的公共请求头,每行一个,格式:KEY=VALUE;请求中已设置同名 Header 时不会覆盖\n注意: 不要将token等敏感内容放在此处,仅限个人和公司内部使用,商业版不要设置", bindUrl: "绑定URL", bindUrlHelper: "绑定URL,在各类通知中显示你的站点URL", }, diff --git a/packages/ui/certd-client/src/store/settings/api.basic.ts b/packages/ui/certd-client/src/store/settings/api.basic.ts index 8fc64bf79..4134621de 100644 --- a/packages/ui/certd-client/src/store/settings/api.basic.ts +++ b/packages/ui/certd-client/src/store/settings/api.basic.ts @@ -99,6 +99,7 @@ export type SuiteSetting = { export type SysPrivateSetting = { httpProxy?: string; httpsProxy?: string; + commonHeaders?: string; reverseProxies?: any; dnsResultOrder?: string; commonCnameEnabled?: boolean; diff --git a/packages/ui/certd-client/src/views/sys/settings/tabs/network.vue b/packages/ui/certd-client/src/views/sys/settings/tabs/network.vue index e46614849..b9946267a 100644 --- a/packages/ui/certd-client/src/views/sys/settings/tabs/network.vue +++ b/packages/ui/certd-client/src/views/sys/settings/tabs/network.vue @@ -19,6 +19,11 @@
{{ t("certd.sys.setting.environmentVarsHelper") }}
+ + +
{{ t("certd.sys.setting.commonHeadersHelper") }}
+
+ {{ t("certd.default") }} @@ -64,6 +69,10 @@ const environmentVarsExample = ref( `ALIYUN_CLIENT_CONNECT_TIMEOUT=16000 #连接超时,单位毫秒 ALIYUN_CLIENT_READ_TIMEOUT=16000 #读取数据超时,单位毫秒` ); +const commonHeadersExample = ref( + `User-Agent=certd +X-Custom-Header=value` +); const formState = reactive>({ public: {},