UNPKG

@cloudbase/node-sdk

Version:

tencent cloud base server sdk for node.js

423 lines (357 loc) 13.5 kB
import http from 'http' /* eslint-disable-next-line */ import { parse } from 'url' import { sign } from '@cloudbase/signature-nodejs' import { ICloudBaseConfig, ICustomParam, ICustomReqOpts } from '../../types' import { IRequestInfo, IReqHooks, IReqOpts } from '../../types/internal' import { ERROR } from '../const/code' import { SYMBOL_CURRENT_ENV, SYMBOL_DEFAULT_ENV } from '../const/symbol' import { generateTracingInfo, ITracingInfo } from './tracing' import * as utils from './utils' import { CloudBase } from '../cloudbase' import { checkIsInScf, checkIsInternalAsync, getCurrRunEnvTag } from './cloudplatform' import { buildUrl } from './tcbapiendpoint' import { extraRequest } from './request' import { getWxCloudToken } from './wxCloudToken' import { version } from './version' const { E, second, processReturn } = utils export function getEnvIdFromContext(): string { const { TCB_ENV, SCF_NAMESPACE } = CloudBase.getCloudbaseContext() return TCB_ENV || SCF_NAMESPACE || '' } interface ITencentCloudCredentials { secretId: string secretKey: string sessionToken?: string } export function getCredentialsOnDemand(credentials: ITencentCloudCredentials): ITencentCloudCredentials { const { secretId, secretKey } = credentials let newCredentials: ITencentCloudCredentials = credentials // 原本这里只在SCF云函数环境下,运行支持任意环境通过环境变量传递密钥 if (!secretId || !secretKey) { // 尝试从环境变量中读取 const { TENCENTCLOUD_SECRETID, TENCENTCLOUD_SECRETKEY, TENCENTCLOUD_SESSIONTOKEN } = CloudBase.getCloudbaseContext() if (TENCENTCLOUD_SECRETID && TENCENTCLOUD_SECRETKEY) { newCredentials = { secretId: TENCENTCLOUD_SECRETID, secretKey: TENCENTCLOUD_SECRETKEY, sessionToken: TENCENTCLOUD_SESSIONTOKEN } } // 注意:CBR 环境下,已经禁止该方式获取临时密钥,这里实际是不会成功的 // if (checkIsInCBR()) { // const tmpSecret = await getTmpSecret() // newCredentials = { // secretId: tmpSecret.id, // secretKey: tmpSecret.key, // sessionToken: tmpSecret.token // } // return newCredentials // } // if (await checkIsInTencentCloud()) { // const tmpSecret = await getTmpSecret() // newCredentials = { // secretId: tmpSecret.id, // secretKey: tmpSecret.key, // sessionToken: tmpSecret.token // } // return newCredentials // } } return newCredentials } export async function prepareCredentials(): Promise<void> { const opts = this.opts // CrossAccountInfo: 跨账号调用 const getCrossAccountInfo = opts.getCrossAccountInfo || this.config.getCrossAccountInfo /* istanbul ignore if */ if (getCrossAccountInfo) { const crossAccountInfo = await getCrossAccountInfo() const { credential } = crossAccountInfo const { secretId, secretKey, token } = credential || {} this.config = { ...this.config, secretId, secretKey, sessionToken: token } if (!this.config.secretId || !this.config.secretKey) { throw E({ ...ERROR.INVALID_PARAM, message: 'missing secretId or secretKey of tencent cloud' }) } // 替换掉原函数,缓存数据,这里缓存是否起作用,取决于 this 实例是否复用 // 另一处获取 authorization 的代码可以服用吃这里的缓存 this.opts.getCrossAccountInfo = async () => await Promise.resolve(crossAccountInfo) } else { const { secretId, secretKey, sessionToken } = this.config const credentials = getCredentialsOnDemand({ secretId, secretKey, sessionToken }) this.config = { ...this.config, secretId: credentials.secretId, secretKey: credentials.secretKey, sessionToken: credentials.sessionToken } if (!this.config.secretId || !this.config.secretKey) { throw E({ ...ERROR.INVALID_PARAM, message: 'missing secretId or secretKey of tencent cloud, please set secretId and secretKey in config' }) } } } export class TcbApiHttpRequester { private readonly args: IRequestInfo private readonly config: ICloudBaseConfig private readonly opts: ICustomReqOpts private readonly defaultTimeout = 15000 private readonly timestamp: number = new Date().valueOf() private readonly tracingInfo: ITracingInfo /* eslint-disable no-undef */ private slowWarnTimer: NodeJS.Timer = null /* eslint-enable no-undef */ private readonly hooks: IReqHooks = {} public constructor(args: IRequestInfo) { this.args = args this.config = args.config this.opts = args.opts || {} this.tracingInfo = generateTracingInfo(args.config?.context?.eventID) } public async request(): Promise<any> { // 如果没有配置 accessKey,则通过密钥获取签名,这里先检查密钥是否存在 if (!this.config.accessKey) { // 检查密钥是否存在 await this.prepareCredentials() } const params = await this.makeParams() // console.log('params', params) const opts = this.makeReqOpts(params) // console.log('opts', opts) const action = this.getAction() const key = { functions: 'function_name', database: 'collectionName', wx: 'apiName' }[action.split('.')[0]] const argopts: any = this.opts const config = this.config // 注意:必须初始化为 null let retryOptions: any = null if (argopts.retryOptions) { retryOptions = argopts.retryOptions } else if (config.retries && typeof config.retries === 'number') { retryOptions = { retries: config.retries } } return await extraRequest(opts, { debug: config.debug, op: `${action}:${this.args.params[key]}@${params.envName}`, seqId: this.tracingInfo.seqId, retryOptions, timingsMeasurerOptions: config.timingsMeasurerOptions || {} }).then((response: any) => { this.slowWarnTimer && clearTimeout(this.slowWarnTimer) const { body } = response if (response.statusCode === 200) { let result: any try { result = typeof body === 'string' ? JSON.parse(body) : body if (this.hooks && this.hooks.handleData) { result = this.hooks.handleData(result, null, response, body) } } catch (e) { result = body } return result } else { const e = E({ code: response.statusCode, message: `${response.statusCode} ${http.STATUS_CODES[response.statusCode]} | [${opts.url}]` }) throw e } }) } public setHooks(hooks: IReqHooks) { Object.assign(this.hooks, hooks) } public setSlowWarning(timeout: number) { const action = this.getAction() const { seqId } = this.tracingInfo this.slowWarnTimer = setTimeout(() => { /* istanbul ignore next */ const msg = `[TCB][WARN] Your current request ${action || ''} is longer than 3s, it may be due to the network or your query performance | [${seqId}]` /* istanbul ignore next */ console.warn(msg) }, timeout) } private getAction(): string { return this.args.params.action } private async makeParams(): Promise<any> { const { TCB_SESSIONTOKEN } = CloudBase.getCloudbaseContext() const args = this.args const opts = this.opts const config = this.config const crossAuthorizationData = opts.getCrossAccountInfo && (await opts.getCrossAccountInfo()).authorization const { wxCloudApiToken, wxCloudbaseAccesstoken } = getWxCloudToken() const params: ICustomParam = { ...args.params, envName: config.envName || '', wxCloudApiToken, wxCloudbaseAccesstoken, tcb_sessionToken: TCB_SESSIONTOKEN || '', sessionToken: config.sessionToken, crossAuthorizationToken: crossAuthorizationData ? Buffer.from(JSON.stringify(crossAuthorizationData)).toString('base64') : '' } if (!params.envName) { if (checkIsInScf()) { params.envName = getEnvIdFromContext() console.warn(`[TCB][WARN] 当前未指定env,将默认使用当前函数所在环境的环境:${params.envName}!`) } else { console.warn('[TCB][WARN] 当前未指定env,将默认使用第一个创建的环境!') } } // 取当前云函数环境时,替换为云函数下环境变量 if (params.envName === SYMBOL_CURRENT_ENV) { params.envName = getEnvIdFromContext() } else if (params.envName === SYMBOL_DEFAULT_ENV) { // 这里传空字符串没有可以跟不传的情况做一个区分 params.envName = '' } utils.filterUndefined(params) return params } private makeReqOpts(params: any): IReqOpts { const config = this.config const args = this.args const url = buildUrl({ envId: params.envName || '', region: this.config.region, protocol: this.config.protocol || 'https', serviceUrl: this.config.serviceUrl, seqId: this.tracingInfo.seqId, isInternal: this.args.isInternal }) const method = this.args.method || 'get' const timeout = this.args.opts?.timeout || this.config.timeout || this.defaultTimeout const opts: IReqOpts = { url, method, timeout, // 优先取config,其次取模块,最后取默认 headers: this.getHeaders(method, url, params), proxy: config.proxy, type: this.opts?.type || 'json' } if (typeof config.keepalive === 'undefined' && !checkIsInScf()) { // 非云函数环境下,默认开启 keepalive opts.keepalive = true } else { /** eslint-disable-next-line */ opts.keepalive = typeof config.keepalive === 'boolean' && config.keepalive } if (args.method === 'post') { if (args.isFormData) { opts.formData = params opts.encoding = null } else { opts.body = params opts.json = true } } else { /* istanbul ignore next */ opts.qs = params } return opts } private async prepareCredentials(): Promise<void> { prepareCredentials.bind(this)() } private getHeaders(method: string, url: string, params: any): any { const config = this.config const { context, secretId, secretKey, accessKey } = config const args = this.args const { TCB_SOURCE } = CloudBase.getCloudbaseContext() // Note: 云函数被调用时可能调用端未传递 SOURCE,TCB_SOURCE 可能为空 const SOURCE = `${context?.extendedContext?.source || TCB_SOURCE || ''},${args.opts.runEnvTag}` // 注意:因为 url.parse 和 url.URL 存在差异,因 url.parse 已被废弃,这里可能会需要改动。 // 因 @cloudbase/signature-nodejs sign 方法目前内部使用 url.parse 解析 url, // 如果这里需要改动,需要注意与 @cloudbase/signature-nodejs 的兼容性 // 否则将导致签名存在问题 const parsedUrl = parse(url) // const parsedUrl = new URL(url) let requiredHeaders = { 'User-Agent': `tcb-node-sdk/${version}`, 'X-TCB-Source': SOURCE, 'X-Client-Timestamp': this.timestamp, 'X-SDK-Version': `tcb-node-sdk/${version}`, Host: parsedUrl.host } if (config.version) { requiredHeaders['X-SDK-Version'] = config.version } if (this.tracingInfo.trace) { requiredHeaders['X-TCB-Tracelog'] = this.tracingInfo.trace } const region = this.config.region || process.env.TENCENTCLOUD_REGION || '' if (region) { requiredHeaders['X-TCB-Region'] = region } requiredHeaders = { ...config.headers, ...args.headers, ...requiredHeaders } const { authorization, timestamp } = sign({ secretId, secretKey, method, url, params, headers: requiredHeaders, withSignedParams: true, timestamp: second() - 1 }) /* eslint-disable @typescript-eslint/dot-notation */ // 优先使用 accessKey,否则使用签名 requiredHeaders['Authorization'] = accessKey ? `Bearer ${accessKey}` : authorization requiredHeaders['X-Signature-Expires'] = 600 requiredHeaders['X-Timestamp'] = timestamp return { ...requiredHeaders } } } const handleWxOpenApiData = (res: any, err: any, response: any, body: any): any => { // wx.openApi 调用时,需用content-type区分buffer or JSON const { headers } = response let transformRes = res if (headers['content-type'] === 'application/json; charset=utf-8') { transformRes = JSON.parse(transformRes.toString()) // JSON错误时buffer转JSON } return transformRes } export async function request(args: IRequestInfo): Promise<any> { // console.log('args', args) if (typeof args.isInternal === 'undefined') { args.isInternal = await checkIsInternalAsync() } args.opts = args.opts || {} args.opts.runEnvTag = await getCurrRunEnvTag() const requester = new TcbApiHttpRequester(args) const { action } = args.params if (action === 'wx.openApi' || action === 'wx.wxPayApi') { requester.setHooks({ handleData: handleWxOpenApiData }) } if (action.startsWith('database') && process.env.SILENCE !== 'true') { requester.setSlowWarning(3000) } const result = await requester.request() if (result?.code) { return processReturn(result) } return result }