@cloudbase/node-sdk
Version:
tencent cloud base server sdk for node.js
423 lines (357 loc) • 13.5 kB
text/typescript
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
}