From 3f90230106e5924401f8e50cfa91958053f8635a Mon Sep 17 00:00:00 2001 From: terwer Date: Fri, 1 Sep 2023 22:10:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20#130=20=E6=94=AF=E6=8C=81=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=88=B0CSDN-=E5=AE=8C=E6=88=90=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/adaptors/index.ts | 21 +++--- src/adaptors/web/base/baseWebApi.ts | 2 +- src/adaptors/web/csdn/csdnUtils.spec.ts | 46 +++++++++++++ src/adaptors/web/csdn/csdnUtils.ts | 87 +++++++++++++++++++++++++ src/adaptors/web/csdn/csdnWebAdaptor.ts | 75 +++++++++++++++++++-- 5 files changed, 216 insertions(+), 15 deletions(-) create mode 100644 src/adaptors/web/csdn/csdnUtils.spec.ts create mode 100644 src/adaptors/web/csdn/csdnUtils.ts diff --git a/src/adaptors/index.ts b/src/adaptors/index.ts index a0ede676..09608b46 100644 --- a/src/adaptors/index.ts +++ b/src/adaptors/index.ts @@ -37,6 +37,7 @@ import { useNotionApi } from "~/src/adaptors/api/notion/useNotionApi.ts" import { useHexoApi } from "~/src/adaptors/api/hexo/useHexoApi.ts" import { CommonBlogConfig } from "~/src/adaptors/api/base/commonBlogConfig.ts" import { useGitlabhexoApi } from "~/src/adaptors/api/gitlab-hexo/useGitlabhexoApi.ts" +import { useCsdnWeb } from "~/src/adaptors/web/csdn/useCsdnWeb.ts" /** * 适配器统一入口 @@ -103,11 +104,11 @@ class Adaptors { conf = cfg break } - // case SubPlatformType.Custom_CSDN: { - // const { cfg } = await useCsdnWeb(key) - // conf = cfg - // break - // } + case SubPlatformType.Custom_CSDN: { + const { cfg } = await useCsdnWeb(key) + conf = cfg + break + } // case SubPlatformType.Custom_Jianshu: { // const { cfg } = await useJianshuWeb(key) // conf = cfg @@ -193,11 +194,11 @@ class Adaptors { blogAdaptor = webApi break } - // case SubPlatformType.Custom_CSDN: { - // const { webApi } = await useCsdnWeb(key, newCfg) - // blogAdaptor = webApi - // break - // } + case SubPlatformType.Custom_CSDN: { + const { webApi } = await useCsdnWeb(key, newCfg) + blogAdaptor = webApi + break + } // case SubPlatformType.Custom_Jianshu: { // const { webApi } = await useJianshuWeb(key, newCfg) // blogAdaptor = webApi diff --git a/src/adaptors/web/base/baseWebApi.ts b/src/adaptors/web/base/baseWebApi.ts index 75fcc4b8..b26c8ddb 100644 --- a/src/adaptors/web/base/baseWebApi.ts +++ b/src/adaptors/web/base/baseWebApi.ts @@ -136,8 +136,8 @@ class BaseWebApi extends WebApi { const header = headers.length > 0 ? headers[0] : {} const webHeaders = [ { - Cookie: this.cfg.password, ...header, + Cookie: this.cfg.password, }, ] return await this.proxyFetch(url, webHeaders, params, method, contentType) diff --git a/src/adaptors/web/csdn/csdnUtils.spec.ts b/src/adaptors/web/csdn/csdnUtils.spec.ts new file mode 100644 index 00000000..8b9984ff --- /dev/null +++ b/src/adaptors/web/csdn/csdnUtils.spec.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023, Terwer . All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Terwer designates this + * particular file as subject to the "Classpath" exception as provided + * by Terwer in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com + * or visit www.terwer.space if you need additional information or have any + * questions. + */ + +import { describe, it } from "vitest" +import CsdnUtils from "~/src/adaptors/web/csdn/csdnUtils.ts" + +describe("test csdnUtils", () => { + it("test generateXCaNonce", () => { + const result = CsdnUtils.generateXCaNonce() + console.log(result) + }) + + it("test generateXCaSignature", () => { + const url = "https://bizapi.csdn.net/blog-console-api/v1/user/info" + const method = "GET" + const accept = "*/*" + + const xCaNonce = CsdnUtils.generateXCaNonce() + const xCaSignature = CsdnUtils.generateXCaSignature(url, method, accept, xCaNonce) + + console.log("x-ca-nonce:", xCaNonce) + console.log("x-ca-signature:", xCaSignature) + }) +}) diff --git a/src/adaptors/web/csdn/csdnUtils.ts b/src/adaptors/web/csdn/csdnUtils.ts new file mode 100644 index 00000000..524cb4dc --- /dev/null +++ b/src/adaptors/web/csdn/csdnUtils.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023, Terwer . All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Terwer designates this + * particular file as subject to the "Classpath" exception as provided + * by Terwer in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com + * or visit www.terwer.space if you need additional information or have any + * questions. + */ + +import Utf8 from "crypto-js/enc-utf8" +import CryptoJS from "crypto-js" +import Base64 from "crypto-js/enc-base64" + +/** + * CSDN工具类,用于生成UUID和签名 + */ +class CsdnUtils { + public static X_CA_KEY = "203803574" + public static APP_SECRET = "9znpamsyl2c7cdrr9sas0le9vbc3r6ba" + + /** + * 生成UUID + * + * @returns 返回生成的UUID + */ + public static generateXCaNonce(): string { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (e) { + const t = (16 * Math.random()) | 0, + n = "x" === e ? t : (3 & t) | 8 + return n.toString(16) + }) + } + + /** + * 获取签名 + * + * @param url - 请求URL + * @param method - HTTP方法 + * @param accept - Accept头 + * @param uuid - UUID + * @param content_type - Content-Type + * @returns 返回签名 + */ + public static generateXCaSignature( + url: string, + method: string, + accept: string, + uuid: string, + content_type?: string | null + ): string { + // https://github.com/brix/crypto-js/issues/189 + // https://www.npmjs.com/package/crypto-js + const s = new URL(url) + const ekey = Utf8.parse(CsdnUtils.APP_SECRET) + let toEnc: string + if (method === "GET") { + const path = s.pathname + s.search + toEnc = `GET\n${accept}\n\n\n\nx-ca-key:${CsdnUtils.X_CA_KEY}\nx-ca-nonce:${uuid}\n${path}` + } else { + const path = s.pathname + toEnc = `POST\n${accept}\n\n${content_type}\n\nx-ca-key:${CsdnUtils.X_CA_KEY}\nx-ca-nonce:${uuid}\n${path}` + } + const hmac = CryptoJS.HmacSHA256(toEnc, ekey) + const sign = Base64.stringify(hmac) + // console.log(uuid) + // console.log(sign) + return sign + } +} + +export default CsdnUtils diff --git a/src/adaptors/web/csdn/csdnWebAdaptor.ts b/src/adaptors/web/csdn/csdnWebAdaptor.ts index f84b8bc5..32603ab2 100644 --- a/src/adaptors/web/csdn/csdnWebAdaptor.ts +++ b/src/adaptors/web/csdn/csdnWebAdaptor.ts @@ -24,6 +24,12 @@ */ import { BaseWebApi } from "~/src/adaptors/web/base/baseWebApi.ts" +import CsdnUtils from "~/src/adaptors/web/csdn/csdnUtils.ts" +import { CsdnConfig } from "~/src/adaptors/web/csdn/csdnConfig.ts" +import { AppInstance } from "~/src/appInstance.ts" +import { createAppLogger } from "~/src/utils/appLogger.ts" +import { CommonFetchClient } from "zhi-fetch-middleware" +import { isDev } from "~/src/utils/constants.ts" /** * CSDN网页授权适配器 @@ -34,15 +40,32 @@ import { BaseWebApi } from "~/src/adaptors/web/base/baseWebApi.ts" * @since 0.9.0 */ class CsdnWebAdaptor extends BaseWebApi { + private readonly commonFetchClient: any + + /** + * 初始化知乎 API 适配器 + * + * @param appInstance 应用实例 + * @param cfg 配置项 + */ + constructor(appInstance: AppInstance, cfg: CsdnConfig) { + super(appInstance, cfg) + this.cfg = cfg + + const middlewareUrl = this.cfg.middlewareUrl ?? "https://api.terwer.space/api/middleware" + this.commonFetchClient = new CommonFetchClient(appInstance, "", middlewareUrl, isDev) + this.logger = createAppLogger("zhihu-web-adaptor") + } + public async getMetaData(): Promise { - const res = await this.proxyFetch("https://bizapi.csdn.net/blog-console-api/v1/user/info") - const flag = !!res.data.csdnid + const res = await this.csdnFetch("https://bizapi.csdn.net/blog-console-api/v1/user/info") + const flag = !!res.data.username this.logger.info(`get csdn metadata finished, flag => ${flag}`) return { flag: flag, - uid: res.data.csdnid, + uid: res.data.username, title: res.data.username, - avatar: res.data.avatarurl, + avatar: res.data.avatar, type: "csdn", displayName: "CSDN", supportTypes: ["markdown", "html"], @@ -50,6 +73,50 @@ class CsdnWebAdaptor extends BaseWebApi { icon: "https://g.csdnimg.cn/static/logo/favicon32.ico", } } + + // ================ + // private methods + // ================ + private async csdnFetch( + url: string, + headers: any = {}, + params: any = undefined, + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET", + contentType?: string | null + ) { + // 设置请求头 + const accept = "*/*" + const APPLICATION_JSON = "application/json" + const xcakey = CsdnUtils.X_CA_KEY + const xCaNonce = CsdnUtils.generateXCaNonce() + const xCaSignature = CsdnUtils.generateXCaSignature(url, method, accept, xCaNonce, contentType) + + const reqHeader = { + accept, + ...(contentType ? { "content-type": contentType } : {}), + "x-ca-key": xcakey, + "x-ca-nonce": xCaNonce, + "x-ca-signature": xCaSignature, + "x-ca-signature-headers": "x-ca-key,x-ca-nonce", + ...headers, + Cookie: this.cfg.password, + } + + // 构建请求选项 + const requestOptions: RequestInit = { + method: method, + headers: reqHeader, + body: params, + redirect: "follow", + } + + // 发送请求并返回响应 + const res = await this.commonFetchClient.fetchCall(url, requestOptions) + if (res?.code !== 200) { + throw new Error(res?.body?.message) + } + return res + } } export { CsdnWebAdaptor }