From d61ea86a97e21de53e804fc228da044052a7e103 Mon Sep 17 00:00:00 2001 From: ccharlieli Date: Tue, 16 Feb 2021 11:07:14 +0800 Subject: [PATCH] feat: add fetch domains api --- .coveralls.yml | 2 + .env | 0 .travis.yml | 27 ++++++++ License | 7 ++ README.md | 62 ++++++++++++++++++ package.json | 33 +++++++--- postman/README.md | 0 resources/README.md | 0 src/Apsara.ts | 118 ++++++++++++++++++++++++++++++++++ src/constants.ts | 8 +++ src/data/ApsaraDomainsData.ts | 22 +++++++ src/data/ApsaraErrorData.ts | 7 ++ src/data/ApsaraOptions.ts | 10 +++ src/data/ApsaraParams.ts | 10 +++ src/data/Logger.ts | 5 ++ src/data/index.ts | 5 ++ src/index.ts | 3 + src/test.spec.ts | 87 +++++++++++++++++++++++++ tsconfig.test.json | 2 +- 19 files changed, 397 insertions(+), 11 deletions(-) create mode 100644 .coveralls.yml delete mode 100644 .env create mode 100644 .travis.yml create mode 100644 License delete mode 100644 postman/README.md delete mode 100644 resources/README.md create mode 100644 src/Apsara.ts create mode 100644 src/constants.ts create mode 100644 src/data/ApsaraDomainsData.ts create mode 100644 src/data/ApsaraErrorData.ts create mode 100644 src/data/ApsaraOptions.ts create mode 100644 src/data/ApsaraParams.ts create mode 100644 src/data/Logger.ts create mode 100644 src/data/index.ts create mode 100644 src/test.spec.ts diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..8604cf8 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: travis-pro +repo_token: FbUTUBuan5AgSUOEJU4YoEAlUyhW4orTR \ No newline at end of file diff --git a/.env b/.env deleted file mode 100644 index e69de29..0000000 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d999a53 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +sudo: required +language: node_js +cache: + yarn: true +notifications: + email: false +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 +branches: + only: + - master +env: + global: + - CXX=g++-4.8 + - NODE_ENV="test" +node_js: + - "8" + - "10" + - "12" + - "14" +script: + - yarn test + - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls --verbose \ No newline at end of file diff --git a/License b/License new file mode 100644 index 0000000..7c2fa77 --- /dev/null +++ b/License @@ -0,0 +1,7 @@ +Copyright 2021 ccharlieli@live.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index e69de29..ede5ed0 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,62 @@ +# alicloud-apsara + +This is a library for integrating with AliCloud live-streaming platform - [Apsara](https://www.alibabacloud.com/help/doc-detail/29951.htm?spm=a2c63.p38356.b99.2.2c0d56a2C7EHql). + +Integration progress: +- [x] [get domains](https://www.alibabacloud.com/help/doc-detail/88332.htm?spm=a2c63.p38356.b99.143.17872c80zDTOBs) +- [ ] TBD + +### How to use +```js +import { Apsara, ApsaraDomainsData } from 'alicloud-apsara' + +const apsara = new Apsara({ + accessKeyId: 'keyId', + accessKeySecret: 'keySecret' +}) + +const domainsData: ApsaraDomainsData = await apsara.getDomains() +// Domains: { +// PageData: [ +// { +// Description: '', +// LiveDomainStatus: 'online', +// DomainName: 'live-ingest.upstra-china.cc', +// LiveDomainType: 'liveEdge', +// RegionName: 'ap-southeast-1', +// GmtModified: '2020-09-14T14:31:23Z', +// GmtCreated: '2020-09-14T14:27:06Z', +// Cname: 'live-ingest.upstra-china.cc.w.alikunlun.com' +// } +// ] +// }, +// TotalCount: 8, +// RequestId: '0500D05A-505D-49B1-B989-BDC9FDAB9BAD', +// PageSize: 20, +// PageNumber: 1 +``` + +### Specs + +#### Apsara(options [,logger]) +- **options**, required + - `accessKeyId`: string, required + - `accessKeySecret`: string, required + - `baseUrl`?: string, by default `'https://live.aliyuncs.com'` + - `timeout`?: number, by default `3000` + - `version`?: string, by default `'2016-11-01'` + - `signatureMethod`?: string, by default `'HMAC-SHA1'` + - `signatureVersion`?: string, by default `'1.0'` + - `format`?: string, by default `'json'` +- **logger**, optional, WinstonLogger/BunyanLogger/Console + +#### [Apsara.getDomains()](https://www.alibabacloud.com/help/doc-detail/88332.htm?spm=a2c63.p38356.b99.143.17872c80zDTOBs) + +### How to test + +```sh +yarn +yarn test +``` + +### [MIT License](https://github.com/CCharlieLi/alicloud-apsara/blob/master/License) diff --git a/package.json b/package.json index 7a13144..f378174 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,18 @@ { - "name": "service-template", + "name": "alicloud-apsara", "version": "0.0.1", "private": true, - "description": "", - "keywords": [], + "description": "A library for AliCloud live-streaming platform - Apsara", + "keywords": [ + "AliCloud", + "Aliyun", + "Live", + "Streaming", + "Apsara", + "Video" + ], "author": "Charlie Li ", - "license": "ISC", - "publishConfig": { - "registry": "https://xxx/api/npm/npm-local" - }, + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/CCharlieLi/service-template.git" @@ -45,8 +49,15 @@ "./dist" ] }, - "dependencies": {}, + "dependencies": { + "axios": "^0.21.1", + "crypto": "^1.0.1", + "http-errors": "^1.8.0", + "uuid": "^8.3.2" + }, "devDependencies": { + "@commitlint/cli": "^11.0.0", + "@commitlint/config-angular": "^11.0.0", "@types/accept-language-parser": "1.5.1", "@types/bluebird": "3.5.33", "@types/body-parser": "1.19.0", @@ -55,6 +66,7 @@ "@types/express": "4.17.3", "@types/fs-extra": "8.0.1", "@types/helmet": "0.0.45", + "@types/http-errors": "^1.8.0", "@types/intl": "1.2.0", "@types/ioredis": "4.16.4", "@types/jest": "~25.1.4", @@ -70,6 +82,7 @@ "@types/xml2js": "0.4.5", "@typescript-eslint/eslint-plugin": "~2.23.0", "@typescript-eslint/parser": "~2.23.0", + "coveralls": "^3.1.0", "dotenv": "~8.2.0", "eslint": "~6.8.0", "eslint-config-prettier": "~6.10.0", @@ -78,9 +91,9 @@ "husky": "~4.2.3", "jest": "~25.1.0", "lint-staged": "10.0.8", - "nock": "~11.7.0", + "nock": "^13.0.7", "nockback-harder": "~4.0.1", - "prettier": "~1.19.1", + "prettier": "^2.2.1", "supertest": "~4.0.2", "ts-jest": "~25.2.1", "ts-mockery": "^1.2.0", diff --git a/postman/README.md b/postman/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/resources/README.md b/resources/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/src/Apsara.ts b/src/Apsara.ts new file mode 100644 index 0000000..28999be --- /dev/null +++ b/src/Apsara.ts @@ -0,0 +1,118 @@ +import axios, { AxiosInstance, AxiosResponse } from 'axios' +import { createHmac } from 'crypto' +import createHttpError from 'http-errors' +import { escape } from 'querystring' +import { v4 as uuidv4 } from 'uuid' + +import { API_VERSION, BASE_URL, FORMAT, SIGNATURE_METHOD, SIGNATURE_VERSION, TIMEOUT } from './constants' +import { ApsaraDomainsData } from './data/ApsaraDomainsData' +import { ApsaraErrorData } from './data/ApsaraErrorData' +import { AsparaOptions } from './data/ApsaraOptions' +import { ApsaraParams } from './data/ApsaraParams' +import { Logger } from './data/Logger' + +export class Apsara { + private options: AsparaOptions + private axios: AxiosInstance + private logger?: Logger + + constructor(options: AsparaOptions, logger?: Logger) { + this.options = { + version: API_VERSION, + signatureMethod: SIGNATURE_METHOD, + signatureVersion: SIGNATURE_VERSION, + format: FORMAT, + ...options + } + this.logger = logger + this.axios = axios.create({ + baseURL: options.baseUrl || BASE_URL, + timeout: options.timeout || TIMEOUT + }) + } + + /** + * Fetch all domains from current Apsara account + * See https://www.alibabacloud.com/help/zh/doc-detail/88332.htm?spm=a2c63.p38356.b99.153.6e601545HUfO2U + */ + async getDomains(): Promise { + this.logger?.info(`Get domains of current Apsara account`) + return this.request({ Action: 'DescribeLiveUserDomains' }) + } + + // Private + + /** + * Make HTTP request to Apsara open API + * @param {object} params + */ + private async request(params: Record): Promise { + this.logger?.info(`Send request to Apsara with payload ${JSON.stringify(params)}`) + + // create payload + const method = 'get' + const commonParams: ApsaraParams = { + Format: this.options.format as string, + Version: this.options.version as string, + SignatureMethod: this.options.signatureMethod as string, + SignatureVersion: this.options.signatureVersion as string, + AccessKeyId: this.options.accessKeyId, + SignatureNonce: uuidv4(), + Timestamp: new Date().toISOString() + } + const signature: string = this.generateSignature({ ...commonParams, ...params }, method) + + // init request + let response: AxiosResponse + try { + response = await this.axios({ + method, + params: { + ...commonParams, + ...params, + Signature: signature + } + }) + + const { status, data } = response + this.logger?.info(`Request to Apsara succeed with status ${status} and data ${JSON.stringify(data)}`) + return data as T + } catch (err) { + this.logger?.error(`Request to Apsara failed with response ${JSON.stringify(err.message)}`) + + // Apsara business error + if (err?.response?.data?.RequestId) { + const data: ApsaraErrorData = err.response.data + const status = err.response.status + throw createHttpError(status, data.Message, data) + } + + // Service error + throw createHttpError(err) + } + } + + /** + * Generate signature for Apsara open API + * See https://www.alibabacloud.com/help/zh/doc-detail/50286.htm?spm=a2c63.p38356.b99.145.2b434281pZhqqP + * @param {object} payload + * @param {string} method + */ + private generateSignature(payload: any, method: string): string { + // sort params + const paramsStr = Object.keys(payload) + .map(key => (key === 'Timestamp' ? `${key}=${encodeURIComponent(payload[key])}` : `${key}=${payload[key]}`)) + .sort() + .join('&') + + // sign + return createHmac('sha1', `${this.options.accessKeySecret}&`) + .update( + `${method.toUpperCase()}&${encodeURIComponent('/')}&${escape(paramsStr) + .replace('+', '%20') + .replace('*', '%2A') + .replace('%7E', '~')}` + ) + .digest('base64') + } +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..de4dc3c --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +// https://www.alibabacloud.com/help/zh/doc-detail/50284.htm?spm=a2c63.p38356.b99.144.1cc659c6QMTj5N +export const BASE_URL = 'https://live.aliyuncs.com' +export const TIMEOUT = 3000 // 3s +export const API_VERSION = '2016-11-01' +export const SIGNATURE_METHOD = 'HMAC-SHA1' +export const SIGNATURE_VERSION = '1.0' +export const FORMAT = 'json' +// ResourceOwnerAccount? diff --git a/src/data/ApsaraDomainsData.ts b/src/data/ApsaraDomainsData.ts new file mode 100644 index 0000000..5df59fe --- /dev/null +++ b/src/data/ApsaraDomainsData.ts @@ -0,0 +1,22 @@ +export interface ApsaraDomainsData { + Domains: PageData + TotalCount: number + RequestId: string + PageSize: number + PageNumber: number +} + +interface PageData { + PageData: Domain[] +} + +interface Domain { + Description: string + LiveDomainStatus: string + DomainName: string + LiveDomainType: string + RegionName: string + GmtModified: string + GmtCreated: string + Cname: string +} diff --git a/src/data/ApsaraErrorData.ts b/src/data/ApsaraErrorData.ts new file mode 100644 index 0000000..773fdbb --- /dev/null +++ b/src/data/ApsaraErrorData.ts @@ -0,0 +1,7 @@ +export interface ApsaraErrorData { + RequestId: string + Message: string + Recommend: string + HostId: string + Code: string +} diff --git a/src/data/ApsaraOptions.ts b/src/data/ApsaraOptions.ts new file mode 100644 index 0000000..91fe983 --- /dev/null +++ b/src/data/ApsaraOptions.ts @@ -0,0 +1,10 @@ +export interface AsparaOptions { + baseUrl?: string + timeout?: number + version?: string + signatureMethod?: string + signatureVersion?: string + accessKeyId: string + accessKeySecret: string + format?: string +} diff --git a/src/data/ApsaraParams.ts b/src/data/ApsaraParams.ts new file mode 100644 index 0000000..9539c11 --- /dev/null +++ b/src/data/ApsaraParams.ts @@ -0,0 +1,10 @@ +export interface ApsaraParams { + Format: string + Version: string + SignatureMethod: string + SignatureVersion: string + AccessKeyId: string + Signature?: string + SignatureNonce: string + Timestamp: string +} diff --git a/src/data/Logger.ts b/src/data/Logger.ts new file mode 100644 index 0000000..4589dda --- /dev/null +++ b/src/data/Logger.ts @@ -0,0 +1,5 @@ +export declare class Logger { + info(...meta: any[]): void + warn(...meta: any[]): void + error(...meta: any[]): void +} diff --git a/src/data/index.ts b/src/data/index.ts new file mode 100644 index 0000000..43be0c0 --- /dev/null +++ b/src/data/index.ts @@ -0,0 +1,5 @@ +export * from './ApsaraDomainsData' +export * from './ApsaraErrorData' +export * from './ApsaraOptions' +export * from './ApsaraParams' +export * from './Logger' diff --git a/src/index.ts b/src/index.ts index e69de29..efd5ca1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './Apsara' +export * from './constants' +export * from './data' diff --git a/src/test.spec.ts b/src/test.spec.ts new file mode 100644 index 0000000..b4ee42d --- /dev/null +++ b/src/test.spec.ts @@ -0,0 +1,87 @@ +import nock from 'nock' + +import { Apsara } from './Apsara' +import { BASE_URL } from './constants' +import { ApsaraDomainsData } from './data/ApsaraDomainsData' + +describe('Unit test', () => { + let apsara: Apsara + beforeEach(async () => { + apsara = new Apsara( + { + accessKeyId: '123', + accessKeySecret: '321' + }, + console + ) + }) + + describe('Fetch domains', () => { + it('should fetch domains successfully', async () => { + nock(BASE_URL) + .get(() => true) + .reply(200, { + Domains: { + PageData: [ + { + Description: '', + LiveDomainStatus: 'online', + DomainName: 'live-ingest.upstra-china.cc', + LiveDomainType: 'liveEdge', + RegionName: 'ap-southeast-1', + GmtModified: '2020-09-14T14:31:23Z', + GmtCreated: '2020-09-14T14:27:06Z', + Cname: 'live-ingest.upstra-china.cc.w.alikunlun.com' + } + ] + }, + TotalCount: 8, + RequestId: '0500D05A-505D-49B1-B989-BDC9FDAB9BAD', + PageSize: 20, + PageNumber: 1 + }) + + const res: ApsaraDomainsData = await apsara.getDomains() + expect(res.Domains.PageData.length).toBe(1) + expect(res.RequestId).toBe('0500D05A-505D-49B1-B989-BDC9FDAB9BAD') + }) + + it('should get error when get Apsara business error', async () => { + nock(BASE_URL) + .get(() => true) + .replyWithError({ + name: 'NotFoundError', + message: 'Specified access key is not found.', + RequestId: '35B98711-0E9D-4133-B12F-15C45FA4C55C', + Message: 'Specified access key is not found.', + Recommend: 'https://error-center.aliyun.com/status/search?Keyword=InvalidAccessKeyId.NotFound&source=PopGw', + HostId: 'live.aliyuncs.com', + Code: 'InvalidAccessKeyId.NotFound' + }) + + try { + await apsara.getDomains() + } catch (err) { + expect(err.name).toBe('NotFoundError') + expect(err.message).toBe('Specified access key is not found.') + expect(err.Code).toBe('InvalidAccessKeyId.NotFound') + } + }) + + it('should get error when get service network error', async () => { + nock(BASE_URL) + .get(() => true) + .replyWithError({ + name: 'InternalServerError', + message: 'randome error' + }) + + try { + await apsara.getDomains() + } catch (err) { + expect(err.name).toBe('InternalServerError') + expect(err.message).toBe('randome error') + } + }) + }) +}) diff --git a/tsconfig.test.json b/tsconfig.test.json index b8173c0..efaa34d 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "types": ["reflect-metadata", "jest"] + "types": ["jest"] } } \ No newline at end of file