diff --git a/src/adaptors/api/base/baseBlogApi.ts b/src/adaptors/api/base/baseBlogApi.ts index fcc9408f..4efb059c 100644 --- a/src/adaptors/api/base/baseBlogApi.ts +++ b/src/adaptors/api/base/baseBlogApi.ts @@ -29,27 +29,7 @@ import { createAppLogger, ILogger } from "~/src/utils/appLogger.ts" import { useProxy } from "~/src/composables/useProxy.ts" import { BaseExtendApi } from "~/src/adaptors/base/baseExtendApi.ts" import { JsonUtil, StrUtil } from "zhi-common" - -/** - * 执行代理 fetch 请求 - * - * @param url - 请求的 URL - * @param headers - 请求的头部信息 - * @param params - 请求的参数 - * @param method - 请求的 HTTP 方法 - * @param contentType - 请求的内容类型 - * @param forceProxy - 是否强制使用代理 - * - * @returns 返回一个 Promise,解析为响应结果 - */ -export type ProxyFetchType = ( - url: string, - headers?: any[], - params?: any, - method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", - contentType?: string, - forceProxy?: boolean -) => Promise +import { useSiyuanDevice } from "~/src/composables/useSiyuanDevice.ts" /** * API授权统一封装基类 @@ -63,7 +43,8 @@ export class BaseBlogApi extends BlogApi { protected logger: ILogger protected cfg: BlogConfig protected readonly baseExtendApi: BaseExtendApi - private readonly proxyFetch: ProxyFetchType + private readonly proxyFetch: any + private readonly corsFetch: any /** * 初始化API授权适配器 @@ -79,8 +60,9 @@ export class BaseBlogApi extends BlogApi { this.logger = createAppLogger("base-blog-api") this.baseExtendApi = new BaseExtendApi(this, cfg) - const { proxyFetch } = useProxy(cfg.middlewareUrl) + const { proxyFetch, corsFetch } = useProxy(cfg.middlewareUrl, cfg.corsAnywhereUrl) this.proxyFetch = proxyFetch + this.corsFetch = corsFetch } public async checkAuth(): Promise { @@ -129,10 +111,26 @@ export class BaseBlogApi extends BlogApi { const isCorsProxyAvailable = !StrUtil.isEmptyString(this.cfg.corsAnywhereUrl) // 如果没有可用的 CORS 代理或者没有强制使用代理,使用默认的自动检测机制 if (!isCorsProxyAvailable || !forceProxy) { + this.logger.info("Using legency api fetch") + // const proxyFetch = async ( + // url: string, + // headers: any[] = [], + // params: any = {}, + // method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET", + // contentType: string = "application/json", + // forceProxy: boolean = false + // ) => { this.logger.info("Using legency api fetch") return this.proxyFetch(url, headers, params, method, contentType, forceProxy) } else { - throw new Error("using cors proxy api") + // const corsFetch = async ( + // url: string, + // headers: any[] = [], + // params: BodyInit = undefined, + // method: "GET" | "POST" | "PUT" | "DELETE" = "GET" + // ) + this.logger.info("Using cors api fetch") + return this.corsFetch(url, headers, params, method) } } @@ -149,6 +147,12 @@ export class BaseBlogApi extends BlogApi { // 如果没有可用的 CORS 代理或者没有强制使用代理,使用默认的自动检测机制 if (!isCorsProxyAvailable || !forceProxy) { this.logger.info("Using legency api formFetch") + const { isInSiyuanOrSiyuanNewWin } = useSiyuanDevice() + if (!isInSiyuanOrSiyuanNewWin()) { + throw new Error( + "检测到当前为非 electron 环境并且未设置 cors 代理,此功能将不可用!请设置 cors 代理或者使用PC 客户端" + ) + } const win = this.appInstance.win const doFetch = win.require(`${this.appInstance.moduleBase}libs/zhi-formdata-fetch/index.cjs`) @@ -164,7 +168,8 @@ export class BaseBlogApi extends BlogApi { return resJson } else { - throw new Error("using cors proxy api formFetch") + this.logger.info("Using cors-anywhere api formFetch") + return this.corsFetch(url, headers, formData, "POST") } } diff --git a/src/adaptors/api/telegraph/telegraphApiAdaptor.ts b/src/adaptors/api/telegraph/telegraphApiAdaptor.ts index 2268ae1f..557653d1 100644 --- a/src/adaptors/api/telegraph/telegraphApiAdaptor.ts +++ b/src/adaptors/api/telegraph/telegraphApiAdaptor.ts @@ -204,7 +204,7 @@ class TelegraphApiAdaptor extends BaseBlogApi { this.logger.debug("向 Telegraph 发送表单数据,apiUrl =>", apiUrl) this.logger.debug("向 Telegraph 发送表单数据,options =>", options) - const resJson = await this.apiFormFetch(apiUrl, [headers], formData) + const resJson = await this.apiFormFetch(apiUrl, [headers], formData, true) if (resJson.error) { throw new Error( "telegra.ph 发布错误,注意:切换设备(包括从PC到浏览器环境)需要重新验证,并且获取新token。详细错误 =>" + diff --git a/src/adaptors/api/telegraph/useTelegraphApi.ts b/src/adaptors/api/telegraph/useTelegraphApi.ts index 0a47ae61..c7c01238 100644 --- a/src/adaptors/api/telegraph/useTelegraphApi.ts +++ b/src/adaptors/api/telegraph/useTelegraphApi.ts @@ -63,6 +63,11 @@ const useTelegraphApi = async (key: string, newCfg?: TelegraphConfig) => { // 默认值 cfg.posidKey = getDynPostidKey(key) } + // 初始化corsAnywhereUrl + if (StrUtil.isEmptyString(cfg.corsAnywhereUrl)) { + // 默认值 + cfg.corsAnywhereUrl = Utils.emptyOrDefault(process.env.VITE_CORS_ANYWHERE_URL, CORS_PROXT_URL) + } } cfg.usernameEnabled = true diff --git a/src/adaptors/web/base/baseWebApi.ts b/src/adaptors/web/base/baseWebApi.ts index 1c698c62..a234ad17 100644 --- a/src/adaptors/web/base/baseWebApi.ts +++ b/src/adaptors/web/base/baseWebApi.ts @@ -38,27 +38,7 @@ import { createAppLogger, ILogger } from "~/src/utils/appLogger.ts" import { useProxy } from "~/src/composables/useProxy.ts" import { BaseExtendApi } from "~/src/adaptors/base/baseExtendApi.ts" import { JsonUtil, StrUtil } from "zhi-common" - -/** - * 执行代理 fetch 请求 - * - * @param url - 请求的 URL - * @param headers - 请求的头部信息 - * @param params - 请求的参数 - * @param method - 请求的 HTTP 方法 - * @param contentType - 请求的内容类型 - * @param forceProxy - 是否强制使用代理 - * - * @returns 返回一个 Promise,解析为响应结果 - */ -export type ProxyFetchType = ( - url: string, - headers?: any[], - params?: any, - method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", - contentType?: string, - forceProxy?: boolean -) => Promise +import { useSiyuanDevice } from "~/src/composables/useSiyuanDevice.ts" /** * 网页授权统一封装基类 @@ -72,7 +52,8 @@ class BaseWebApi extends WebApi { protected logger: ILogger protected cfg: WebConfig protected readonly baseExtendApi: BaseExtendApi - private readonly proxyFetch: ProxyFetchType + private readonly proxyFetch: any + private readonly corsFetch: any /** * 初始化网页授权 API 适配器 @@ -88,8 +69,9 @@ class BaseWebApi extends WebApi { this.logger = createAppLogger("base-web-api") this.baseExtendApi = new BaseExtendApi(this, cfg) - const { proxyFetch } = useProxy(cfg.middlewareUrl) + const { proxyFetch, corsFetch } = useProxy(cfg.middlewareUrl, cfg.corsAnywhereUrl) this.proxyFetch = proxyFetch + this.corsFetch = corsFetch } public async checkAuth(): Promise { @@ -200,7 +182,8 @@ class BaseWebApi extends WebApi { this.logger.info("Using legency web fetch") return await this.proxyFetch(url, webHeaders, params, method, contentType, forceProxy) } else { - throw new Error("using cors proxy web") + this.logger.info("Using cors web fetch") + return this.corsFetch(url, headers, params, method) } } @@ -218,6 +201,13 @@ class BaseWebApi extends WebApi { // 如果没有可用的 CORS 代理或者没有强制使用代理,使用默认的自动检测机制 if (!isCorsProxyAvailable || !forceProxy) { this.logger.info("Using legency web formFetch") + const { isInSiyuanOrSiyuanNewWin } = useSiyuanDevice() + if (!isInSiyuanOrSiyuanNewWin()) { + throw new Error( + "检测到当前为非 electron 环境并且未设置 cors 代理,此功能将不可用!请设置 cors 代理或者使用PC 客户端" + ) + } + const win = this.appInstance.win const doFetch = win.require(`${this.appInstance.moduleBase}libs/zhi-formdata-fetch/index.cjs`) @@ -233,7 +223,8 @@ class BaseWebApi extends WebApi { return resJson } else { - throw new Error("using cors proxy web formFetch") + this.logger.info("Using cors-anywhere web formFetch") + return this.corsFetch(url, headers, formData, "POST") } } diff --git a/src/composables/useProxy.ts b/src/composables/useProxy.ts index a2589822..92e54e6d 100644 --- a/src/composables/useProxy.ts +++ b/src/composables/useProxy.ts @@ -26,7 +26,7 @@ import { useSiyuanApi } from "~/src/composables/useSiyuanApi.ts" import { JsonUtil, ObjectUtil, StrUtil } from "zhi-common" import { CommonFetchClient } from "zhi-fetch-middleware" -import { isDev, LEGENCY_SHARED_PROXT_MIDDLEWARE } from "~/src/utils/constants.ts" +import { CORS_PROXT_URL, isDev, LEGENCY_SHARED_PROXT_MIDDLEWARE } from "~/src/utils/constants.ts" import { PublisherAppInstance } from "~/src/publisherAppInstance.ts" import { createAppLogger } from "~/src/utils/appLogger.ts" import { Deserializer, Serializer, XmlrpcUtil } from "simple-xmlrpc" @@ -35,11 +35,12 @@ import { Deserializer, Serializer, XmlrpcUtil } from "simple-xmlrpc" * 用于处理代理请求的自定义 hook * * @param middlewareUrl - 可选,如果使用 CommonFetchClient 需要传递,否则可留空 + * @param corsProxyUrl - 可选,可留空 * @author terwer * @version 1.7.0 * @since 1.7.0 */ -const useProxy = (middlewareUrl?: string) => { +const useProxy = (middlewareUrl?: string, corsProxyUrl?: string) => { const logger = createAppLogger("use-proxy") const { kernelApi, isUseSiyuanProxy } = useSiyuanApi() @@ -49,6 +50,7 @@ const useProxy = (middlewareUrl?: string) => { const appInstance = new PublisherAppInstance() const apiUrl = "" middlewareUrl = middlewareUrl ?? LEGENCY_SHARED_PROXT_MIDDLEWARE + corsProxyUrl = corsProxyUrl ?? CORS_PROXT_URL const commonFetchClient = new CommonFetchClient(appInstance, apiUrl, middlewareUrl, isDev) const serializer = new Serializer(appInstance) @@ -164,7 +166,76 @@ const useProxy = (middlewareUrl?: string) => { return resJson } - return { proxyFetch, proxyXmlrpc } + /** + * 向 Telegraph 发送表单数据 + * + * @param url 请求地址 + * @param headers 请求头,默认为{} + * @param params 表单数据,默认为undefined,支持 ReadableStream、Blob | BufferSource | FormData | URLSearchParams | string + * @param method 请求方法,默认为GET + */ + const corsFetch = async ( + url: string, + headers: any[] = [], + params: BodyInit = undefined, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET" + ) => { + // 如果corsProxyUrl结尾有/,则直接使用,否则在url前加上/ + const apiUrl = `${corsProxyUrl.endsWith("/") ? corsProxyUrl : corsProxyUrl + "/"}${url}` + const header = headers.length > 0 ? headers[0] : {} + + // 处理不安全的 header + const UNSAFE_HEADERS = ["Origin", "Referer", "Cookie"] + const xCorsHeaderString = ObjectUtil.getProperty(header, "x-cors-headers") + let xCorsHeaders = JsonUtil.safeParse(xCorsHeaderString, {}) + for (const [key, value] of Object.entries(header)) { + const lowercaseKey = key.toLowerCase() + if (UNSAFE_HEADERS.map((unsafeHeaderItem) => unsafeHeaderItem.toLowerCase()).includes(lowercaseKey)) { + logger.warn(`corsFetch header ${key} is not allowed`) + xCorsHeaders[key] = value + delete header[key] + } + } + header["x-cors-headers"] = JSON.stringify(xCorsHeaders) + + const options: RequestInit = { + method: method, + headers: header, + body: params, + } + + logger.debug("corsFetch url =>", apiUrl) + logger.debug("corsFetch options =>", options) + + const res = await fetch(apiUrl, options) + + // 处理返回 header + const corsRespHeaders = {} as any + const respHeaderObj = JsonUtil.safeParse(res.headers.get("cors-received-headers"), {}) + for (const [resp_key, resp_value] of Object.entries(respHeaderObj)) { + if (resp_key === "cors-received-headers") { + const corsRecv = respHeaderObj["cors-received-headers"] + for (const [cors_key, cors_value] of Object.entries(corsRecv)) { + corsRespHeaders[cors_key] = cors_value + } + delete respHeaderObj[resp_key] + } else { + corsRespHeaders[resp_key] = resp_value + } + } + logger.debug("corsFetch corsRespHeaders =>", corsRespHeaders) + + const resText = await res.text() + logger.debug("corsFetch resText =>", resText) + + const resJson = JsonUtil.safeParse(resText, {}) + resJson["cors-received-headers"] = JSON.stringify(corsRespHeaders) + logger.debug("corsFetch resJson =>", resJson) + + return resJson + } + + return { proxyFetch, proxyXmlrpc, corsFetch } } export { useProxy }