@cloudbase/node-sdk
Version:
tencent cloud base server sdk for node.js
239 lines (196 loc) • 7.47 kB
text/typescript
/* eslint-disable-next-line */
import { parse } from 'url'
import { sign } from '@cloudbase/signature-nodejs'
import { ICloudBaseConfig, ICustomReqOpts } from '../../types'
import { IRequestInfo, IReqOpts } from '../../types/internal'
import { CloudBase } from '../cloudbase'
import { generateTracingInfo, ITracingInfo } from './tracing'
import { checkIsInScf, checkIsInternalAsync, getCurrRunEnvTag } from './cloudplatform'
import { prepareCredentials } from './tcbapirequester'
import { SYMBOL_CURRENT_ENV } from '../const/symbol'
import { extraRequest } from './request'
import { version } from './version'
import * as utils from './utils'
import { buildCommonOpenApiUrlWithPath } from './tcbopenapiendpoint'
const { second } = utils
export function getEnvIdFromContext(): string {
const { TCB_ENV, SCF_NAMESPACE } = CloudBase.getCloudbaseContext()
return TCB_ENV || SCF_NAMESPACE || ''
}
type ITcbOpenApiHttpCommonRequestInfo = Omit<IRequestInfo, 'params'> & {
path?: string
url?: string
token?: string
data: any
}
export class TcbOpenApiHttpCommonRequester {
private readonly args: ITcbOpenApiHttpCommonRequestInfo
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 readonly slowWarnTimer: NodeJS.Timer = null
/* eslint-enable no-undef */
public constructor(args: ITcbOpenApiHttpCommonRequestInfo) {
this.args = args
this.config = args.config
this.opts = args.opts || {}
this.tracingInfo = generateTracingInfo(args.config?.context?.eventID)
}
public async request() {
// 如果没有 accessKey,去检查密钥是否存在,有则直接使用
if (!this.config.accessKey) {
// 检查密钥是否存在
await this.prepareCredentials()
}
const opts = this.makeReqOpts()
// console.log('opts', opts)
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: `${opts.method}:${opts.url}`,
seqId: this.tracingInfo.seqId,
retryOptions,
timingsMeasurerOptions: config.timingsMeasurerOptions || {}
}).then((response) => {
this.slowWarnTimer && clearTimeout(this.slowWarnTimer)
return response
})
}
private makeReqOpts(): IReqOpts {
const config = this.config
const args = this.args
const envId = args.config.envName === SYMBOL_CURRENT_ENV
? getEnvIdFromContext()
: (args.config.envName as string)
const url = args.url || buildCommonOpenApiUrlWithPath({
envId,
path: args.path,
region: config.region
})
const timeout = this.args.opts?.timeout || this.config.timeout || this.defaultTimeout
const opts: IReqOpts = {
url,
method: args.method,
timeout,
headers: this.buildHeaders(args.method, url),
proxy: config.proxy
}
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.data) {
if (['post', 'put', 'patch', 'delete'].includes(args.method.toLowerCase())) {
if (args.isFormData) {
opts.formData = args.data
opts.encoding = null
} else {
opts.body = args.data
if (args.opts?.stream !== true) {
opts.json = true
} else {
// 使用 rawStream 类型返回原始的 Node.js 流,实现真正的流式响应
opts.type = 'rawStream'
}
}
} else {
/* istanbul ignore next */
opts.qs = args.data
}
} else {
opts.noBody = true
}
return opts
}
private async prepareCredentials(): Promise<void> {
prepareCredentials.bind(this)()
}
private buildHeaders(method: string, url: string): any {
const config = this.config
const { context, secretId, secretKey, sessionToken, 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 }
// TODO: 升级SDK版本,否则没传 args.data 时会签名失败
const { authorization, timestamp } = sign({
secretId,
secretKey,
method,
url,
params: args.data || '',
headers: requiredHeaders,
timestamp: second() - 1,
withSignedParams: false,
isCloudApi: true
})
// console.log('xxxx', authorization)
let token = ''
// 如果请求参数里面传了 token,优先使用 token
if (args.token) {
token = makeBearerToken(args.token)
} else if (accessKey) {
// 如果配置了 API_KEY,优先使用 API_KEY
token = makeBearerToken(accessKey)
} else if (typeof sessionToken === 'string' && sessionToken !== '') {
// 如果配置了 sessionToken,携带 sessionToken
token = `${authorization}, Timestamp=${timestamp}, Token=${sessionToken}`
} else {
token = `${authorization}, Timestamp=${timestamp}`
}
/* eslint-disable @typescript-eslint/dot-notation */
requiredHeaders['Authorization'] = token
return { ...requiredHeaders }
}
}
export async function request(args: ITcbOpenApiHttpCommonRequestInfo) {
if (typeof args.isInternal === 'undefined') {
args.isInternal = await checkIsInternalAsync()
}
args.opts = args.opts || {}
args.opts.runEnvTag = await getCurrRunEnvTag()
const requester = new TcbOpenApiHttpCommonRequester(args)
return await requester.request()
}
function makeBearerToken(token: string): string {
const trimmed = token.trim()
return trimmed.startsWith('Bearer ') ? trimmed : `Bearer ${trimmed}`
}