From c764fef8dd96fbee19b0e1444cbe3d880e1e3f43 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 9 Mar 2022 21:48:27 +0000 Subject: [PATCH 01/11] Fix lint --- .gitignore | 2 +- package.json | 3 ++- src/server_manager/cloud/gcp_api.ts | 16 ++++++++-------- src/server_manager/web_app/app.ts | 2 +- src/server_manager/web_app/cloud_accounts.ts | 2 ++ .../web_app/data_formatting.spec.ts | 1 - src/server_manager/web_app/survey.spec.ts | 1 - tsconfig.json | 1 + tslint.json | 1 - 9 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 629f93e1..491ca9d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .DS_Store -/build +/build/ node_modules/ /src/server_manager/install_scripts/do_install_script.ts /src/server_manager/install_scripts/gcp_install_script.ts diff --git a/package.json b/package.json index eb183586..80ba7330 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "clean": "rm -rf src/*/node_modules/ build/ node_modules/ src/server_manager/install_scripts/do_install_script.ts src/server_manager/install_scripts/gcp_install_script.ts third_party/shellcheck/download/", "lint": "npm run lint:sh && npm run lint:ts", "lint:sh": "bash ./scripts/shellcheck.sh", - "lint:ts": "npx tslint 'src/**/*.ts' -e '**/node_modules/**'" + "lint:ts": "npx tslint 'src/**/*.ts' -e '**/node_modules/**'", + "test": "npm run lint && npm run action metrics_server/test && npm run action sentry_webhook/build && npm run action server_manager/test && npm run action shadowbox/test" }, "workspaces": [ "src/*" diff --git a/src/server_manager/cloud/gcp_api.ts b/src/server_manager/cloud/gcp_api.ts index 60d49f37..ada9afc2 100644 --- a/src/server_manager/cloud/gcp_api.ts +++ b/src/server_manager/cloud/gcp_api.ts @@ -579,7 +579,6 @@ export class RestApiClient { * @see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims */ getUserInfo(): Promise { - const parameters = new Map([['access_token', this.accessToken]]); return this.fetchAuthenticated( 'POST', new URL('https://openidconnect.googleapis.com/v1/userinfo'), this.GCP_HEADERS); } @@ -607,13 +606,14 @@ export class RestApiClient { * * @param token - A refresh token or access token */ - private async revokeGcpToken(token: string): Promise { - const headers = new Map( - [['Host', 'oauth2.googleapis.com'], ['Content-Type', 'application/x-www-form-urlencoded']]); - const parameters = new Map([['token', token]]); - return this.fetchUnauthenticated( - 'GET', new URL('https://oauth2.googleapis.com/revoke'), headers, parameters); - } + // TODO(fortuna): use this to revoke the access token on account disconnection. + // private async revokeGcpToken(token: string): Promise { + // const headers = new Map( + // [['Host', 'oauth2.googleapis.com'], ['Content-Type', 'application/x-www-form-urlencoded']]); + // const parameters = new Map([['token', token]]); + // return this.fetchUnauthenticated( + // 'GET', new URL('https://oauth2.googleapis.com/revoke'), headers, parameters); + // } // tslint:disable-next-line:no-any private async fetchAuthenticated(method: string, url: URL, headers: Map, parameters?: Map, data?: any): Promise { diff --git a/src/server_manager/web_app/app.ts b/src/server_manager/web_app/app.ts index db4123ca..53286ac3 100644 --- a/src/server_manager/web_app/app.ts +++ b/src/server_manager/web_app/app.ts @@ -35,7 +35,7 @@ import type {DisplayAccessKey, ServerView} from './ui_components/outline-server- // The Outline DigitalOcean team's referral code: // https://www.digitalocean.com/help/referral-program/ -const UNUSED_DIGITALOCEAN_REFERRAL_CODE = '5ddb4219b716'; +//const UNUSED_DIGITALOCEAN_REFERRAL_CODE = '5ddb4219b716'; const CHANGE_KEYS_PORT_VERSION = '1.0.0'; const DATA_LIMITS_VERSION = '1.1.0'; diff --git a/src/server_manager/web_app/cloud_accounts.ts b/src/server_manager/web_app/cloud_accounts.ts index 9b0a946b..b88ecf44 100644 --- a/src/server_manager/web_app/cloud_accounts.ts +++ b/src/server_manager/web_app/cloud_accounts.ts @@ -60,12 +60,14 @@ export class CloudAccounts implements accounts.CloudAccounts { /** See {@link CloudAccounts#disconnectDigitalOceanAccount} */ disconnectDigitalOceanAccount(): void { + // TODO(fortuna): Revoke access token. this.digitalOceanAccount = null; this.save(); } /** See {@link CloudAccounts#disconnectGcpAccount} */ disconnectGcpAccount(): void { + // TODO(fortuna): Revoke access token. this.gcpAccount = null; this.save(); } diff --git a/src/server_manager/web_app/data_formatting.spec.ts b/src/server_manager/web_app/data_formatting.spec.ts index 63d1cdd4..fae6d978 100644 --- a/src/server_manager/web_app/data_formatting.spec.ts +++ b/src/server_manager/web_app/data_formatting.spec.ts @@ -14,7 +14,6 @@ limitations under the License. */ -import {format} from 'path'; import * as formatting from './data_formatting'; describe('formatBytesParts', () => { diff --git a/src/server_manager/web_app/survey.spec.ts b/src/server_manager/web_app/survey.spec.ts index 6ad9d562..bc96e15f 100644 --- a/src/server_manager/web_app/survey.spec.ts +++ b/src/server_manager/web_app/survey.spec.ts @@ -13,7 +13,6 @@ // limitations under the License. import {InMemoryStorage} from '../infrastructure/memory_storage'; -import {Surveys} from '../model/survey'; import {OutlineSurveys} from './survey'; diff --git a/tsconfig.json b/tsconfig.json index ed55b99a..a7ffe792 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,5 +9,6 @@ "experimentalDecorators":true, "allowJs": true, "resolveJsonModule": true, + "noUnusedLocals": true, } } diff --git a/tslint.json b/tslint.json index 32abeaea..c46f0724 100644 --- a/tslint.json +++ b/tslint.json @@ -25,7 +25,6 @@ "no-reference": true, "no-require-imports": true, "no-unused-expression": true, - "no-unused-variable": [true, {"ignore-pattern": "^UNUSED"}], "no-use-before-declare": false, "no-var-keyword": true, "object-literal-shorthand": true, From 3034e59f9f7e80fbfaa93bad6176bad17f06a92f Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 9 Mar 2022 23:10:15 +0000 Subject: [PATCH 02/11] Fix lint --- .editorconfig | 10 + .prettierignore | 3 + .prettierrc | 5 + CONTRIBUTING.md | 2 +- README.md | 21 +- jasmine.json | 4 +- package.json | 3 +- src/metrics_server/README.md | 39 +- src/metrics_server/connection_metrics.spec.ts | 119 ++--- src/metrics_server/connection_metrics.ts | 32 +- src/metrics_server/feature_metrics.spec.ts | 50 ++- src/metrics_server/feature_metrics.ts | 27 +- src/metrics_server/index.ts | 8 +- src/metrics_server/tsconfig.json | 4 +- src/sentry_webhook/README.md | 33 +- src/sentry_webhook/index.ts | 23 +- .../post_sentry_event_to_salesforce.ts | 60 ++- src/sentry_webhook/tsconfig.json | 4 +- src/shadowbox/README.md | 24 +- src/shadowbox/infrastructure/clock.ts | 2 +- src/shadowbox/infrastructure/file.spec.ts | 34 +- .../infrastructure/follow_redirects.ts | 4 +- src/shadowbox/infrastructure/json_config.ts | 1 - src/shadowbox/infrastructure/logging.ts | 7 +- .../infrastructure/prometheus_scraper.ts | 80 ++-- .../integration_test/target/index.html | 4 +- src/shadowbox/model/access_key.ts | 2 +- src/shadowbox/model/metrics.ts | 4 +- src/shadowbox/server/main.ts | 106 +++-- src/shadowbox/server/manager_metrics.spec.ts | 3 +- src/shadowbox/server/manager_metrics.ts | 6 +- src/shadowbox/server/manager_service.spec.ts | 411 ++++++++++-------- src/shadowbox/server/manager_service.ts | 170 +++++--- src/shadowbox/server/mocks/mocks.ts | 6 +- .../server/outline_shadowsocks_server.ts | 14 +- .../server/server_access_key.spec.ts | 144 +++--- src/shadowbox/server/server_access_key.ts | 77 ++-- src/shadowbox/server/shared_metrics.spec.ts | 100 +++-- src/shadowbox/server/shared_metrics.ts | 35 +- src/shadowbox/shadowbox_config.json | 2 +- src/shadowbox/tsconfig.json | 13 +- src/shadowbox/types/node.d.ts | 10 +- src/shadowbox/webpack.config.js | 6 +- third_party/shellcheck/README.md | 15 +- tsconfig.json | 4 +- tslint.json | 3 +- 46 files changed, 1058 insertions(+), 676 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..417d89a9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..d250d45c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +/build/ +node_modules/ +/src/server_manager/messages/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..de41e34c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "bracketSpacing": false, + "printWidth": 100 +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d364e1d..ae319c70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,4 +20,4 @@ again. All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. \ No newline at end of file +information on using pull requests. diff --git a/README.md b/README.md index d591f1cd..c9a3758b 100644 --- a/README.md +++ b/README.md @@ -29,35 +29,35 @@ The system comprises the following components: See [`src/metrics_server`](src/metrics_server) - ## Code Prerequisites In order to build and run the code, you need the following installed: - - [Node](https://nodejs.org/en/download/) LTS (`lts/gallium`, version `16.13.0`) - - [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (version `8.1.0`) - - Manager-specic - - [Wine](https://www.winehq.org/download), if you would like to generate binaries for Windows. - - Server-specific - - [Docker](https://docs.docker.com/engine/install/), to build the Docker image and to run the integration test. - - [docker-compose](https://docs.docker.com/compose/install/), to run the integration test. +- [Node](https://nodejs.org/en/download/) LTS (`lts/gallium`, version `16.13.0`) +- [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (version `8.1.0`) +- Manager-specic + - [Wine](https://www.winehq.org/download), if you would like to generate binaries for Windows. +- Server-specific + - [Docker](https://docs.docker.com/engine/install/), to build the Docker image and to run the integration test. + - [docker-compose](https://docs.docker.com/compose/install/), to run the integration test. > 💡 NOTE: if you have `nvm` installed, run `nvm use` to switch to the correct node version! Install dependencies with: + ```sh npm install ``` This project uses [NPM workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces/). - ## Build System We have a very simple build system based on package.json scripts that are called using `npm run` and a thin wrapper for what we call build "actions". We've defined a package.json script called `action` whose parameter is a relative path: + ```shell npm run action $ACTION ``` @@ -77,6 +77,7 @@ It also defines two environmental variables: ### Build output Building creates the following directories under `build/`: + - `web_app/`: The Manager web app. - `static/`: The standalone web app static files. This is what one deploys to a web server or runs with Electron. - `electron_app/`: The launcher desktop Electron app @@ -88,11 +89,13 @@ Building creates the following directories under `build/`: - `shadowbox`: The Proxy Server The directories have subdirectories for intermediate output: + - `ts/`: Autogenerated Typescript files - `js/`: The output from compiling Typescript code - `browserified/`: The output of browserifying the JavaScript code To clean up: + ``` npm run clean ``` diff --git a/jasmine.json b/jasmine.json index 2b46020b..e35b0376 100644 --- a/jasmine.json +++ b/jasmine.json @@ -1,8 +1,6 @@ { "spec_dir": ".", - "spec_files": [ - "build/js/**/*.spec.js" - ], + "spec_files": ["build/js/**/*.spec.js"], "stopSpecOnExpectationFailure": false, "random": false } diff --git a/package.json b/package.json index 80ba7330..052d10c6 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "action:help": "npm run action", "action:list": "npm run action", "clean": "rm -rf src/*/node_modules/ build/ node_modules/ src/server_manager/install_scripts/do_install_script.ts src/server_manager/install_scripts/gcp_install_script.ts third_party/shellcheck/download/", + "format": "prettier \"**/*.{cjs,html,js,json,md,ts}\" --write", "lint": "npm run lint:sh && npm run lint:ts", "lint:sh": "bash ./scripts/shellcheck.sh", "lint:ts": "npx tslint 'src/**/*.ts' -e '**/node_modules/**'", @@ -31,7 +32,7 @@ ], "husky": { "hooks": { - "pre-commit": "npm run lint && npx git-clang-format && npx pretty-quick --staged --pattern '**/*.html'" + "pre-commit": "npm run lint && npx git-clang-format && npx pretty-quick --staged --pattern \"**/*.{cjs,html,js,json,md,ts}\"" } } } diff --git a/src/metrics_server/README.md b/src/metrics_server/README.md index 5db3242e..c4a0298f 100644 --- a/src/metrics_server/README.md +++ b/src/metrics_server/README.md @@ -12,7 +12,7 @@ The metrics server deploys two services: `dev`, used for development testing and The metrics server supports two URL paths: -* `POST /connections`: report server data usage broken down by user. +- `POST /connections`: report server data usage broken down by user. ``` { @@ -26,23 +26,24 @@ The metrics server supports two URL paths: }] } ``` -* `POST /features`: report feature usage. - ``` - { - serverId: string, - serverVersion: string, - timestampUtcMs: number, - dataLimit: { - enabled: boolean - perKeyLimitCount: number - } - } - ``` +- `POST /features`: report feature usage. + + ``` + { + serverId: string, + serverVersion: string, + timestampUtcMs: number, + dataLimit: { + enabled: boolean + perKeyLimitCount: number + } + } + ``` ## Requirements -* [Google Cloud SDK](https://cloud.google.com/sdk/) +- [Google Cloud SDK](https://cloud.google.com/sdk/) ## Build @@ -60,26 +61,26 @@ npm run action metrics_server/start ## Deploy -* Authenticate with `gcloud`: +- Authenticate with `gcloud`: ```sh gcloud auth login ``` -* To deploy to dev: +- To deploy to dev: ```sh npm run action metrics_server/deploy_dev ``` -* To deploy to prod: +- To deploy to prod: ```sh npm run action metrics_server/deploy_prod ``` ## Test -* Unit test +- Unit test ```sh npm run action metrics_server/test ``` -* Integration test +- Integration test ```sh npm run action metrics_server/test_integration ``` diff --git a/src/metrics_server/connection_metrics.spec.ts b/src/metrics_server/connection_metrics.spec.ts index 062ac60d..5e611482 100644 --- a/src/metrics_server/connection_metrics.spec.ts +++ b/src/metrics_server/connection_metrics.spec.ts @@ -12,12 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ConnectionRow, isValidConnectionMetricsReport, postConnectionMetrics} from './connection_metrics'; +import { + ConnectionRow, + isValidConnectionMetricsReport, + postConnectionMetrics, +} from './connection_metrics'; import {InsertableTable} from './infrastructure/table'; import {HourlyConnectionMetricsReport} from './model'; class FakeConnectionsTable implements InsertableTable { - public rows: ConnectionRow[]|undefined; + public rows: ConnectionRow[] | undefined; async insert(rows: ConnectionRow[]) { this.rows = rows; @@ -37,7 +41,7 @@ describe('postConnectionMetrics', () => { userId: 'uid1', countries: ['EC'], bytesTransferred: 456, - } + }, ]; const report = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; await postConnectionMetrics(table, report); @@ -48,7 +52,7 @@ describe('postConnectionMetrics', () => { endTimestamp: new Date(report.endUtcMs).toISOString(), userId: userReports[0].userId, bytesTransferred: userReports[0].bytesTransferred, - countries: userReports[0].countries + countries: userReports[0].countries, }, { serverId: report.serverId, @@ -56,8 +60,8 @@ describe('postConnectionMetrics', () => { endTimestamp: new Date(report.endUtcMs).toISOString(), userId: userReports[1].userId, bytesTransferred: userReports[1].bytesTransferred, - countries: userReports[1].countries - } + countries: userReports[1].countries, + }, ]; expect(table.rows).toEqual(rows); }); @@ -67,7 +71,7 @@ describe('isValidConnectionMetricsReport', () => { it('returns true for valid report', () => { const userReports = [ {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, - {userId: 'uid1', countries: ['EC'], bytesTransferred: 456} + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, ]; const report = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; expect(isValidConnectionMetricsReport(report)).toBeTruthy(); @@ -78,13 +82,13 @@ describe('isValidConnectionMetricsReport', () => { it('returns false for inconsistent timestamp values', () => { const userReports = [ {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, - {userId: 'uid1', countries: ['EC'], bytesTransferred: 456} + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, ]; const invalidReport = { serverId: 'id', - startUtcMs: 999, // startUtcMs > endUtcMs + startUtcMs: 999, // startUtcMs > endUtcMs endUtcMs: 1, - userReports + userReports, }; expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); }); @@ -93,19 +97,20 @@ describe('isValidConnectionMetricsReport', () => { { userId: 'uid0', countries: ['US', 'UK'], - bytesTransferred: -123 // Should not be negative + bytesTransferred: -123, // Should not be negative }, - {userId: 'uid1', countries: ['EC'], bytesTransferred: 456} + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, ]; const invalidReport = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); const userReports2 = [ - {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, { + {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, + { userId: 'uid1', countries: ['EC'], - bytesTransferred: 2 * Math.pow(2, 40) // 2TB is above the server capacity - } + bytesTransferred: 2 * Math.pow(2, 40), // 2TB is above the server capacity + }, ]; const invalidReport2 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: userReports2}; expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); @@ -123,19 +128,19 @@ describe('isValidConnectionMetricsReport', () => { serverId: 'id', startUtcMs: 1, endUtcMs: 2, - userReports: [] // Should not be empty + userReports: [], // Should not be empty }; expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); const userReports = [ {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, - {userId: 'uid1', countries: ['EC'], bytesTransferred: 456} + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, ]; const invalidReport3 = { // Missing `serverId` startUtcMs: 1, endUtcMs: 2, - userReports + userReports, }; expect(isValidConnectionMetricsReport(invalidReport3)).toBeFalsy(); @@ -143,7 +148,7 @@ describe('isValidConnectionMetricsReport', () => { // Missing `startUtcMs` serverId: 'id', endUtcMs: 2, - userReports + userReports, }; expect(isValidConnectionMetricsReport(invalidReport4)).toBeFalsy(); @@ -151,7 +156,7 @@ describe('isValidConnectionMetricsReport', () => { // Missing `endUtcMs` serverId: 'id', startUtcMs: 2, - userReports + userReports, }; expect(isValidConnectionMetricsReport(invalidReport5)).toBeFalsy(); }); @@ -160,26 +165,30 @@ describe('isValidConnectionMetricsReport', () => { { // Missing `userId` countries: ['US', 'UK'], - bytesTransferred: 123 + bytesTransferred: 123, }, - {userId: 'uid1', countries: ['EC'], bytesTransferred: 456} + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, ]; const invalidReport = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); - const userReports2 = [{ - // Missing `countries` - userId: 'uid0', - bytesTransferred: 123 - }]; + const userReports2 = [ + { + // Missing `countries` + userId: 'uid0', + bytesTransferred: 123, + }, + ]; const invalidReport2 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: userReports2}; expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); - const userReports3 = [{ - // Missing `bytesTransferred` - userId: 'uid0', - countries: ['US', 'UK'], - }]; + const userReports3 = [ + { + // Missing `bytesTransferred` + userId: 'uid0', + countries: ['US', 'UK'], + }, + ]; const invalidReport3 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: userReports3}; expect(isValidConnectionMetricsReport(invalidReport3)).toBeFalsy(); }); @@ -188,27 +197,27 @@ describe('isValidConnectionMetricsReport', () => { serverId: 'id', startUtcMs: 1, endUtcMs: 2, - userReports: [1, 2, 3] // Should be `HourlyUserConnectionMetricsReport[]` + userReports: [1, 2, 3], // Should be `HourlyUserConnectionMetricsReport[]` }; expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); const userReports = [ {userId: 'uid0', countries: ['US', 'UK'], bytesTransferred: 123}, - {userId: 'uid1', countries: ['EC'], bytesTransferred: 456} + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, ]; const invalidReport2 = { - serverId: 987, // Should be a string + serverId: 987, // Should be a string startUtcMs: 1, endUtcMs: 2, - userReports + userReports, }; expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); const invalidReport3 = { serverId: 'id', - startUtcMs: '100', // Should be a number + startUtcMs: '100', // Should be a number endUtcMs: 200, - userReports + userReports, }; expect(isValidConnectionMetricsReport(invalidReport3)).toBeFalsy(); @@ -216,36 +225,40 @@ describe('isValidConnectionMetricsReport', () => { // Missing `startUtcMs` serverId: 'id', startUtcMs: 1, - endUtcMs: '200', // Should be a number - userReports + endUtcMs: '200', // Should be a number + userReports, }; expect(isValidConnectionMetricsReport(invalidReport4)).toBeFalsy(); }); it('returns false for incorrect user report field types ', () => { const userReports = [ { - userId: 1234, // Should be a string + userId: 1234, // Should be a string countries: ['US', 'UK'], - bytesTransferred: 123 + bytesTransferred: 123, }, - {userId: 'uid1', countries: ['EC'], bytesTransferred: 456} + {userId: 'uid1', countries: ['EC'], bytesTransferred: 456}, ]; const invalidReport = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports}; expect(isValidConnectionMetricsReport(invalidReport)).toBeFalsy(); - const userReports2 = [{ - userId: 'uid0', - countries: [1, 2, 3], // Should be string[] - bytesTransferred: 123 - }]; + const userReports2 = [ + { + userId: 'uid0', + countries: [1, 2, 3], // Should be string[] + bytesTransferred: 123, + }, + ]; const invalidReport2 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: userReports2}; expect(isValidConnectionMetricsReport(invalidReport2)).toBeFalsy(); - const userReports3 = [{ - userId: 'uid0', - countries: ['US', 'UK'], - bytesTransferred: '1234', // Should be a number - }]; + const userReports3 = [ + { + userId: 'uid0', + countries: ['US', 'UK'], + bytesTransferred: '1234', // Should be a number + }, + ]; const invalidReport3 = {serverId: 'id', startUtcMs: 1, endUtcMs: 2, userReports: userReports3}; expect(isValidConnectionMetricsReport(invalidReport3)).toBeFalsy(); }); diff --git a/src/metrics_server/connection_metrics.ts b/src/metrics_server/connection_metrics.ts index a08fb249..4469c56e 100644 --- a/src/metrics_server/connection_metrics.ts +++ b/src/metrics_server/connection_metrics.ts @@ -18,8 +18,8 @@ import {HourlyConnectionMetricsReport, HourlyUserConnectionMetricsReport} from ' export interface ConnectionRow { serverId: string; - startTimestamp: string; // ISO formatted string. - endTimestamp: string; // ISO formatted string. + startTimestamp: string; // ISO formatted string. + endTimestamp: string; // ISO formatted string. userId: string; bytesTransferred: number; countries: string[]; @@ -34,7 +34,9 @@ export class BigQueryConnectionsTable implements InsertableTable } export function postConnectionMetrics( - table: InsertableTable, report: HourlyConnectionMetricsReport) { + table: InsertableTable, + report: HourlyConnectionMetricsReport +) { return table.insert(getConnectionRowsFromReport(report)); } @@ -49,7 +51,7 @@ function getConnectionRowsFromReport(report: HourlyConnectionMetricsReport): Con endTimestamp: endTimestampStr, userId: userReport.userId, bytesTransferred: userReport.bytesTransferred, - countries: userReport.countries + countries: userReport.countries, }); } return rows; @@ -57,8 +59,9 @@ function getConnectionRowsFromReport(report: HourlyConnectionMetricsReport): Con // Returns true iff testObject contains a valid HourlyConnectionMetricsReport. // tslint:disable-next-line:no-any -export function isValidConnectionMetricsReport(testObject: any): - testObject is HourlyConnectionMetricsReport { +export function isValidConnectionMetricsReport( + testObject: any +): testObject is HourlyConnectionMetricsReport { if (!testObject) { return false; } @@ -77,8 +80,11 @@ export function isValidConnectionMetricsReport(testObject: any): } // Check timestamp types and that startUtcMs is not after endUtcMs. - if (typeof testObject.startUtcMs !== 'number' || typeof testObject.endUtcMs !== 'number' || - testObject.startUtcMs >= testObject.endUtcMs) { + if ( + typeof testObject.startUtcMs !== 'number' || + typeof testObject.endUtcMs !== 'number' || + testObject.startUtcMs >= testObject.endUtcMs + ) { return false; } @@ -89,7 +95,7 @@ export function isValidConnectionMetricsReport(testObject: any): const requiredUserReportFields = ['userId', 'countries', 'bytesTransferred']; const MIN_BYTES_TRANSFERRED = 0; - const MAX_BYTES_TRANSFERRED = 1 * Math.pow(2, 40); // 1 TB. + const MAX_BYTES_TRANSFERRED = 1 * Math.pow(2, 40); // 1 TB. for (const userReport of testObject.userReports) { // Test that each `userReport` contains the required fields. for (const fieldName of requiredUserReportFields) { @@ -103,9 +109,11 @@ export function isValidConnectionMetricsReport(testObject: any): } // Check that `bytesTransferred` is a number between min and max transfer limits - if (typeof userReport.bytesTransferred !== 'number' || - userReport.bytesTransferred < MIN_BYTES_TRANSFERRED || - userReport.bytesTransferred > MAX_BYTES_TRANSFERRED) { + if ( + typeof userReport.bytesTransferred !== 'number' || + userReport.bytesTransferred < MIN_BYTES_TRANSFERRED || + userReport.bytesTransferred > MAX_BYTES_TRANSFERRED + ) { return false; } diff --git a/src/metrics_server/feature_metrics.spec.ts b/src/metrics_server/feature_metrics.spec.ts index 4f766221..d7cab022 100644 --- a/src/metrics_server/feature_metrics.spec.ts +++ b/src/metrics_server/feature_metrics.spec.ts @@ -17,7 +17,7 @@ import {InsertableTable} from './infrastructure/table'; import {DailyFeatureMetricsReport} from './model'; class FakeFeaturesTable implements InsertableTable { - public rows: FeatureRow[]|undefined; + public rows: FeatureRow[] | undefined; async insert(rows: FeatureRow[]) { this.rows = rows; @@ -31,15 +31,17 @@ describe('postFeatureMetrics', () => { serverId: 'id', serverVersion: '0.0.0', timestampUtcMs: 123456, - dataLimit: {enabled: false} + dataLimit: {enabled: false}, }; await postFeatureMetrics(table, report); - const rows: FeatureRow[] = [{ - serverId: report.serverId, - serverVersion: report.serverVersion, - timestamp: new Date(report.timestampUtcMs).toISOString(), - dataLimit: report.dataLimit - }]; + const rows: FeatureRow[] = [ + { + serverId: report.serverId, + serverVersion: report.serverVersion, + timestamp: new Date(report.timestampUtcMs).toISOString(), + dataLimit: report.dataLimit, + }, + ]; expect(table.rows).toEqual(rows); }); }); @@ -50,7 +52,7 @@ describe('isValidFeatureMetricsReport', () => { serverId: 'id', serverVersion: '0.0.0', timestampUtcMs: 123456, - dataLimit: {enabled: true} + dataLimit: {enabled: true}, }; expect(isValidFeatureMetricsReport(report)).toBeTruthy(); }); @@ -59,7 +61,7 @@ describe('isValidFeatureMetricsReport', () => { serverId: 'id', serverVersion: '0.0.0', timestampUtcMs: 123456, - dataLimit: {enabled: true, perKeyLimitCount: 1} + dataLimit: {enabled: true, perKeyLimitCount: 1}, }; expect(isValidFeatureMetricsReport(report)).toBeTruthy(); }); @@ -68,7 +70,7 @@ describe('isValidFeatureMetricsReport', () => { serverId: 'id', serverVersion: '0.0.0', timestampUtcMs: 123456, - dataLimit: {enabled: true, perKeyLimitCount: -1} + dataLimit: {enabled: true, perKeyLimitCount: -1}, }; expect(isValidFeatureMetricsReport(report)).toBeFalsy(); }); @@ -77,26 +79,26 @@ describe('isValidFeatureMetricsReport', () => { }); it('returns false for incorrect report field types', () => { const invalidReport = { - serverId: 1234, // Should be a string + serverId: 1234, // Should be a string serverVersion: '0.0.0', timestampUtcMs: 123456, - dataLimit: {enabled: true} + dataLimit: {enabled: true}, }; expect(isValidFeatureMetricsReport(invalidReport)).toBeFalsy(); const invalidReport2 = { serverId: 'id', - serverVersion: 1010, // Should be a string + serverVersion: 1010, // Should be a string timestampUtcMs: 123456, - dataLimit: {enabled: true} + dataLimit: {enabled: true}, }; expect(isValidFeatureMetricsReport(invalidReport2)).toBeFalsy(); const invalidReport3 = { serverId: 'id', serverVersion: '0.0.0', - timestampUtcMs: '123', // Should be a number - dataLimit: {enabled: true} + timestampUtcMs: '123', // Should be a number + dataLimit: {enabled: true}, }; expect(isValidFeatureMetricsReport(invalidReport3)).toBeFalsy(); @@ -104,7 +106,7 @@ describe('isValidFeatureMetricsReport', () => { serverId: 'id', serverVersion: '0.0.0', timestampUtcMs: 123456, - dataLimit: 'enabled' // Should be `DailyDataLimitMetricsReport` + dataLimit: 'enabled', // Should be `DailyDataLimitMetricsReport` }; expect(isValidFeatureMetricsReport(invalidReport4)).toBeFalsy(); @@ -113,8 +115,8 @@ describe('isValidFeatureMetricsReport', () => { serverVersion: '0.0.0', timestampUtcMs: 123456, dataLimit: { - enabled: 'true' // Should be a boolean - } + enabled: 'true', // Should be a boolean + }, }; expect(isValidFeatureMetricsReport(invalidReport5)).toBeFalsy(); }); @@ -123,7 +125,7 @@ describe('isValidFeatureMetricsReport', () => { // Missing `serverId` serverVersion: '0.0.0', timestampUtcMs: 123456, - dataLimit: {enabled: true} + dataLimit: {enabled: true}, }; expect(isValidFeatureMetricsReport(invalidReport)).toBeFalsy(); @@ -131,7 +133,7 @@ describe('isValidFeatureMetricsReport', () => { // Missing `serverVersion` serverId: 'id', timestampUtcMs: 123456, - dataLimit: {enabled: true} + dataLimit: {enabled: true}, }; expect(isValidFeatureMetricsReport(invalidReport2)).toBeFalsy(); @@ -139,7 +141,7 @@ describe('isValidFeatureMetricsReport', () => { // Missing `timestampUtcMs` serverId: 'id', serverVersion: '0.0.0', - dataLimit: {enabled: true} + dataLimit: {enabled: true}, }; expect(isValidFeatureMetricsReport(invalidReport3)).toBeFalsy(); @@ -156,7 +158,7 @@ describe('isValidFeatureMetricsReport', () => { serverId: 'id', serverVersion: '0.0.0', timestampUtcMs: 123456, - dataLimit: {} + dataLimit: {}, }; expect(isValidFeatureMetricsReport(invalidReport5)).toBeFalsy(); }); diff --git a/src/metrics_server/feature_metrics.ts b/src/metrics_server/feature_metrics.ts index 2d3ce2fb..28aff7e4 100644 --- a/src/metrics_server/feature_metrics.ts +++ b/src/metrics_server/feature_metrics.ts @@ -21,25 +21,27 @@ import {DailyDataLimitMetricsReport, DailyFeatureMetricsReport} from './model'; export interface FeatureRow { serverId: string; serverVersion: string; - timestamp: string; // ISO formatted string + timestamp: string; // ISO formatted string dataLimit: DailyDataLimitMetricsReport; } export class BigQueryFeaturesTable implements InsertableTable { constructor(private bigqueryTable: Table) {} - async insert(rows: FeatureRow|FeatureRow[]): Promise { + async insert(rows: FeatureRow | FeatureRow[]): Promise { await this.bigqueryTable.insert(rows); } } export async function postFeatureMetrics( - table: InsertableTable, report: DailyFeatureMetricsReport) { + table: InsertableTable, + report: DailyFeatureMetricsReport +) { const featureRow: FeatureRow = { serverId: report.serverId, serverVersion: report.serverVersion, timestamp: new Date(report.timestampUtcMs).toISOString(), - dataLimit: report.dataLimit + dataLimit: report.dataLimit, }; return table.insert([featureRow]); } @@ -52,8 +54,12 @@ export function isValidFeatureMetricsReport(obj: any): obj is DailyFeatureMetric } // Check that all required fields are present. - const requiredFeatureMetricsReportFields = - ['serverId', 'serverVersion', 'timestampUtcMs', 'dataLimit']; + const requiredFeatureMetricsReportFields = [ + 'serverId', + 'serverVersion', + 'timestampUtcMs', + 'dataLimit', + ]; for (const fieldName of requiredFeatureMetricsReportFields) { if (!obj[fieldName]) { return false; @@ -61,8 +67,11 @@ export function isValidFeatureMetricsReport(obj: any): obj is DailyFeatureMetric } // Validate the report types are what we expect. - if (typeof obj.serverId !== 'string' || typeof obj.serverVersion !== 'string' || - typeof obj.timestampUtcMs !== 'number') { + if ( + typeof obj.serverId !== 'string' || + typeof obj.serverVersion !== 'string' || + typeof obj.timestampUtcMs !== 'number' + ) { return false; } @@ -73,7 +82,7 @@ export function isValidFeatureMetricsReport(obj: any): obj is DailyFeatureMetric // Validate the per-key data limit feature const perKeyLimitCount = obj.dataLimit.perKeyLimitCount; - if(perKeyLimitCount === undefined) { + if (perKeyLimitCount === undefined) { return true; } if (typeof perKeyLimitCount === 'number') { diff --git a/src/metrics_server/index.ts b/src/metrics_server/index.ts index 59786b1f..d925de26 100644 --- a/src/metrics_server/index.ts +++ b/src/metrics_server/index.ts @@ -36,9 +36,11 @@ const config = loadConfig(); const bigqueryDataset = new BigQuery({projectId: 'uproxysite'}).dataset(config.datasetName); const connectionsTable = new connections.BigQueryConnectionsTable( - bigqueryDataset.table(config.connectionMetricsTableName)); -const featuresTable = - new features.BigQueryFeaturesTable(bigqueryDataset.table(config.featureMetricsTableName)); + bigqueryDataset.table(config.connectionMetricsTableName) +); +const featuresTable = new features.BigQueryFeaturesTable( + bigqueryDataset.table(config.featureMetricsTableName) +); const app = express(); // Parse the request body for content-type 'application/json'. diff --git a/src/metrics_server/tsconfig.json b/src/metrics_server/tsconfig.json index 9384e72f..d62271eb 100644 --- a/src/metrics_server/tsconfig.json +++ b/src/metrics_server/tsconfig.json @@ -6,7 +6,5 @@ "module": "commonjs", "outDir": "../../build/metrics_server" }, - "include": [ - "**/*.ts" - ] + "include": ["**/*.ts"] } diff --git a/src/sentry_webhook/README.md b/src/sentry_webhook/README.md index d12eab02..9a92f41e 100644 --- a/src/sentry_webhook/README.md +++ b/src/sentry_webhook/README.md @@ -4,8 +4,8 @@ The Outline Sentry webhook is a [Google Cloud Function](https://cloud.google.com ## Requirements -* [Google Cloud SDK](https://cloud.google.com/sdk/) -* Access to Outline's Sentry account. +- [Google Cloud SDK](https://cloud.google.com/sdk/) +- Access to Outline's Sentry account. ## Build @@ -16,20 +16,23 @@ npm run action sentry_webhook/build ## Deploy Authenticate with `gcloud`: - ```sh - gcloud auth login - ``` + +```sh +gcloud auth login +``` + To deploy: - ```sh - npm run action sentry_webhook/deploy - ``` + +```sh +npm run action sentry_webhook/deploy +``` ## Configure Sentry Webhooks -* Log in to Outline's [Sentry account](https://sentry.io/outlinevpn/) -* Select a project (outline-client, outline-client-dev, outline-server, outline-server-dev). - * Note that this process must be repeated for all Sentry projects. -* Enable the WebHooks plugin at `https://sentry.io/settings/outlinevpn//plugins/` -* Set the webhook endpoint at `https://sentry.io/settings/outlinevpn//plugins/webhooks/` -* Configure alerts to invoke the webhook at `https://sentry.io/settings/outlinevpn//alerts/` -* Create rules to trigger the webhook at `https://sentry.io/settings/outlinevpn//alerts/rules/` +- Log in to Outline's [Sentry account](https://sentry.io/outlinevpn/) +- Select a project (outline-client, outline-client-dev, outline-server, outline-server-dev). + - Note that this process must be repeated for all Sentry projects. +- Enable the WebHooks plugin at `https://sentry.io/settings/outlinevpn//plugins/` +- Set the webhook endpoint at `https://sentry.io/settings/outlinevpn//plugins/webhooks/` +- Configure alerts to invoke the webhook at `https://sentry.io/settings/outlinevpn//alerts/` +- Create rules to trigger the webhook at `https://sentry.io/settings/outlinevpn//alerts/rules/` diff --git a/src/sentry_webhook/index.ts b/src/sentry_webhook/index.ts index c3c74c1b..b3bdccf3 100644 --- a/src/sentry_webhook/index.ts +++ b/src/sentry_webhook/index.ts @@ -15,7 +15,10 @@ import * as sentry from '@sentry/types'; import * as express from 'express'; -import {postSentryEventToSalesforce, shouldPostEventToSalesforce} from './post_sentry_event_to_salesforce'; +import { + postSentryEventToSalesforce, + shouldPostEventToSalesforce, +} from './post_sentry_event_to_salesforce'; exports.postSentryEventToSalesforce = (req: express.Request, res: express.Response) => { if (req.method !== 'POST') { @@ -35,13 +38,13 @@ exports.postSentryEventToSalesforce = (req: express.Request, res: express.Respon // Use the request message if SentryEvent.message is unpopulated. sentryEvent.message = sentryEvent.message || req.body.message; postSentryEventToSalesforce(sentryEvent, req.body.project) - .then(() => { - res.status(200).send(); - }) - .catch((e) => { - console.error(e); - // Send an OK response to Sentry - they don't need to know about errors with posting to - // Salesforce. - res.status(200).send(); - }); + .then(() => { + res.status(200).send(); + }) + .catch((e) => { + console.error(e); + // Send an OK response to Sentry - they don't need to know about errors with posting to + // Salesforce. + res.status(200).send(); + }); }; diff --git a/src/sentry_webhook/post_sentry_event_to_salesforce.ts b/src/sentry_webhook/post_sentry_event_to_salesforce.ts index 002523a3..43064734 100644 --- a/src/sentry_webhook/post_sentry_event_to_salesforce.ts +++ b/src/sentry_webhook/post_sentry_event_to_salesforce.ts @@ -48,7 +48,7 @@ const SALESFORCE_FORM_FIELDS_DEV: SalesforceFormFields = { sentryEventUrl: '00N3F000002Rqhq', os: '00N3F000002cLcN', version: '00N3F000002cLcI', - type: 'type' + type: 'type', }; const SALESFORCE_FORM_FIELDS_PROD: SalesforceFormFields = { orgId: 'orgid', @@ -60,7 +60,7 @@ const SALESFORCE_FORM_FIELDS_PROD: SalesforceFormFields = { sentryEventUrl: '00N0b00000BqOA4', os: '00N0b00000BqOfW', version: '00N0b00000BqOfR', - type: 'type' + type: 'type', }; const SALESFORCE_FORM_VALUES_DEV: SalesforceFormValues = { orgId: '00D3F000000DDDH', @@ -80,7 +80,9 @@ export function shouldPostEventToSalesforce(event: sentry.SentryEvent) { // Posts a Sentry event to Salesforce using predefined form data. Assumes // `shouldPostEventToSalesforce` has returned true for `event`. export function postSentryEventToSalesforce( - event: sentry.SentryEvent, project: string): Promise { + event: sentry.SentryEvent, + project: string +): Promise { return new Promise((resolve, reject) => { // Sentry development projects are marked with 'dev', i.e. outline-client-dev. const isProd = project.indexOf('-dev') === -1; @@ -88,26 +90,33 @@ export function postSentryEventToSalesforce( const formFields = isProd ? SALESFORCE_FORM_FIELDS_PROD : SALESFORCE_FORM_FIELDS_DEV; const formValues = isProd ? SALESFORCE_FORM_VALUES_PROD : SALESFORCE_FORM_VALUES_DEV; const isClient = project.indexOf('client') !== -1; - const formData = - getSalesforceFormData(formFields, formValues, event, event.user!.email!, isClient, project); + const formData = getSalesforceFormData( + formFields, + formValues, + event, + event.user!.email!, + isClient, + project + ); const req = https.request( - { - host: salesforceHost, - path: SALESFORCE_PATH, - protocol: 'https:', - method: 'post', - headers: { - // The production server will reject requests that do not specify this content type. - 'Content-Type': 'application/x-www-form-urlencoded' - } + { + host: salesforceHost, + path: SALESFORCE_PATH, + protocol: 'https:', + method: 'post', + headers: { + // The production server will reject requests that do not specify this content type. + 'Content-Type': 'application/x-www-form-urlencoded', }, - (res) => { - if (res.statusCode === 200) { - resolve(); - } else { - reject(new Error(`Failed to post form data, response status: ${res.statusCode}`)); - } - }); + }, + (res) => { + if (res.statusCode === 200) { + resolve(); + } else { + reject(new Error(`Failed to post form data, response status: ${res.statusCode}`)); + } + } + ); req.on('error', (err) => { reject(new Error(`Failed to submit form: ${err}`)); }); @@ -118,8 +127,13 @@ export function postSentryEventToSalesforce( // Returns a URL-encoded string with the Salesforce form data. function getSalesforceFormData( - formFields: SalesforceFormFields, formValues: SalesforceFormValues, event: sentry.SentryEvent, - email: string, isClient: boolean, project: string): string { + formFields: SalesforceFormFields, + formValues: SalesforceFormValues, + event: sentry.SentryEvent, + email: string, + isClient: boolean, + project: string +): string { const form = []; form.push(encodeFormData(formFields.orgId, formValues.orgId)); form.push(encodeFormData(formFields.recordType, formValues.recordType)); diff --git a/src/sentry_webhook/tsconfig.json b/src/sentry_webhook/tsconfig.json index fcddb19b..0f3f042c 100644 --- a/src/sentry_webhook/tsconfig.json +++ b/src/sentry_webhook/tsconfig.json @@ -6,7 +6,5 @@ "module": "commonjs", "outDir": "../../build/sentry_webhook" }, - "include": [ - "**/*.ts" - ] + "include": ["**/*.ts"] } diff --git a/src/shadowbox/README.md b/src/shadowbox/README.md index 702b4315..5144ec8d 100644 --- a/src/shadowbox/README.md +++ b/src/shadowbox/README.md @@ -10,11 +10,13 @@ client apps. Shadowbox is also compatible with standard Shadowsocks clients. ## Self-hosted installation To install and run Shadowbox on your own server, run + ``` sudo bash -c "$(wget -qO- https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/src/server_manager/install_scripts/install_server.sh)" ``` You can specify flags to customize the installation. For example, to use hostname `myserver.com` and the port 443 for access keys, you can run: + ``` sudo bash -c "$(wget -qO- https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/src/server_manager/install_scripts/install_server.sh)" install_server.sh --hostname=myserver.com --keys-port=443 ``` @@ -35,9 +37,11 @@ Besides [Node](https://nodejs.org/en/download/) you will also need: ### Running Shadowbox as a Node.js app Build and run the server as a Node.js app: + ``` npm run action shadowbox/server/start ``` + The output will be at `build/shadowbox/app`. ### Running Shadowbox as a Docker container @@ -45,36 +49,41 @@ The output will be at `build/shadowbox/app`. ### With docker command Build the image and run server: + ``` npm run action shadowbox/docker/start ``` You should be able to successfully query the management API: + ``` curl --insecure https://[::]:8081/TestApiPrefix/server ``` To build the image only: + ``` npm run action shadowbox/docker/build ``` Debug image: + ``` docker run --rm -it --entrypoint=sh outline/shadowbox ``` Or a running container: + ``` docker exec -it shadowbox sh ``` Delete dangling images: + ``` docker rmi $(docker images -f dangling=true -q) ``` - ## Access Keys Management API In order to utilize the Management API, you'll need to know the apiUrl for your Outline server. @@ -87,6 +96,7 @@ The OpenAPI specification can be found at [api.yml](./server/api.yml). ### Examples Start by storing the apiURL you see see in that file, as a variable. For example: + ``` API_URL=https://1.2.3.4:1234/3pQ4jf6qSr5WVeMO0XOo4z ``` @@ -94,34 +104,40 @@ API_URL=https://1.2.3.4:1234/3pQ4jf6qSr5WVeMO0XOo4z You can then perform the following operations on the server, remotely. List access keys + ``` curl --insecure $API_URL/access-keys/ ``` Create an access key + ``` curl --insecure -X POST $API_URL/access-keys ``` Rename an access key (e.g. rename access key 2 to 'albion') + ``` curl --insecure -X PUT curl -F 'name=albion' $API_URL/access-keys/2/name ``` Remove an access key (e.g. remove access key 2) + ``` curl --insecure -X DELETE $API_URL/access-keys/2 ``` Set a data limit for all access keys (e.g. limit outbound data transfer access keys to 1MB over 30 days) + ``` curl -v --insecure -X PUT -H "Content-Type: application/json" -d '{"limit": {"bytes": 1000}}' $API_URL/experimental/access-key-data-limit ``` Remove the access key data limit + ``` curl -v --insecure -X DELETE $API_URL/experimental/access-key-data-limit ``` @@ -142,11 +158,13 @@ modified image. ### Automated To run the integration test: + ``` npm run action shadowbox/integration_test/start ``` This will set up three containers and two networks: + ``` client <-> shadowbox <-> target ``` @@ -155,11 +173,13 @@ client <-> shadowbox <-> target To test clients that rely on fetching a docker image from Dockerhub, you can push an image to your account and modify the client to use your image. To push your own image: + ``` npm run action shadowbox/docker/build && docker tag quay.io/outline/shadowbox $USER/shadowbox && docker push $USER/shadowbox ``` If you need to test an unsigned image (e.g. your dev one): + ``` DOCKER_CONTENT_TRUST=0 SB_IMAGE=$USER/shadowbox npm run action shadowbox/integration_test/start ``` @@ -175,4 +195,4 @@ start-up time, then you mey need to remove the pre-existing test config: rm /tmp/outline/persisted-state/shadowbox_server_config.json ``` -This will warn about deleting a write-protected file, which is okay to ignore. You will then need to hand-edit the JSON string in src/shadowbox/docker/start.action.sh. +This will warn about deleting a write-protected file, which is okay to ignore. You will then need to hand-edit the JSON string in src/shadowbox/docker/start.action.sh. diff --git a/src/shadowbox/infrastructure/clock.ts b/src/shadowbox/infrastructure/clock.ts index ac7e7a28..a14f0421 100644 --- a/src/shadowbox/infrastructure/clock.ts +++ b/src/shadowbox/infrastructure/clock.ts @@ -49,4 +49,4 @@ export class ManualClock implements Clock { await callback(); } } -} \ No newline at end of file +} diff --git a/src/shadowbox/infrastructure/file.spec.ts b/src/shadowbox/infrastructure/file.spec.ts index 738c7362..bb8b773e 100644 --- a/src/shadowbox/infrastructure/file.spec.ts +++ b/src/shadowbox/infrastructure/file.spec.ts @@ -8,7 +8,7 @@ describe('file', () => { describe('readFileIfExists', () => { let tmpFile: tmp.FileResult; - beforeEach(() => tmpFile = tmp.fileSync()); + beforeEach(() => (tmpFile = tmp.fileSync())); it('reads the file if it exists', () => { const contents = 'test'; @@ -24,14 +24,14 @@ describe('file', () => { expect(file.readFileIfExists(tmpFile.name)).toBe(''); }); - it('returns null if file doesn\'t exist', - () => expect(file.readFileIfExists(tmp.tmpNameSync())).toBe(null)); + it("returns null if file doesn't exist", () => + expect(file.readFileIfExists(tmp.tmpNameSync())).toBe(null)); }); describe('atomicWriteFileSync', () => { let tmpFile: tmp.FileResult; - beforeEach(() => tmpFile = tmp.fileSync()); + beforeEach(() => (tmpFile = tmp.fileSync())); it('writes to the file', () => { const contents = 'test'; @@ -44,20 +44,24 @@ describe('file', () => { it('supports multiple simultaneous writes to the same file', async () => { const writeCount = 100; - const writer = (_, id) => new Promise((resolve, reject) => { - try { - file.atomicWriteFileSync( - tmpFile.name, `${fs.readFileSync(tmpFile.name, {encoding: 'utf-8'})}${id}\n`); - resolve(); - } catch (e) { - reject(e); - } - }); + const writer = (_, id) => + new Promise((resolve, reject) => { + try { + file.atomicWriteFileSync( + tmpFile.name, + `${fs.readFileSync(tmpFile.name, {encoding: 'utf-8'})}${id}\n` + ); + resolve(); + } catch (e) { + reject(e); + } + }); await Promise.all(Array.from({length: writeCount}, writer)); - expect(fs.readFileSync(tmpFile.name, {encoding: 'utf8'}).trimEnd().split('\n').length) - .toBe(writeCount); + expect(fs.readFileSync(tmpFile.name, {encoding: 'utf8'}).trimEnd().split('\n').length).toBe( + writeCount + ); }); }); }); diff --git a/src/shadowbox/infrastructure/follow_redirects.ts b/src/shadowbox/infrastructure/follow_redirects.ts index b30d14ec..af344f9d 100644 --- a/src/shadowbox/infrastructure/follow_redirects.ts +++ b/src/shadowbox/infrastructure/follow_redirects.ts @@ -20,7 +20,9 @@ import fetch, {RequestInit, Response} from 'node-fetch'; // method to GET and removing the request body. The options parameter matches the // fetch() function. export async function requestFollowRedirectsWithSameMethodAndBody( - url: string, options: RequestInit): Promise { + url: string, + options: RequestInit +): Promise { // Make a copy of options to modify parameters. const manualRedirectOptions = { ...options, diff --git a/src/shadowbox/infrastructure/json_config.ts b/src/shadowbox/infrastructure/json_config.ts index 29d40cf9..922e1a50 100644 --- a/src/shadowbox/infrastructure/json_config.ts +++ b/src/shadowbox/infrastructure/json_config.ts @@ -31,7 +31,6 @@ export function loadFileConfig(filename: string): JsonConfig { return new FileConfig(filename, dataJson); } - // FileConfig is a JsonConfig backed by a filesystem file. export class FileConfig implements JsonConfig { constructor(private filename: string, private dataJson: T) {} diff --git a/src/shadowbox/infrastructure/logging.ts b/src/shadowbox/infrastructure/logging.ts index 97d70950..ea9eb55c 100644 --- a/src/shadowbox/infrastructure/logging.ts +++ b/src/shadowbox/infrastructure/logging.ts @@ -35,7 +35,7 @@ function getCallsite(): Callsite { } // Possible values for the level prefix. -type LevelPrefix = 'E'|'W'|'I'|'D'; +type LevelPrefix = 'E' | 'W' | 'I' | 'D'; // Formats the log message. Example: // I2018-08-16T16:46:21.577Z 167288 main.js:86] ... @@ -46,8 +46,9 @@ function makeLogMessage(level: LevelPrefix, callsite: Callsite, message: string) const timeStr = new Date().toISOString(); // TODO(alalama): preserve the source file structure in the webpack build so we can use // `callsite.getFileName()`. - return `${level}${timeStr} ${process.pid} ${ - path.basename(callsite.getFileName() || __filename)}:${callsite.getLineNumber()}] ${message}`; + return `${level}${timeStr} ${process.pid} ${path.basename( + callsite.getFileName() || __filename + )}:${callsite.getLineNumber()}] ${message}`; } export function error(message: string) { diff --git a/src/shadowbox/infrastructure/prometheus_scraper.ts b/src/shadowbox/infrastructure/prometheus_scraper.ts index ff7ed716..da74615a 100644 --- a/src/shadowbox/infrastructure/prometheus_scraper.ts +++ b/src/shadowbox/infrastructure/prometheus_scraper.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. - import * as child_process from 'child_process'; import * as fs from 'fs'; import * as http from 'http'; @@ -23,17 +22,16 @@ import * as path from 'path'; import * as logging from '../infrastructure/logging'; export interface QueryResultData { - resultType: 'matrix'|'vector'|'scalar'|'string'; - result: Array < { + resultType: 'matrix' | 'vector' | 'scalar' | 'string'; + result: Array<{ metric: {[labelValue: string]: string}; value: [number, string]; - } - > ; + }>; } // From https://prometheus.io/docs/prometheus/latest/querying/api/ interface QueryResult { - status: 'success'|'error'; + status: 'success' | 'error'; data: QueryResultData; errorType: string; error: string; @@ -45,40 +43,46 @@ export class PrometheusClient { query(query: string): Promise { return new Promise((fulfill, reject) => { const url = `${this.address}/api/v1/query?query=${encodeURIComponent(query)}`; - http.get(url, (response) => { - if (response.statusCode < 200 || response.statusCode > 299) { - reject(new Error(`Got error ${response.statusCode}`)); - response.resume(); - return; + http + .get(url, (response) => { + if (response.statusCode < 200 || response.statusCode > 299) { + reject(new Error(`Got error ${response.statusCode}`)); + response.resume(); + return; + } + let body = ''; + response.on('data', (data) => { + body += data; + }); + response.on('end', () => { + const result = JSON.parse(body) as QueryResult; + if (result.status !== 'success') { + return reject(new Error(`Error ${result.errorType}: ${result.error}`)); } - let body = ''; - response.on('data', (data) => { - body += data; - }); - response.on('end', () => { - const result = JSON.parse(body) as QueryResult; - if (result.status !== 'success') { - return reject(new Error(`Error ${result.errorType}: ${result.error}`)); - } - fulfill(result.data); - }); - }).on('error', (e) => { - reject(new Error(`Failed to query prometheus API: ${e}`)); - }); + fulfill(result.data); + }); + }) + .on('error', (e) => { + reject(new Error(`Failed to query prometheus API: ${e}`)); + }); }); } } export async function startPrometheus( - binaryFilename: string, configFilename: string, configJson: {}, processArgs: string[], - endpoint: string) { + binaryFilename: string, + configFilename: string, + configJson: {}, + processArgs: string[], + endpoint: string +) { await writePrometheusConfigToDisk(configFilename, configJson); await spawnPrometheusSubprocess(binaryFilename, processArgs, endpoint); } async function writePrometheusConfigToDisk(configFilename: string, configJson: {}) { await mkdirp.sync(path.dirname(configFilename)); - const ymlTxt = jsyaml.safeDump(configJson, {'sortKeys': true}); + const ymlTxt = jsyaml.safeDump(configJson, {sortKeys: true}); // Write the file asynchronously to prevent blocking the node thread. await new Promise((resolve, reject) => { fs.writeFile(configFilename, ymlTxt, 'utf-8', (err) => { @@ -92,8 +96,10 @@ async function writePrometheusConfigToDisk(configFilename: string, configJson: { } async function spawnPrometheusSubprocess( - binaryFilename: string, processArgs: string[], - prometheusEndpoint: string): Promise { + binaryFilename: string, + processArgs: string[], + prometheusEndpoint: string +): Promise { logging.info(`Starting Prometheus with args [${processArgs}]`); const runProcess = child_process.spawn(binaryFilename, processArgs); runProcess.on('error', (error) => { @@ -120,11 +126,13 @@ async function waitForPrometheusReady(prometheusEndpoint: string) { function isHttpEndpointHealthy(endpoint: string): Promise { return new Promise((resolve, reject) => { - http.get(endpoint, (response) => { - resolve(response.statusCode >= 200 && response.statusCode < 300); - }).on('error', (e) => { - // Prometheus is not ready yet. - resolve(false); - }); + http + .get(endpoint, (response) => { + resolve(response.statusCode >= 200 && response.statusCode < 300); + }) + .on('error', (e) => { + // Prometheus is not ready yet. + resolve(false); + }); }); } diff --git a/src/shadowbox/integration_test/target/index.html b/src/shadowbox/integration_test/target/index.html index 03e16be5..58344201 100644 --- a/src/shadowbox/integration_test/target/index.html +++ b/src/shadowbox/integration_test/target/index.html @@ -1 +1,3 @@ -TARGET PAGE CONTENT + + TARGET PAGE CONTENT + diff --git a/src/shadowbox/model/access_key.ts b/src/shadowbox/model/access_key.ts index 00dfd097..0d279703 100644 --- a/src/shadowbox/model/access_key.ts +++ b/src/shadowbox/model/access_key.ts @@ -62,7 +62,7 @@ export interface AccessKeyRepository { // Apply the specified update to the specified access key. Throws on failure. renameAccessKey(id: AccessKeyId, name: string): void; // Gets the metrics id for a given Access Key. - getMetricsId(id: AccessKeyId): AccessKeyMetricsId|undefined; + getMetricsId(id: AccessKeyId): AccessKeyMetricsId | undefined; // Sets a data transfer limit for all access keys. setDefaultDataLimit(limit: DataLimit): void; // Removes the access key data transfer limit. diff --git a/src/shadowbox/model/metrics.ts b/src/shadowbox/model/metrics.ts index da45dfd2..9a9a901b 100644 --- a/src/shadowbox/model/metrics.ts +++ b/src/shadowbox/model/metrics.ts @@ -25,4 +25,6 @@ export interface DataUsageByUser { } // Sliding time frame for measuring data utilization. -export interface DataUsageTimeframe { hours: number; } \ No newline at end of file +export interface DataUsageTimeframe { + hours: number; +} diff --git a/src/shadowbox/server/main.ts b/src/shadowbox/server/main.ts index bc573391..988b2fe5 100644 --- a/src/shadowbox/server/main.ts +++ b/src/shadowbox/server/main.ts @@ -33,7 +33,12 @@ import {bindService, ShadowsocksManagerService} from './manager_service'; import {OutlineShadowsocksServer} from './outline_shadowsocks_server'; import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key'; import * as server_config from './server_config'; -import {OutlineSharedMetricsPublisher, PrometheusUsageMetrics, RestMetricsCollectorClient, SharedMetricsPublisher} from './shared_metrics'; +import { + OutlineSharedMetricsPublisher, + PrometheusUsageMetrics, + RestMetricsCollectorClient, + SharedMetricsPublisher, +} from './shared_metrics'; const APP_BASE_DIR = path.join(__dirname, '..'); const DEFAULT_STATE_DIR = '/root/shadowbox/persisted-state'; @@ -53,14 +58,17 @@ async function exportPrometheusMetrics(registry: prometheus.Registry, port): Pro } function reserveExistingAccessKeyPorts( - keyConfig: json_config.JsonConfig, portProvider: PortProvider) { + keyConfig: json_config.JsonConfig, + portProvider: PortProvider +) { const accessKeys = keyConfig.data().accessKeys || []; - const dedupedPorts = new Set(accessKeys.map(ak => ak.port)); - dedupedPorts.forEach(p => portProvider.addReservedPort(p)); + const dedupedPorts = new Set(accessKeys.map((ak) => ak.port)); + dedupedPorts.forEach((p) => portProvider.addReservedPort(p)); } -function createRolloutTracker(serverConfig: json_config.JsonConfig): - RolloutTracker { +function createRolloutTracker( + serverConfig: json_config.JsonConfig +): RolloutTracker { const rollouts = new RolloutTracker(serverConfig.data().serverId); if (serverConfig.data().rollouts) { for (const rollout of serverConfig.data().rollouts) { @@ -74,7 +82,8 @@ async function main() { const verbose = process.env.LOG_LEVEL === 'debug'; const portProvider = new PortProvider(); const accessKeyConfig = json_config.loadFileConfig( - getPersistentFilename('shadowbox_config.json')); + getPersistentFilename('shadowbox_config.json') + ); reserveExistingAccessKeyPorts(accessKeyConfig, portProvider); prometheus.collectDefaultMetrics({register: prometheus.register}); @@ -94,8 +103,9 @@ async function main() { } portProvider.addReservedPort(apiPortNumber); - const serverConfig = - server_config.readServerConfig(getPersistentFilename('shadowbox_server_config.json')); + const serverConfig = server_config.readServerConfig( + getPersistentFilename('shadowbox_server_config.json') + ); const proxyHostname = serverConfig.data().hostname; if (!proxyHostname) { @@ -130,22 +140,29 @@ async function main() { scrape_configs: [ {job_name: 'prometheus', static_configs: [{targets: [prometheusLocation]}]}, {job_name: 'outline-server-main', static_configs: [{targets: [nodeMetricsLocation]}]}, - ] + ], }; const ssMetricsLocation = `127.0.0.1:${ssMetricsPort}`; logging.info(`outline-ss-server metrics is at ${ssMetricsLocation}`); - prometheusConfigJson.scrape_configs.push( - {job_name: 'outline-server-ss', static_configs: [{targets: [ssMetricsLocation]}]}); + prometheusConfigJson.scrape_configs.push({ + job_name: 'outline-server-ss', + static_configs: [{targets: [ssMetricsLocation]}], + }); const shadowsocksServer = new OutlineShadowsocksServer( - getBinaryFilename('outline-ss-server'), getPersistentFilename('outline-ss-server/config.yml'), - verbose, ssMetricsLocation); + getBinaryFilename('outline-ss-server'), + getPersistentFilename('outline-ss-server/config.yml'), + verbose, + ssMetricsLocation + ); if (fs.existsSync(MMDB_LOCATION)) { shadowsocksServer.enableCountryMetrics(MMDB_LOCATION); } - const isReplayProtectionEnabled = - createRolloutTracker(serverConfig).isRolloutEnabled('replay-protection', 100); + const isReplayProtectionEnabled = createRolloutTracker(serverConfig).isRolloutEnabled( + 'replay-protection', + 100 + ); logging.info(`Replay protection enabled: ${isReplayProtectionEnabled}`); if (isReplayProtectionEnabled) { shadowsocksServer.enableReplayProtection(); @@ -157,13 +174,25 @@ async function main() { const prometheusEndpoint = `http://${prometheusLocation}`; const prometheusBinary = getBinaryFilename('prometheus'); const prometheusArgs = [ - '--config.file', prometheusConfigFilename, '--web.enable-admin-api', - '--storage.tsdb.retention.time', '31d', '--storage.tsdb.path', prometheusTsdbFilename, - '--web.listen-address', prometheusLocation, '--log.level', verbose ? 'debug' : 'info' + '--config.file', + prometheusConfigFilename, + '--web.enable-admin-api', + '--storage.tsdb.retention.time', + '31d', + '--storage.tsdb.path', + prometheusTsdbFilename, + '--web.listen-address', + prometheusLocation, + '--log.level', + verbose ? 'debug' : 'info', ]; await startPrometheus( - prometheusBinary, prometheusConfigFilename, prometheusConfigJson, prometheusArgs, - prometheusEndpoint); + prometheusBinary, + prometheusConfigFilename, + prometheusConfigJson, + prometheusArgs, + prometheusEndpoint + ); const prometheusClient = new PrometheusClient(prometheusEndpoint); if (!serverConfig.data().portForNewAccessKeys) { @@ -171,8 +200,13 @@ async function main() { serverConfig.write(); } const accessKeyRepository = new ServerAccessKeyRepository( - serverConfig.data().portForNewAccessKeys, proxyHostname, accessKeyConfig, shadowsocksServer, - prometheusClient, serverConfig.data().accessKeyDataLimit); + serverConfig.data().portForNewAccessKeys, + proxyHostname, + accessKeyConfig, + shadowsocksServer, + prometheusClient, + serverConfig.data().accessKeyDataLimit + ); const metricsReader = new PrometheusUsageMetrics(prometheusClient); const toMetricsId = (id: AccessKeyId) => { @@ -185,21 +219,35 @@ async function main() { const managerMetrics = new PrometheusManagerMetrics(prometheusClient); const metricsCollector = new RestMetricsCollectorClient(metricsCollectorUrl); const metricsPublisher: SharedMetricsPublisher = new OutlineSharedMetricsPublisher( - new RealClock(), serverConfig, accessKeyConfig, metricsReader, toMetricsId, metricsCollector); + new RealClock(), + serverConfig, + accessKeyConfig, + metricsReader, + toMetricsId, + metricsCollector + ); const managerService = new ShadowsocksManagerService( - process.env.SB_DEFAULT_SERVER_NAME || 'Outline Server', serverConfig, accessKeyRepository, - managerMetrics, metricsPublisher); + process.env.SB_DEFAULT_SERVER_NAME || 'Outline Server', + serverConfig, + accessKeyRepository, + managerMetrics, + metricsPublisher + ); const certificateFilename = process.env.SB_CERTIFICATE_FILE; const privateKeyFilename = process.env.SB_PRIVATE_KEY_FILE; const apiServer = restify.createServer({ certificate: fs.readFileSync(certificateFilename), - key: fs.readFileSync(privateKeyFilename) + key: fs.readFileSync(privateKeyFilename), }); // Pre-routing handlers - const cors = - corsMiddleware({origins: ['*'], allowHeaders: [], exposeHeaders: [], credentials: false}); + const cors = corsMiddleware({ + origins: ['*'], + allowHeaders: [], + exposeHeaders: [], + credentials: false, + }); apiServer.pre(cors.preflight); apiServer.pre(restify.pre.sanitizePath()); diff --git a/src/shadowbox/server/manager_metrics.spec.ts b/src/shadowbox/server/manager_metrics.spec.ts index 0c730939..febd2c5d 100644 --- a/src/shadowbox/server/manager_metrics.spec.ts +++ b/src/shadowbox/server/manager_metrics.spec.ts @@ -20,7 +20,8 @@ import {FakePrometheusClient} from './mocks/mocks'; describe('PrometheusManagerMetrics', () => { it('getOutboundByteTransfer', async (done) => { const managerMetrics = new PrometheusManagerMetrics( - new FakePrometheusClient({'access-key-1': 1000, 'access-key-2': 10000})); + new FakePrometheusClient({'access-key-1': 1000, 'access-key-2': 10000}) + ); const dataUsage = await managerMetrics.getOutboundByteTransfer({hours: 0}); const bytesTransferredByUserId = dataUsage.bytesTransferredByUserId; expect(Object.keys(bytesTransferredByUserId).length).toEqual(2); diff --git a/src/shadowbox/server/manager_metrics.ts b/src/shadowbox/server/manager_metrics.ts index 56f83343..cb8275f5 100644 --- a/src/shadowbox/server/manager_metrics.ts +++ b/src/shadowbox/server/manager_metrics.ts @@ -27,9 +27,9 @@ export class PrometheusManagerMetrics implements ManagerMetrics { // TODO(fortuna): Consider pre-computing this to save server's CPU. // We measure only traffic leaving the server, since that's what DigitalOcean charges. // TODO: Display all directions to admin - const result = - await this.prometheusClient.query(`sum(increase(shadowsocks_data_bytes{dir=~"ct"}[${ - timeframe.hours}h])) by (access_key)`); + const result = await this.prometheusClient.query( + `sum(increase(shadowsocks_data_bytes{dir=~"ct"}[${timeframe.hours}h])) by (access_key)` + ); const usage = {} as {[userId: string]: number}; for (const entry of result.result) { const bytes = Math.round(parseFloat(entry.value[1])); diff --git a/src/shadowbox/server/manager_service.spec.ts b/src/shadowbox/server/manager_service.spec.ts index c65f28dd..e8182038 100644 --- a/src/shadowbox/server/manager_service.spec.ts +++ b/src/shadowbox/server/manager_service.spec.ts @@ -33,8 +33,15 @@ interface ServerInfo { const NEW_PORT = 12345; const OLD_PORT = 54321; -const EXPECTED_ACCESS_KEY_PROPERTIES = - ['id', 'name', 'password', 'port', 'method', 'accessUrl', 'dataLimit'].sort(); +const EXPECTED_ACCESS_KEY_PROPERTIES = [ + 'id', + 'name', + 'password', + 'port', + 'method', + 'accessUrl', + 'dataLimit', +].sort(); describe('ShadowsocksManagerService', () => { // After processing the response callback, we should set @@ -53,38 +60,44 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); service.getServer( - {params: {}}, { - send: (httpCode, data: ServerInfo) => { - expect(httpCode).toEqual(200); - expect(data.name).toEqual('default name'); - responseProcessed = true; - } + {params: {}}, + { + send: (httpCode, data: ServerInfo) => { + expect(httpCode).toEqual(200); + expect(data.name).toEqual('default name'); + responseProcessed = true; }, - done); + }, + done + ); }); it('Returns persisted properties', (done) => { const repo = getAccessKeyRepository(); const defaultDataLimit = {bytes: 999}; - const serverConfig = - new InMemoryConfig({name: 'Server', accessKeyDataLimit: defaultDataLimit} as ServerConfigJson); + const serverConfig = new InMemoryConfig({ + name: 'Server', + accessKeyDataLimit: defaultDataLimit, + } as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); service.getServer( - {params: {}}, { - send: (httpCode, data: ServerInfo) => { - expect(httpCode).toEqual(200); - expect(data.name).toEqual('Server'); - expect(data.accessKeyDataLimit).toEqual(defaultDataLimit); - responseProcessed = true; - } + {params: {}}, + { + send: (httpCode, data: ServerInfo) => { + expect(httpCode).toEqual(200); + expect(data.name).toEqual('Server'); + expect(data.accessKeyDataLimit).toEqual(defaultDataLimit); + responseProcessed = true; }, - done); + }, + done + ); }); }); @@ -93,18 +106,20 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); service.renameServer( - {params: {name: 'new name'}}, { - send: (httpCode, _) => { - expect(httpCode).toEqual(204); - expect(serverConfig.mostRecentWrite.name).toEqual('new name'); - responseProcessed = true; - } + {params: {name: 'new name'}}, + { + send: (httpCode, _) => { + expect(httpCode).toEqual(204); + expect(serverConfig.mostRecentWrite.name).toEqual('new name'); + responseProcessed = true; }, - done); + }, + done + ); }); }); @@ -112,19 +127,26 @@ describe('ShadowsocksManagerService', () => { it(`accepts valid hostnames`, (done) => { const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(getAccessKeyRepository()) - .build(); + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); const res = { send: (httpCode) => { expect(httpCode).toEqual(204); - } + }, }; const goodHostnames = [ - '-bad', 'localhost', 'example.com', 'www.example.org', 'www.exa-mple.tw', '123abc.co.uk', - '93.184.216.34', '::0', '2606:2800:220:1:248:1893:25c8:1946' + '-bad', + 'localhost', + 'example.com', + 'www.example.org', + 'www.exa-mple.tw', + '123abc.co.uk', + '93.184.216.34', + '::0', + '2606:2800:220:1:248:1893:25c8:1946', ]; for (const hostname of goodHostnames) { service.setHostnameForAccessKeys({params: {hostname}}, res, () => {}); @@ -136,19 +158,23 @@ describe('ShadowsocksManagerService', () => { it(`rejects invalid hostnames`, (done) => { const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(getAccessKeyRepository()) - .build(); + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); const res = {send: (httpCode) => {}}; const next = (error) => { expect(error.statusCode).toEqual(400); }; - const badHostnames = [ - null, '', '-abc.com', 'abc-.com', 'abc.com/def', 'i_have_underscores.net', - 'gggg:ggg:220:1:248:1893:25c8:1946' + null, + '', + '-abc.com', + 'abc-.com', + 'abc.com/def', + 'i_have_underscores.net', + 'gggg:ggg:220:1:248:1893:25c8:1946', ]; for (const hostname of badHostnames) { service.setHostnameForAccessKeys({params: {hostname}}, res, next); @@ -157,28 +183,28 @@ describe('ShadowsocksManagerService', () => { responseProcessed = true; done(); }); - it('Changes the server\'s hostname', (done) => { + it("Changes the server's hostname", (done) => { const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(getAccessKeyRepository()) - .build(); + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); const hostname = 'www.example.org'; const res = { send: (httpCode) => { expect(httpCode).toEqual(204); expect(serverConfig.data().hostname).toEqual(hostname); responseProcessed = true; - } + }, }; service.setHostnameForAccessKeys({params: {hostname}}, res, done); }); it('Rejects missing hostname', (done) => { const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(getAccessKeyRepository()) - .build(); + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); const res = {send: (httpCode) => {}}; const next = (error) => { expect(error.statusCode).toEqual(400); @@ -191,9 +217,9 @@ describe('ShadowsocksManagerService', () => { it('Rejects non-string hostname', (done) => { const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(getAccessKeyRepository()) - .build(); + .serverConfig(serverConfig) + .accessKeys(getAccessKeyRepository()) + .build(); const res = {send: (httpCode) => {}}; const next = (error) => { expect(error.statusCode).toEqual(400); @@ -201,7 +227,7 @@ describe('ShadowsocksManagerService', () => { done(); }; // tslint:disable-next-line: no-any - const badHostname = ({params: {hostname: 123}} as any) as {params: {hostname: string}}; + const badHostname = {params: {hostname: 123}} as any as {params: {hostname: string}}; service.setHostnameForAccessKeys(badHostname, res, next); }); }); @@ -222,8 +248,8 @@ describe('ShadowsocksManagerService', () => { expect(data.accessKeys[0].id).toEqual(key1.id); expect(data.accessKeys[1].name).toEqual(key2.name); expect(data.accessKeys[1].id).toEqual(key2.id); - responseProcessed = true; // required for afterEach to pass. - } + responseProcessed = true; // required for afterEach to pass. + }, }; service.listAccessKeys({params: {}}, res, done); }); @@ -243,8 +269,8 @@ describe('ShadowsocksManagerService', () => { expect(Object.keys(serviceAccessKey1).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); expect(Object.keys(serviceAccessKey2).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); expect(serviceAccessKey1.name).toEqual(accessKeyName); - responseProcessed = true; // required for afterEach to pass. - } + responseProcessed = true; // required for afterEach to pass. + }, }; service.listAccessKeys({params: {}}, res, done); }); @@ -260,8 +286,8 @@ describe('ShadowsocksManagerService', () => { send: (httpCode, data) => { expect(httpCode).toEqual(201); expect(Object.keys(data).sort()).toEqual(EXPECTED_ACCESS_KEY_PROPERTIES); - responseProcessed = true; // required for afterEach to pass. - } + responseProcessed = true; // required for afterEach to pass. + }, }; service.createNewAccessKey({params: {}}, res, done); }); @@ -273,7 +299,7 @@ describe('ShadowsocksManagerService', () => { const res = {send: (httpCode, data) => {}}; service.createNewAccessKey({params: {}}, res, (error) => { expect(error.statusCode).toEqual(500); - responseProcessed = true; // required for afterEach to pass. + responseProcessed = true; // required for afterEach to pass. done(); }); }); @@ -283,15 +309,15 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); const oldKey = await repo.createNewAccessKey(); const res = { send: (httpCode) => { expect(httpCode).toEqual(204); - } + }, }; await service.setPortForNewAccessKeys({params: {port: NEW_PORT}}, res, () => {}); const newKey = await repo.createNewAccessKey(); @@ -305,16 +331,16 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); const res = { send: (httpCode) => { expect(httpCode).toEqual(204); expect(serverConfig.data().portForNewAccessKeys).toEqual(NEW_PORT); responseProcessed = true; - } + }, }; await service.setPortForNewAccessKeys({params: {port: NEW_PORT}}, res, done); }); @@ -323,16 +349,16 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); const res = { send: (httpCode) => { fail( - `setPortForNewAccessKeys should have failed with 400 Bad Request, instead succeeded with code ${ - httpCode}`); - } + `setPortForNewAccessKeys should have failed with 400 Bad Request, instead succeeded with code ${httpCode}` + ); + }, }; const next = (error) => { // Bad Request @@ -352,16 +378,16 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); const res = { send: (httpCode) => { fail( - `setPortForNewAccessKeys should have failed with 409 Conflict, instead succeeded with code ${ - httpCode}`); - } + `setPortForNewAccessKeys should have failed with 409 Conflict, instead succeeded with code ${httpCode}` + ); + }, }; const next = (error) => { // Conflict @@ -380,9 +406,9 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); await service.createNewAccessKey({params: {}}, {send: () => {}}, () => {}); await service.setPortForNewAccessKeys({params: {port: NEW_PORT}}, {send: () => {}}, () => {}); @@ -390,7 +416,7 @@ describe('ShadowsocksManagerService', () => { send: (httpCode) => { expect(httpCode).toEqual(204); responseProcessed = true; - } + }, }; const firstKeyConnection = new net.Server(); @@ -405,17 +431,17 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); const noPort = {params: {}}; const res = { send: (httpCode) => { fail( - `setPortForNewAccessKeys should have failed with 400 BadRequest, instead succeeded with code ${ - httpCode}`); - } + `setPortForNewAccessKeys should have failed with 400 BadRequest, instead succeeded with code ${httpCode}` + ); + }, }; const next = (error) => { expect(error.statusCode).toEqual(400); @@ -426,7 +452,10 @@ describe('ShadowsocksManagerService', () => { const nonNumericPort = {params: {port: 'abc'}}; await service.setPortForNewAccessKeys( // tslint:disable-next-line: no-any - (nonNumericPort as any) as {params: {port: number}}, res, next); + nonNumericPort as any as {params: {port: number}}, + res, + next + ); responseProcessed = true; done(); @@ -446,8 +475,8 @@ describe('ShadowsocksManagerService', () => { const keys = repo.listAccessKeys(); expect(keys.length).toEqual(1); expect(keys[0].id === key2.id); - responseProcessed = true; // required for afterEach to pass. - } + responseProcessed = true; // required for afterEach to pass. + }, }; // remove the 1st key. service.removeAccessKey({params: {id: key1.id}}, res, done); @@ -460,7 +489,7 @@ describe('ShadowsocksManagerService', () => { const res = {send: (httpCode, data) => {}}; service.removeAccessKey({params: {id: key.id}}, res, (error) => { expect(error.statusCode).toEqual(500); - responseProcessed = true; // required for afterEach to pass. + responseProcessed = true; // required for afterEach to pass. done(); }); }); @@ -479,8 +508,8 @@ describe('ShadowsocksManagerService', () => { send: (httpCode, data) => { expect(httpCode).toEqual(204); expect(key.name === NEW_NAME); - responseProcessed = true; // required for afterEach to pass. - } + responseProcessed = true; // required for afterEach to pass. + }, }; service.renameAccessKey({params: {id: key.id, name: NEW_NAME}}, res, done); }); @@ -492,7 +521,7 @@ describe('ShadowsocksManagerService', () => { const res = {send: (httpCode, data) => {}}; service.renameAccessKey({params: {id: 123}}, res, (error) => { expect(error.statusCode).toEqual(400); - responseProcessed = true; // required for afterEach to pass. + responseProcessed = true; // required for afterEach to pass. done(); }); }); @@ -505,7 +534,7 @@ describe('ShadowsocksManagerService', () => { const res = {send: (httpCode, data) => {}}; service.renameAccessKey({params: {id: key.id, name: 'newName'}}, res, (error) => { expect(error.statusCode).toEqual(500); - responseProcessed = true; // required for afterEach to pass. + responseProcessed = true; // required for afterEach to pass. done(); }); }); @@ -517,12 +546,14 @@ describe('ShadowsocksManagerService', () => { const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); const key = await repo.createNewAccessKey(); const limit = {bytes: 1000}; - const res = {send: (httpCode) => { - expect(httpCode).toEqual(204); - expect(key.dataLimit.bytes).toEqual(1000); - responseProcessed = true; - done(); - }}; + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + expect(key.dataLimit.bytes).toEqual(1000); + responseProcessed = true; + done(); + }, + }; service.setAccessKeyDataLimit({params: {id: key.id, limit}}, res, () => {}); }); @@ -542,7 +573,7 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); const keyId = (await repo.createNewAccessKey()).id; - const limit = {bytes: "1"}; + const limit = {bytes: '1'}; service.setAccessKeyDataLimit({params: {id: keyId, limit}}, {send: () => {}}, (error) => { expect(error.statusCode).toEqual(400); responseProcessed = true; @@ -567,11 +598,15 @@ describe('ShadowsocksManagerService', () => { const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); await repo.createNewAccessKey(); const limit: DataLimit = {bytes: 1000}; - service.setAccessKeyDataLimit({params: {id: "not an id", limit}}, {send: () => {}}, (error) => { - expect(error.statusCode).toEqual(404); - responseProcessed = true; - done(); - }); + service.setAccessKeyDataLimit( + {params: {id: 'not an id', limit}}, + {send: () => {}}, + (error) => { + expect(error.statusCode).toEqual(404); + responseProcessed = true; + done(); + } + ); }); }); @@ -582,19 +617,21 @@ describe('ShadowsocksManagerService', () => { const key = await repo.createNewAccessKey(); repo.setAccessKeyDataLimit(key.id, {bytes: 1000}); await repo.enforceAccessKeyDataLimits(); - const res = {send: (httpCode) => { - expect(httpCode).toEqual(204); - expect(key.dataLimit).toBeFalsy(); - responseProcessed = true; - done(); - }}; + const res = { + send: (httpCode) => { + expect(httpCode).toEqual(204); + expect(key.dataLimit).toBeFalsy(); + responseProcessed = true; + done(); + }, + }; service.removeAccessKeyDataLimit({params: {id: key.id}}, res, () => {}); }); it('returns 404 for a nonexistent key', async (done) => { const repo = getAccessKeyRepository(); const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build(); await repo.createNewAccessKey(); - service.removeAccessKeyDataLimit({params: {id: "not an id"}}, {send: () => {}}, (error) => { + service.removeAccessKeyDataLimit({params: {id: 'not an id'}}, {send: () => {}}, (error) => { expect(error.statusCode).toEqual(404); responseProcessed = true; done(); @@ -608,9 +645,9 @@ describe('ShadowsocksManagerService', () => { const repo = getAccessKeyRepository(); spyOn(repo, 'setDefaultDataLimit'); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); const limit = {bytes: 10000}; const res = { send: (httpCode, data) => { @@ -618,15 +655,17 @@ describe('ShadowsocksManagerService', () => { expect(serverConfig.data().accessKeyDataLimit).toEqual(limit); expect(repo.setDefaultDataLimit).toHaveBeenCalledWith(limit); service.getServer( - {params: {}}, { - send: (httpCode, data: ServerInfo) => { - expect(httpCode).toEqual(200); - expect(data.accessKeyDataLimit).toEqual(limit); - responseProcessed = true; // required for afterEach to pass. - } + {params: {}}, + { + send: (httpCode, data: ServerInfo) => { + expect(httpCode).toEqual(200); + expect(data.accessKeyDataLimit).toEqual(limit); + responseProcessed = true; // required for afterEach to pass. }, - done); - } + }, + done + ); + }, }; service.setDefaultDataLimit({params: {limit}}, res, done); }); @@ -638,7 +677,7 @@ describe('ShadowsocksManagerService', () => { const res = {send: (httpCode, data) => {}}; service.setDefaultDataLimit({params: {limit}}, res, (error) => { expect(error.statusCode).toEqual(400); - responseProcessed = true; // required for afterEach to pass. + responseProcessed = true; // required for afterEach to pass. done(); }); }); @@ -650,7 +689,7 @@ describe('ShadowsocksManagerService', () => { const res = {send: (httpCode, data) => {}}; service.setDefaultDataLimit({params: {limit}}, res, (error) => { expect(error.statusCode).toEqual(400); - responseProcessed = true; // required for afterEach to pass. + responseProcessed = true; // required for afterEach to pass. done(); }); }); @@ -663,7 +702,7 @@ describe('ShadowsocksManagerService', () => { const res = {send: (httpCode, data) => {}}; service.setDefaultDataLimit({params: {limit}}, res, (error) => { expect(error.statusCode).toEqual(500); - responseProcessed = true; // required for afterEach to pass. + responseProcessed = true; // required for afterEach to pass. done(); }); }); @@ -672,21 +711,21 @@ describe('ShadowsocksManagerService', () => { describe('removeDefaultDataLimit', () => { it('clears default data limit', async (done) => { const limit = {bytes: 10000}; - const serverConfig = new InMemoryConfig({'accessKeyDataLimit': limit} as ServerConfigJson); + const serverConfig = new InMemoryConfig({accessKeyDataLimit: limit} as ServerConfigJson); const repo = getAccessKeyRepository(); spyOn(repo, 'removeDefaultDataLimit').and.callThrough(); const service = new ShadowsocksManagerServiceBuilder() - .serverConfig(serverConfig) - .accessKeys(repo) - .build(); + .serverConfig(serverConfig) + .accessKeys(repo) + .build(); await repo.setDefaultDataLimit(limit); const res = { send: (httpCode, data) => { expect(httpCode).toEqual(204); expect(serverConfig.data().accessKeyDataLimit).toBeUndefined(); expect(repo.removeDefaultDataLimit).toHaveBeenCalled(); - responseProcessed = true; // required for afterEach to pass. - } + responseProcessed = true; // required for afterEach to pass. + }, }; service.removeDefaultDataLimit({params: {}}, res, done); }); @@ -698,7 +737,7 @@ describe('ShadowsocksManagerService', () => { const res = {send: (httpCode, data) => {}}; service.removeDefaultDataLimit({params: {id: accessKey.id}}, res, (error) => { expect(error.statusCode).toEqual(500); - responseProcessed = true; // required for afterEach to pass. + responseProcessed = true; // required for afterEach to pass. done(); }); }); @@ -708,34 +747,40 @@ describe('ShadowsocksManagerService', () => { it('Returns value from sharedMetrics', (done) => { const sharedMetrics = fakeSharedMetricsReporter(); sharedMetrics.startSharing(); - const service = - new ShadowsocksManagerServiceBuilder().metricsPublisher(sharedMetrics).build(); + const service = new ShadowsocksManagerServiceBuilder() + .metricsPublisher(sharedMetrics) + .build(); service.getShareMetrics( - {params: {}}, { - send: (httpCode, data: {metricsEnabled: boolean}) => { - expect(httpCode).toEqual(200); - expect(data.metricsEnabled).toEqual(true); - responseProcessed = true; - } + {params: {}}, + { + send: (httpCode, data: {metricsEnabled: boolean}) => { + expect(httpCode).toEqual(200); + expect(data.metricsEnabled).toEqual(true); + responseProcessed = true; }, - done); + }, + done + ); }); }); describe('setShareMetrics', () => { it('Sets value in the config', (done) => { const sharedMetrics = fakeSharedMetricsReporter(); sharedMetrics.stopSharing(); - const service = - new ShadowsocksManagerServiceBuilder().metricsPublisher(sharedMetrics).build(); + const service = new ShadowsocksManagerServiceBuilder() + .metricsPublisher(sharedMetrics) + .build(); service.setShareMetrics( - {params: {metricsEnabled: true}}, { - send: (httpCode, _) => { - expect(httpCode).toEqual(204); - expect(sharedMetrics.isSharingEnabled()).toEqual(true); - responseProcessed = true; - } + {params: {metricsEnabled: true}}, + { + send: (httpCode, _) => { + expect(httpCode).toEqual(204); + expect(sharedMetrics.isSharingEnabled()).toEqual(true); + responseProcessed = true; }, - done); + }, + done + ); }); }); }); @@ -746,7 +791,7 @@ describe('bindService', () => { let url: URL; const PREFIX = '/TestApiPrefix'; - const fakeResponse = {'foo': 'bar'}; + const fakeResponse = {foo: 'bar'}; const fakeHandler = async (req, res, next) => { res.send(200, fakeResponse); next(); @@ -764,7 +809,7 @@ describe('bindService', () => { }); it('basic routing', async () => { - spyOn(service, "renameServer").and.callFake(fakeHandler); + spyOn(service, 'renameServer').and.callFake(fakeHandler); bindService(server, PREFIX, service); url.pathname = `${PREFIX}/name`; @@ -776,7 +821,7 @@ describe('bindService', () => { }); it('parameterized routing', async () => { - spyOn(service, "removeAccessKeyDataLimit").and.callFake(fakeHandler); + spyOn(service, 'removeAccessKeyDataLimit').and.callFake(fakeHandler); bindService(server, PREFIX, service); url.pathname = `${PREFIX}/access-keys/fake-access-key-id/data-limit`; @@ -796,7 +841,7 @@ describe('bindService', () => { '/123TestApiPrefix', '/very-long-path-that-does-not-exist', `${PREFIX}/does-not-exist`, - ].forEach(path => { + ].forEach((path) => { it(`404 (${path})`, async () => { // Ensure no methods are called on the Service. spyOnAllFunctions(service); @@ -810,7 +855,7 @@ describe('bindService', () => { expect(response.status).toEqual(404); expect(body).toEqual({ code: 'ResourceNotFound', - message: `${path} does not exist` + message: `${path} does not exist`, }); }); }); @@ -819,7 +864,7 @@ describe('bindService', () => { it(`standard routing for authorized queries`, async () => { bindService(server, PREFIX, service); // Verify that ordinary routing goes through the Router. - spyOn(server.router, "lookup").and.callThrough(); + spyOn(server.router, 'lookup').and.callThrough(); // This is an authorized request, so it will pass the prefix filter // and reach the Router. @@ -833,13 +878,7 @@ describe('bindService', () => { // Check that unauthorized queries are rejected without ever reaching // the routing stage. - [ - '/', - '/T', - '/TestApiPre', - '/TestApi123456', - '/TestApi123456789', - ].forEach(path => { + ['/', '/T', '/TestApiPre', '/TestApi123456', '/TestApi123456789'].forEach((path) => { it(`no routing for unauthorized queries (${path})`, async () => { bindService(server, PREFIX, service); // Ensure no methods are called on the Router. @@ -901,13 +940,19 @@ class ShadowsocksManagerServiceBuilder { build(): ShadowsocksManagerService { return new ShadowsocksManagerService( - this.defaultServerName_, this.serverConfig_, this.accessKeys_, this.managerMetrics_, - this.metricsPublisher_); + this.defaultServerName_, + this.serverConfig_, + this.accessKeys_, + this.managerMetrics_, + this.metricsPublisher_ + ); } } async function createNewAccessKeyWithName( - repo: AccessKeyRepository, name: string): Promise { + repo: AccessKeyRepository, + name: string +): Promise { const accessKey = await repo.createNewAccessKey(); try { repo.renameAccessKey(accessKey.id, name); @@ -928,12 +973,16 @@ function fakeSharedMetricsReporter(): SharedMetricsPublisher { }, isSharingEnabled(): boolean { return sharing; - } + }, }; } function getAccessKeyRepository(): ServerAccessKeyRepository { return new ServerAccessKeyRepository( - OLD_PORT, 'hostname', new InMemoryConfig({accessKeys: [], nextId: 0}), - new FakeShadowsocksServer(), new FakePrometheusClient({})); + OLD_PORT, + 'hostname', + new InMemoryConfig({accessKeys: [], nextId: 0}), + new FakeShadowsocksServer(), + new FakePrometheusClient({}) + ); } diff --git a/src/shadowbox/server/manager_service.ts b/src/shadowbox/server/manager_service.ts index 68dd5970..0e54926b 100644 --- a/src/shadowbox/server/manager_service.ts +++ b/src/shadowbox/server/manager_service.ts @@ -40,13 +40,15 @@ function accessKeyToApiJson(accessKey: AccessKey) { port: accessKey.proxyParams.portNumber, method: accessKey.proxyParams.encryptionMethod, dataLimit: accessKey.dataLimit, - accessUrl: SIP002_URI.stringify(makeConfig({ - host: accessKey.proxyParams.hostname, - port: accessKey.proxyParams.portNumber, - method: accessKey.proxyParams.encryptionMethod, - password: accessKey.proxyParams.password, - outline: 1 - })) + accessUrl: SIP002_URI.stringify( + makeConfig({ + host: accessKey.proxyParams.hostname, + port: accessKey.proxyParams.portNumber, + method: accessKey.proxyParams.encryptionMethod, + password: accessKey.proxyParams.password, + outline: 1, + }) + ), }; } @@ -99,30 +101,45 @@ function prefixFilter(apiPrefix: string): restify.RequestHandler { } export function bindService( - apiServer: restify.Server, apiPrefix: string, service: ShadowsocksManagerService) { + apiServer: restify.Server, + apiPrefix: string, + service: ShadowsocksManagerService +) { // Reject unauthorized requests in constant time before they reach the routing step. apiServer.pre(prefixFilter(apiPrefix)); apiServer.put(`${apiPrefix}/name`, service.renameServer.bind(service)); apiServer.get(`${apiPrefix}/server`, service.getServer.bind(service)); apiServer.put( - `${apiPrefix}/server/access-key-data-limit`, service.setDefaultDataLimit.bind(service)); + `${apiPrefix}/server/access-key-data-limit`, + service.setDefaultDataLimit.bind(service) + ); apiServer.del( - `${apiPrefix}/server/access-key-data-limit`, service.removeDefaultDataLimit.bind(service)); + `${apiPrefix}/server/access-key-data-limit`, + service.removeDefaultDataLimit.bind(service) + ); apiServer.put( - `${apiPrefix}/server/hostname-for-access-keys`, - service.setHostnameForAccessKeys.bind(service)); + `${apiPrefix}/server/hostname-for-access-keys`, + service.setHostnameForAccessKeys.bind(service) + ); apiServer.put( - `${apiPrefix}/server/port-for-new-access-keys`, - service.setPortForNewAccessKeys.bind(service)); + `${apiPrefix}/server/port-for-new-access-keys`, + service.setPortForNewAccessKeys.bind(service) + ); apiServer.post(`${apiPrefix}/access-keys`, service.createNewAccessKey.bind(service)); apiServer.get(`${apiPrefix}/access-keys`, service.listAccessKeys.bind(service)); apiServer.del(`${apiPrefix}/access-keys/:id`, service.removeAccessKey.bind(service)); apiServer.put(`${apiPrefix}/access-keys/:id/name`, service.renameAccessKey.bind(service)); - apiServer.put(`${apiPrefix}/access-keys/:id/data-limit`, service.setAccessKeyDataLimit.bind(service)); - apiServer.del(`${apiPrefix}/access-keys/:id/data-limit`, service.removeAccessKeyDataLimit.bind(service)); + apiServer.put( + `${apiPrefix}/access-keys/:id/data-limit`, + service.setAccessKeyDataLimit.bind(service) + ); + apiServer.del( + `${apiPrefix}/access-keys/:id/data-limit`, + service.removeAccessKeyDataLimit.bind(service) + ); apiServer.get(`${apiPrefix}/metrics/transfer`, service.getDataUsage.bind(service)); apiServer.get(`${apiPrefix}/metrics/enabled`, service.getShareMetrics.bind(service)); @@ -130,11 +147,13 @@ export function bindService( // Redirect former experimental APIs apiServer.put( - `${apiPrefix}/experimental/access-key-data-limit`, - redirect(`${apiPrefix}/server/access-key-data-limit`)); + `${apiPrefix}/experimental/access-key-data-limit`, + redirect(`${apiPrefix}/server/access-key-data-limit`) + ); apiServer.del( - `${apiPrefix}/experimental/access-key-data-limit`, - redirect(`${apiPrefix}/server/access-key-data-limit`)); + `${apiPrefix}/experimental/access-key-data-limit`, + redirect(`${apiPrefix}/server/access-key-data-limit`) + ); } // Returns a request handler that redirects a bound request path to `url` with HTTP status code 308. @@ -150,20 +169,23 @@ function validateAccessKeyId(accessKeyId: unknown): string { throw new restifyErrors.MissingParameterError({statusCode: 400}, 'Parameter `id` is missing'); } else if (typeof accessKeyId !== 'string') { throw new restifyErrors.InvalidArgumentError( - {statusCode: 400}, 'Parameter `id` must be of type string'); + {statusCode: 400}, + 'Parameter `id` must be of type string' + ); } return accessKeyId; } function validateDataLimit(limit: unknown): DataLimit { if (!limit) { - throw new restifyErrors.MissingParameterError( - {statusCode: 400}, 'Missing `limit` parameter'); + throw new restifyErrors.MissingParameterError({statusCode: 400}, 'Missing `limit` parameter'); } const bytes = (limit as DataLimit).bytes; if (!(Number.isInteger(bytes) && bytes >= 0)) { throw new restifyErrors.InvalidArgumentError( - {statusCode: 400}, '`limit.bytes` must be an non-negative integer'); + {statusCode: 400}, + '`limit.bytes` must be an non-negative integer' + ); } return limit as DataLimit; } @@ -173,20 +195,27 @@ function validateDataLimit(limit: unknown): DataLimit { // for each existing access key, with the port and password assigned for that access key. export class ShadowsocksManagerService { constructor( - private defaultServerName: string, private serverConfig: JsonConfig, - private accessKeys: AccessKeyRepository, private managerMetrics: ManagerMetrics, - private metricsPublisher: SharedMetricsPublisher) {} + private defaultServerName: string, + private serverConfig: JsonConfig, + private accessKeys: AccessKeyRepository, + private managerMetrics: ManagerMetrics, + private metricsPublisher: SharedMetricsPublisher + ) {} public renameServer(req: RequestType, res: ResponseType, next: restify.Next): void { logging.debug(`renameServer request ${JSON.stringify(req.params)}`); const name = req.params.name; if (!name) { - return next(new restifyErrors.MissingParameterError( - {statusCode: 400}, 'Parameter `name` is missing')); + return next( + new restifyErrors.MissingParameterError({statusCode: 400}, 'Parameter `name` is missing') + ); } if (typeof name !== 'string' || name.length > 100) { - next(new restifyErrors.InvalidArgumentError( - `Requested server name should be a string <= 100 characters long. Got ${name}`)); + next( + new restifyErrors.InvalidArgumentError( + `Requested server name should be a string <= 100 characters long. Got ${name}` + ) + ); return; } this.serverConfig.data().name = name; @@ -204,7 +233,7 @@ export class ShadowsocksManagerService { version, accessKeyDataLimit: this.serverConfig.data().accessKeyDataLimit, portForNewAccessKeys: this.serverConfig.data().portForNewAccessKeys, - hostnameForAccessKeys: this.serverConfig.data().hostname + hostnameForAccessKeys: this.serverConfig.data().hostname, }); next(); } @@ -216,20 +245,28 @@ export class ShadowsocksManagerService { const hostname = req.params.hostname; if (typeof hostname === 'undefined') { return next( - new restifyErrors.MissingParameterError({statusCode: 400}, 'hostname must be provided')); + new restifyErrors.MissingParameterError({statusCode: 400}, 'hostname must be provided') + ); } if (typeof hostname !== 'string') { - return next(new restifyErrors.InvalidArgumentError( + return next( + new restifyErrors.InvalidArgumentError( {statusCode: 400}, - `Expected hostname to be a string, instead got ${hostname} of type ${typeof hostname}`)); + `Expected hostname to be a string, instead got ${hostname} of type ${typeof hostname}` + ) + ); } // Hostnames can have any number of segments of alphanumeric characters and hyphens, separated // by periods. No segment may start or end with a hyphen. const hostnameRegex = - /^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)*[A-Za-z0-9]([A-Za-z0-9\-]*[A-Za-z0-9])?$/; + /^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)*[A-Za-z0-9]([A-Za-z0-9\-]*[A-Za-z0-9])?$/; if (!hostnameRegex.test(hostname) && !ipRegex({includeBoundaries: true}).test(hostname)) { - return next(new restifyErrors.InvalidArgumentError( - {statusCode: 400}, `Hostname ${hostname} isn't a valid hostname or IP address`)); + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + `Hostname ${hostname} isn't a valid hostname or IP address` + ) + ); } this.serverConfig.data().hostname = hostname; @@ -268,18 +305,25 @@ export class ShadowsocksManagerService { } // Sets the default ports for new access keys - public async setPortForNewAccessKeys(req: RequestType, res: ResponseType, next: restify.Next): - Promise { + public async setPortForNewAccessKeys( + req: RequestType, + res: ResponseType, + next: restify.Next + ): Promise { try { logging.debug(`setPortForNewAccessKeys request ${JSON.stringify(req.params)}`); const port = req.params.port; if (!port) { - return next(new restifyErrors.MissingParameterError( - {statusCode: 400}, 'Parameter `port` is missing')); + return next( + new restifyErrors.MissingParameterError({statusCode: 400}, 'Parameter `port` is missing') + ); } else if (typeof port !== 'number') { - return next(new restifyErrors.InvalidArgumentError( + return next( + new restifyErrors.InvalidArgumentError( {statusCode: 400}, - `Expected a numeric port, instead got ${port} of type ${typeof port}`)); + `Expected a numeric port, instead got ${port} of type ${typeof port}` + ) + ); } await this.accessKeys.setPortForNewAccessKeys(port); this.serverConfig.data().portForNewAccessKeys = port; @@ -322,11 +366,16 @@ export class ShadowsocksManagerService { const accessKeyId = validateAccessKeyId(req.params.id); const name = req.params.name; if (!name) { - return next(new restifyErrors.MissingParameterError( - {statusCode: 400}, 'Parameter `name` is missing')); + return next( + new restifyErrors.MissingParameterError({statusCode: 400}, 'Parameter `name` is missing') + ); } else if (typeof name !== 'string') { - return next(new restifyErrors.InvalidArgumentError( - {statusCode: 400}, 'Parameter `name` must be of type string')); + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + 'Parameter `name` must be of type string' + ) + ); } this.accessKeys.renameAccessKey(accessKeyId, name); res.send(HttpSuccess.NO_CONTENT); @@ -352,7 +401,7 @@ export class ShadowsocksManagerService { this.accessKeys.setAccessKeyDataLimit(accessKeyId, limit); res.send(HttpSuccess.NO_CONTENT); return next(); - } catch(error) { + } catch (error) { logging.error(error); if (error instanceof errors.AccessKeyNotFound) { return next(new restifyErrors.NotFoundError(error.message)); @@ -370,7 +419,7 @@ export class ShadowsocksManagerService { this.accessKeys.removeAccessKeyDataLimit(accessKeyId); res.send(HttpSuccess.NO_CONTENT); return next(); - } catch(error) { + } catch (error) { logging.error(error); if (error instanceof errors.AccessKeyNotFound) { return next(new restifyErrors.NotFoundError(error.message)); @@ -392,7 +441,10 @@ export class ShadowsocksManagerService { return next(); } catch (error) { logging.error(error); - if (error instanceof restifyErrors.InvalidArgumentError || error instanceof restifyErrors.MissingParameterError) { + if ( + error instanceof restifyErrors.InvalidArgumentError || + error instanceof restifyErrors.MissingParameterError + ) { return next(error); } return next(new restifyErrors.InternalServerError()); @@ -440,11 +492,19 @@ export class ShadowsocksManagerService { logging.debug(`setShareMetrics request ${JSON.stringify(req.params)}`); const metricsEnabled = req.params.metricsEnabled; if (metricsEnabled === undefined || metricsEnabled === null) { - return next(new restifyErrors.MissingParameterError( - {statusCode: 400}, 'Parameter `metricsEnabled` is missing')); + return next( + new restifyErrors.MissingParameterError( + {statusCode: 400}, + 'Parameter `metricsEnabled` is missing' + ) + ); } else if (typeof metricsEnabled !== 'boolean') { - return next(new restifyErrors.InvalidArgumentError( - {statusCode: 400}, 'Parameter `hours` must be an integer')); + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + 'Parameter `hours` must be an integer' + ) + ); } if (metricsEnabled) { this.metricsPublisher.startSharing(); diff --git a/src/shadowbox/server/mocks/mocks.ts b/src/shadowbox/server/mocks/mocks.ts index a103816b..124bf809 100644 --- a/src/shadowbox/server/mocks/mocks.ts +++ b/src/shadowbox/server/mocks/mocks.ts @@ -58,8 +58,10 @@ export class FakePrometheusClient extends PrometheusClient { const queryResultData = {result: []} as QueryResultData; for (const accessKeyId of Object.keys(this.bytesTransferredById)) { const bytesTransferred = this.bytesTransferredById[accessKeyId] || 0; - queryResultData.result.push( - {metric: {'access_key': accessKeyId}, value: [bytesTransferred, `${bytesTransferred}`]}); + queryResultData.result.push({ + metric: {access_key: accessKeyId}, + value: [bytesTransferred, `${bytesTransferred}`], + }); } return queryResultData; } diff --git a/src/shadowbox/server/outline_shadowsocks_server.ts b/src/shadowbox/server/outline_shadowsocks_server.ts index 4fe1b03d..27ff14bf 100644 --- a/src/shadowbox/server/outline_shadowsocks_server.ts +++ b/src/shadowbox/server/outline_shadowsocks_server.ts @@ -30,8 +30,11 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { // binaryFilename is the location for the outline-ss-server binary. // configFilename is the location for the outline-ss-server config. constructor( - private readonly binaryFilename: string, private readonly configFilename: string, - private readonly verbose: boolean, private readonly metricsLocation: string) {} + private readonly binaryFilename: string, + private readonly configFilename: string, + private readonly verbose: boolean, + private readonly metricsLocation: string + ) {} // Annotates the Prometheus data metrics with countries. // ipCountryFilename is the location of the ip-country.mmdb IP-to-country database file. @@ -64,8 +67,9 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { const keysJson = {keys: [] as ShadowsocksAccessKey[]}; for (const key of keys) { if (!isAeadCipher(key.cipher)) { - logging.error(`Cipher ${key.cipher} for access key ${ - key.id} is not supported: use an AEAD cipher instead.`); + logging.error( + `Cipher ${key.cipher} for access key ${key.id} is not supported: use an AEAD cipher instead.` + ); continue; } @@ -114,4 +118,4 @@ export class OutlineShadowsocksServer implements ShadowsocksServer { function isAeadCipher(cipherAlias: string) { cipherAlias = cipherAlias.toLowerCase(); return cipherAlias.endsWith('gcm') || cipherAlias.endsWith('poly1305'); -} \ No newline at end of file +} diff --git a/src/shadowbox/server/server_access_key.spec.ts b/src/shadowbox/server/server_access_key.spec.ts index 468c5ac5..c29e2d55 100644 --- a/src/shadowbox/server/server_access_key.spec.ts +++ b/src/shadowbox/server/server_access_key.spec.ts @@ -81,8 +81,9 @@ describe('ServerAccessKeyRepository', () => { const repo = new RepoBuilder().build(); repo.createNewAccessKey().then((accessKey) => { const NEW_NAME = 'newName'; - expect(repo.renameAccessKey.bind(repo, 'badId', NEW_NAME)) - .toThrowError(errors.AccessKeyNotFound); + expect(repo.renameAccessKey.bind(repo, 'badId', NEW_NAME)).toThrowError( + errors.AccessKeyNotFound + ); // List keys again and expect to NOT see the NEW_NAME. const accessKeys = repo.listAccessKeys(); expect(accessKeys[0].name).not.toEqual(NEW_NAME); @@ -126,9 +127,13 @@ describe('ServerAccessKeyRepository', () => { await expectAsyncThrow(repo.setPortForNewAccessKeys.bind(repo, 0), errors.InvalidPortNumber); await expectAsyncThrow(repo.setPortForNewAccessKeys.bind(repo, -1), errors.InvalidPortNumber); await expectAsyncThrow( - repo.setPortForNewAccessKeys.bind(repo, 100.1), errors.InvalidPortNumber); + repo.setPortForNewAccessKeys.bind(repo, 100.1), + errors.InvalidPortNumber + ); await expectAsyncThrow( - repo.setPortForNewAccessKeys.bind(repo, 65536), errors.InvalidPortNumber); + repo.setPortForNewAccessKeys.bind(repo, 65536), + errors.InvalidPortNumber + ); done(); }); @@ -164,8 +169,8 @@ describe('ServerAccessKeyRepository', () => { done(); }); }); - - it('setAccessKeyDataLimit can set a custom data limit', async(done) => { + + it('setAccessKeyDataLimit can set a custom data limit', async (done) => { const server = new FakeShadowsocksServer(); const config = new InMemoryConfig({accessKeys: [], nextId: 0}); const repo = new RepoBuilder().shadowsocksServer(server).keyConfig(config).build(); @@ -178,18 +183,23 @@ describe('ServerAccessKeyRepository', () => { }); async function setKeyLimitAndEnforce( - repo: ServerAccessKeyRepository, id: AccessKeyId, limit: DataLimit) { + repo: ServerAccessKeyRepository, + id: AccessKeyId, + limit: DataLimit + ) { repo.setAccessKeyDataLimit(id, limit); // We enforce asynchronously, in setAccessKeyDataLimit, so explicitly call it here to make sure // enforcement is done before we make assertions. return repo.enforceAccessKeyDataLimits(); } - it('setAccessKeyDataLimit can change a key\'s limit status', async(done) => { + it("setAccessKeyDataLimit can change a key's limit status", async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); await repo.start(new ManualClock()); const key = await repo.createNewAccessKey(); await setKeyLimitAndEnforce(repo, key.id, {bytes: 0}); @@ -206,12 +216,14 @@ describe('ServerAccessKeyRepository', () => { expect(serverKeys[0].id).toEqual(key.id); done(); }); - - it('setAccessKeyDataLimit overrides default data limit', async(done) => { + + it('setAccessKeyDataLimit overrides default data limit', async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 750, '1': 1250}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); await repo.start(new ManualClock()); const lowerLimitThanDefault = await repo.createNewAccessKey(); const higherLimitThanDefault = await repo.createNewAccessKey(); @@ -242,8 +254,10 @@ describe('ServerAccessKeyRepository', () => { it('removeAccessKeyDataLimit restores a key to the default data limit', async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); const key = await repo.createNewAccessKey(); await repo.start(new ManualClock()); await repo.setDefaultDataLimit({bytes: 0}); @@ -255,11 +269,13 @@ describe('ServerAccessKeyRepository', () => { done(); }); - it('setAccessKeyDataLimit can change a key\'s limit status', async (done) => { + it("setAccessKeyDataLimit can change a key's limit status", async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); await repo.start(new ManualClock()); const key = await repo.createNewAccessKey(); await setKeyLimitAndEnforce(repo, key.id, {bytes: 0}); @@ -280,8 +296,10 @@ describe('ServerAccessKeyRepository', () => { it('setAccessKeyDataLimit overrides default data limit', async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 750, '1': 1250}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); await repo.start(new ManualClock()); const lowerLimitThanDefault = await repo.createNewAccessKey(); const higherLimitThanDefault = await repo.createNewAccessKey(); @@ -319,8 +337,10 @@ describe('ServerAccessKeyRepository', () => { it('removeAccessKeyDataLimit restores a key to the default data limit', async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); const key = await repo.createNewAccessKey(); await repo.start(new ManualClock()); await repo.setDefaultDataLimit({bytes: 0}); @@ -332,11 +352,13 @@ describe('ServerAccessKeyRepository', () => { done(); }); - it('removeAccessKeyDataLimit can restore an over-limit access key', async(done) => { + it('removeAccessKeyDataLimit can restore an over-limit access key', async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); const key = await repo.createNewAccessKey(); await repo.start(new ManualClock()); @@ -349,7 +371,7 @@ describe('ServerAccessKeyRepository', () => { expect(server.getAccessKeys().length).toEqual(1); done(); }); - + it('can set default data limit', async (done) => { const repo = new RepoBuilder().build(); const limit = {bytes: 5000}; @@ -361,8 +383,10 @@ describe('ServerAccessKeyRepository', () => { it('setDefaultDataLimit updates keys limit status', async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500, '1': 200}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); const accessKey1 = await repo.createNewAccessKey(); const accessKey2 = await repo.createNewAccessKey(); await repo.start(new ManualClock()); @@ -404,10 +428,10 @@ describe('ServerAccessKeyRepository', () => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500, '1': 100}); const repo = new RepoBuilder() - .prometheusClient(prometheusClient) - .shadowsocksServer(server) - .defaultDataLimit({bytes: 200}) - .build(); + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .defaultDataLimit({bytes: 200}) + .build(); const accessKey1 = await repo.createNewAccessKey(); const accessKey2 = await repo.createNewAccessKey(); @@ -426,34 +450,45 @@ describe('ServerAccessKeyRepository', () => { }); it('enforceAccessKeyDataLimits updates keys limit status', async (done) => { - const prometheusClient = - new FakePrometheusClient({'0': 100, '1': 200, '2': 300, '3': 400, '4': 500}); + const prometheusClient = new FakePrometheusClient({ + '0': 100, + '1': 200, + '2': 300, + '3': 400, + '4': 500, + }); const limit = {bytes: 250}; - const repo = - new RepoBuilder().prometheusClient(prometheusClient).defaultDataLimit(limit).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .defaultDataLimit(limit) + .build(); for (let i = 0; i < Object.keys(prometheusClient.bytesTransferredById).length; ++i) { await repo.createNewAccessKey(); } await repo.enforceAccessKeyDataLimits(); for (const key of repo.listAccessKeys()) { - expect(key.isOverDataLimit) - .toEqual(prometheusClient.bytesTransferredById[key.id] > limit.bytes); + expect(key.isOverDataLimit).toEqual( + prometheusClient.bytesTransferredById[key.id] > limit.bytes + ); } // Simulate a change in usage. prometheusClient.bytesTransferredById = {'0': 500, '1': 400, '2': 300, '3': 200, '4': 100}; await repo.enforceAccessKeyDataLimits(); for (const key of repo.listAccessKeys()) { - expect(key.isOverDataLimit) - .toEqual(prometheusClient.bytesTransferredById[key.id] > limit.bytes); + expect(key.isOverDataLimit).toEqual( + prometheusClient.bytesTransferredById[key.id] > limit.bytes + ); } done(); }); it('enforceAccessKeyDataLimits respects both default and per-key limits', async (done) => { const prometheusClient = new FakePrometheusClient({'0': 200, '1': 300}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).defaultDataLimit({bytes: 500}).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .defaultDataLimit({bytes: 500}) + .build(); const perKeyLimited = await repo.createNewAccessKey(); const defaultLimited = await repo.createNewAccessKey(); await setKeyLimitAndEnforce(repo, perKeyLimited.id, {bytes: 100}); @@ -475,10 +510,10 @@ describe('ServerAccessKeyRepository', () => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500, '1': 100}); const repo = new RepoBuilder() - .prometheusClient(prometheusClient) - .shadowsocksServer(server) - .defaultDataLimit({bytes: 200}) - .build(); + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .defaultDataLimit({bytes: 200}) + .build(); const accessKey1 = await repo.createNewAccessKey(); const accessKey2 = await repo.createNewAccessKey(); @@ -552,8 +587,10 @@ describe('ServerAccessKeyRepository', () => { it('start periodically enforces access key data limits', async (done) => { const server = new FakeShadowsocksServer(); const prometheusClient = new FakePrometheusClient({'0': 500, '1': 200, '2': 400}); - const repo = - new RepoBuilder().prometheusClient(prometheusClient).shadowsocksServer(server).build(); + const repo = new RepoBuilder() + .prometheusClient(prometheusClient) + .shadowsocksServer(server) + .build(); const accessKey1 = await repo.createNewAccessKey(); const accessKey2 = await repo.createNewAccessKey(); const accessKey3 = await repo.createNewAccessKey(); @@ -650,7 +687,12 @@ class RepoBuilder { public build(): ServerAccessKeyRepository { return new ServerAccessKeyRepository( - this.port_, 'hostname', this.keyConfig_, this.shadowsocksServer_, this.prometheusClient_, - this.defaultDataLimit_); + this.port_, + 'hostname', + this.keyConfig_, + this.shadowsocksServer_, + this.prometheusClient_, + this.defaultDataLimit_ + ); } } diff --git a/src/shadowbox/server/server_access_key.ts b/src/shadowbox/server/server_access_key.ts index d9a2e2c8..ddb0c562 100644 --- a/src/shadowbox/server/server_access_key.ts +++ b/src/shadowbox/server/server_access_key.ts @@ -20,7 +20,14 @@ import {isPortUsed} from '../infrastructure/get_port'; import {JsonConfig} from '../infrastructure/json_config'; import * as logging from '../infrastructure/logging'; import {PrometheusClient} from '../infrastructure/prometheus_scraper'; -import {AccessKey, AccessKeyId, AccessKeyMetricsId, AccessKeyRepository, DataLimit, ProxyParams} from '../model/access_key'; +import { + AccessKey, + AccessKeyId, + AccessKeyMetricsId, + AccessKeyRepository, + DataLimit, + ProxyParams, +} from '../model/access_key'; import * as errors from '../model/errors'; import {ShadowsocksServer} from '../model/shadowsocks_server'; import {PrometheusManagerMetrics} from './manager_metrics'; @@ -47,8 +54,12 @@ export interface AccessKeyConfigJson { class ServerAccessKey implements AccessKey { public isOverDataLimit = false; constructor( - readonly id: AccessKeyId, public name: string, public metricsId: AccessKeyMetricsId, - readonly proxyParams: ProxyParams, public dataLimit?: DataLimit) {} + readonly id: AccessKeyId, + public name: string, + public metricsId: AccessKeyMetricsId, + readonly proxyParams: ProxyParams, + public dataLimit?: DataLimit + ) {} } // Generates a random password for Shadowsocks access keys. @@ -64,7 +75,12 @@ function makeAccessKey(hostname: string, accessKeyJson: AccessKeyStorageJson): A password: accessKeyJson.password, }; return new ServerAccessKey( - accessKeyJson.id, accessKeyJson.name, accessKeyJson.metricsId, proxyParams, accessKeyJson.dataLimit); + accessKeyJson.id, + accessKeyJson.name, + accessKeyJson.metricsId, + proxyParams, + accessKeyJson.dataLimit + ); } function accessKeyToStorageJson(accessKey: AccessKey): AccessKeyStorageJson { @@ -75,7 +91,7 @@ function accessKeyToStorageJson(accessKey: AccessKey): AccessKeyStorageJson { password: accessKey.proxyParams.password, port: accessKey.proxyParams.portNumber, encryptionMethod: accessKey.proxyParams.encryptionMethod, - dataLimit: accessKey.dataLimit + dataLimit: accessKey.dataLimit, }; } @@ -83,15 +99,18 @@ function accessKeyToStorageJson(accessKey: AccessKey): AccessKeyStorageJson { // to start and stop per-access-key Shadowsocks instances. Requires external validation // that portForNewAccessKeys is valid. export class ServerAccessKeyRepository implements AccessKeyRepository { - private static DATA_LIMITS_ENFORCEMENT_INTERVAL_MS = 60 * 60 * 1000; // 1h + private static DATA_LIMITS_ENFORCEMENT_INTERVAL_MS = 60 * 60 * 1000; // 1h private NEW_USER_ENCRYPTION_METHOD = 'chacha20-ietf-poly1305'; private accessKeys: ServerAccessKey[]; constructor( - private portForNewAccessKeys: number, private proxyHostname: string, - private keyConfig: JsonConfig, - private shadowsocksServer: ShadowsocksServer, private prometheusClient: PrometheusClient, - private _defaultDataLimit?: DataLimit) { + private portForNewAccessKeys: number, + private proxyHostname: string, + private keyConfig: JsonConfig, + private shadowsocksServer: ShadowsocksServer, + private prometheusClient: PrometheusClient, + private _defaultDataLimit?: DataLimit + ) { if (this.keyConfig.data().accessKeys === undefined) { this.keyConfig.data().accessKeys = []; } @@ -114,7 +133,9 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { await tryEnforceDataLimits(); await this.updateServer(); clock.setInterval( - tryEnforceDataLimits, ServerAccessKeyRepository.DATA_LIMITS_ENFORCEMENT_INTERVAL_MS); + tryEnforceDataLimits, + ServerAccessKeyRepository.DATA_LIMITS_ENFORCEMENT_INTERVAL_MS + ); } private isExistingAccessKeyPort(port: number): boolean { @@ -131,7 +152,7 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new errors.InvalidPortNumber(port.toString()); } - if (!this.isExistingAccessKeyPort(port) && await isPortUsed(port)) { + if (!this.isExistingAccessKeyPort(port) && (await isPortUsed(port))) { throw new errors.PortUnavailable(port); } this.portForNewAccessKeys = port; @@ -169,7 +190,7 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { } listAccessKeys(): AccessKey[] { - return [...this.accessKeys]; // Return a copy of the access key array. + return [...this.accessKeys]; // Return a copy of the access key array. } renameAccessKey(id: AccessKeyId, name: string) { @@ -190,7 +211,7 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { this.enforceAccessKeyDataLimits(); } - get defaultDataLimit(): DataLimit|undefined { + get defaultDataLimit(): DataLimit | undefined { return this._defaultDataLimit; } @@ -204,7 +225,7 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { this.enforceAccessKeyDataLimits(); } - getMetricsId(id: AccessKeyId): AccessKeyMetricsId|undefined { + getMetricsId(id: AccessKeyId): AccessKeyMetricsId | undefined { const accessKey = this.getAccessKey(id); return accessKey ? accessKey.metricsId : undefined; } @@ -213,8 +234,8 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { // Updates access key data usage. async enforceAccessKeyDataLimits() { const metrics = new PrometheusManagerMetrics(this.prometheusClient); - const bytesTransferredById = - (await metrics.getOutboundByteTransfer({hours: 30 * 24})).bytesTransferredByUserId; + const bytesTransferredById = (await metrics.getOutboundByteTransfer({hours: 30 * 24})) + .bytesTransferredByUserId; let limitStatusChanged = false; for (const accessKey of this.accessKeys) { const usageBytes = bytesTransferredById[accessKey.id] ?? 0; @@ -232,23 +253,25 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { } private updateServer(): Promise { - const serverAccessKeys = this.accessKeys.filter(key => !key.isOverDataLimit).map(key => { - return { - id: key.id, - port: key.proxyParams.portNumber, - cipher: key.proxyParams.encryptionMethod, - secret: key.proxyParams.password - }; - }); + const serverAccessKeys = this.accessKeys + .filter((key) => !key.isOverDataLimit) + .map((key) => { + return { + id: key.id, + port: key.proxyParams.portNumber, + cipher: key.proxyParams.encryptionMethod, + secret: key.proxyParams.password, + }; + }); return this.shadowsocksServer.update(serverAccessKeys); } private loadAccessKeys(): AccessKey[] { - return this.keyConfig.data().accessKeys.map(key => makeAccessKey(this.proxyHostname, key)); + return this.keyConfig.data().accessKeys.map((key) => makeAccessKey(this.proxyHostname, key)); } private saveAccessKeys() { - this.keyConfig.data().accessKeys = this.accessKeys.map(key => accessKeyToStorageJson(key)); + this.keyConfig.data().accessKeys = this.accessKeys.map((key) => accessKeyToStorageJson(key)); this.keyConfig.write(); } diff --git a/src/shadowbox/server/shared_metrics.spec.ts b/src/shadowbox/server/shared_metrics.spec.ts index 6b2fb03b..707decbe 100644 --- a/src/shadowbox/server/shared_metrics.spec.ts +++ b/src/shadowbox/server/shared_metrics.spec.ts @@ -19,15 +19,28 @@ import {version} from '../package.json'; import {AccessKeyConfigJson} from './server_access_key'; import {ServerConfigJson} from './server_config'; -import {DailyFeatureMetricsReportJson, HourlyServerMetricsReportJson, KeyUsage, MetricsCollectorClient, OutlineSharedMetricsPublisher, UsageMetrics} from './shared_metrics'; +import { + DailyFeatureMetricsReportJson, + HourlyServerMetricsReportJson, + KeyUsage, + MetricsCollectorClient, + OutlineSharedMetricsPublisher, + UsageMetrics, +} from './shared_metrics'; describe('OutlineSharedMetricsPublisher', () => { describe('Enable/Disable', () => { it('Mirrors config', () => { const serverConfig = new InMemoryConfig({}); - const publisher = - new OutlineSharedMetricsPublisher(new ManualClock(), serverConfig, null, null, null, null); + const publisher = new OutlineSharedMetricsPublisher( + new ManualClock(), + serverConfig, + null, + null, + null, + null + ); expect(publisher.isSharingEnabled()).toBeFalsy(); publisher.startSharing(); @@ -40,8 +53,14 @@ describe('OutlineSharedMetricsPublisher', () => { }); it('Reads from config', () => { const serverConfig = new InMemoryConfig({metricsEnabled: true}); - const publisher = - new OutlineSharedMetricsPublisher(new ManualClock(), serverConfig, null, null, null, null); + const publisher = new OutlineSharedMetricsPublisher( + new ManualClock(), + serverConfig, + null, + null, + null, + null + ); expect(publisher.isSharingEnabled()).toBeTruthy(); }); }); @@ -54,7 +73,13 @@ describe('OutlineSharedMetricsPublisher', () => { const toMetricsId = (id: AccessKeyId) => `M(${id})`; const metricsCollector = new FakeMetricsCollector(); const publisher = new OutlineSharedMetricsPublisher( - clock, serverConfig, null, usageMetrics, toMetricsId, metricsCollector); + clock, + serverConfig, + null, + usageMetrics, + toMetricsId, + metricsCollector + ); publisher.startSharing(); usageMetrics.usage = [ @@ -73,7 +98,7 @@ describe('OutlineSharedMetricsPublisher', () => { {userId: 'M(user-0)', bytesTransferred: 11, countries: ['AA', 'BB']}, {userId: 'M(user-1)', bytesTransferred: 22, countries: ['CC']}, {userId: 'M(user-0)', bytesTransferred: 33, countries: ['AA', 'DD']}, - ] + ], }); startTime = clock.nowMs; @@ -90,8 +115,8 @@ describe('OutlineSharedMetricsPublisher', () => { endUtcMs: clock.nowMs, userReports: [ {userId: 'M(user-0)', bytesTransferred: 44, countries: ['EE']}, - {userId: 'M(user-2)', bytesTransferred: 55, countries: ['FF']} - ] + {userId: 'M(user-2)', bytesTransferred: 55, countries: ['FF']}, + ], }); publisher.stopSharing(); @@ -104,7 +129,13 @@ describe('OutlineSharedMetricsPublisher', () => { const toMetricsId = (id: AccessKeyId) => `M(${id})`; const metricsCollector = new FakeMetricsCollector(); const publisher = new OutlineSharedMetricsPublisher( - clock, serverConfig, null, usageMetrics, toMetricsId, metricsCollector); + clock, + serverConfig, + null, + usageMetrics, + toMetricsId, + metricsCollector + ); publisher.startSharing(); usageMetrics.usage = [ @@ -122,7 +153,7 @@ describe('OutlineSharedMetricsPublisher', () => { userReports: [ {userId: 'M(user-1)', bytesTransferred: 22, countries: ['CC']}, {userId: 'M(user-0)', bytesTransferred: 33, countries: ['AA', 'DD']}, - ] + ], }); publisher.stopSharing(); }); @@ -130,28 +161,33 @@ describe('OutlineSharedMetricsPublisher', () => { it('reports feature metrics correctly', async () => { const clock = new ManualClock(); let timestamp = clock.nowMs; - const serverConfig = new InMemoryConfig( - {serverId: 'server-id', accessKeyDataLimit: {bytes: 123}}); + const serverConfig = new InMemoryConfig({ + serverId: 'server-id', + accessKeyDataLimit: {bytes: 123}, + }); let keyId = 0; const makeKeyJson = (dataLimit?: DataLimit) => { return { id: (keyId++).toString(), - metricsId: "id", - name: "name", - password: "pass", + metricsId: 'id', + name: 'name', + password: 'pass', port: 12345, dataLimit, }; }; const keyConfig = new InMemoryConfig({ - accessKeys: [ - makeKeyJson({bytes: 2}), - makeKeyJson() - ] + accessKeys: [makeKeyJson({bytes: 2}), makeKeyJson()], }); const metricsCollector = new FakeMetricsCollector(); const publisher = new OutlineSharedMetricsPublisher( - clock, serverConfig, keyConfig, new ManualUsageMetrics(), (id: AccessKeyId) => '', metricsCollector); + clock, + serverConfig, + keyConfig, + new ManualUsageMetrics(), + (id: AccessKeyId) => '', + metricsCollector + ); publisher.startSharing(); await clock.runCallbacks(); @@ -161,8 +197,8 @@ describe('OutlineSharedMetricsPublisher', () => { timestampUtcMs: timestamp, dataLimit: { enabled: true, - perKeyLimitCount: 1 - } + perKeyLimitCount: 1, + }, }); clock.nowMs += 24 * 60 * 60 * 1000; timestamp = clock.nowMs; @@ -175,8 +211,8 @@ describe('OutlineSharedMetricsPublisher', () => { timestampUtcMs: timestamp, dataLimit: { enabled: false, - perKeyLimitCount: 1 - } + perKeyLimitCount: 1, + }, }); clock.nowMs += 24 * 60 * 60 * 1000; @@ -186,13 +222,21 @@ describe('OutlineSharedMetricsPublisher', () => { }); it('does not report metrics when sharing is disabled', async () => { const clock = new ManualClock(); - const serverConfig = - new InMemoryConfig({serverId: 'server-id', metricsEnabled: false}); + const serverConfig = new InMemoryConfig({ + serverId: 'server-id', + metricsEnabled: false, + }); const metricsCollector = new FakeMetricsCollector(); spyOn(metricsCollector, 'collectServerUsageMetrics').and.callThrough(); spyOn(metricsCollector, 'collectFeatureMetrics').and.callThrough(); const publisher = new OutlineSharedMetricsPublisher( - clock, serverConfig, new InMemoryConfig({}), new ManualUsageMetrics(), (id: AccessKeyId) => '', metricsCollector); + clock, + serverConfig, + new InMemoryConfig({}), + new ManualUsageMetrics(), + (id: AccessKeyId) => '', + metricsCollector + ); await clock.runCallbacks(); expect(metricsCollector.collectServerUsageMetrics).not.toHaveBeenCalled(); diff --git a/src/shadowbox/server/shared_metrics.ts b/src/shadowbox/server/shared_metrics.ts index 9f865970..12ee4bdd 100644 --- a/src/shadowbox/server/shared_metrics.ts +++ b/src/shadowbox/server/shared_metrics.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. - import {Clock} from '../infrastructure/clock'; import * as follow_redirects from '../infrastructure/follow_redirects'; import {JsonConfig} from '../infrastructure/json_config'; @@ -88,9 +87,9 @@ export class PrometheusUsageMetrics implements UsageMetrics { async getUsage(): Promise { const timeDeltaSecs = Math.round((Date.now() - this.resetTimeMs) / 1000); // We measure the traffic to and from the target, since that's what we are protecting. - const result = - await this.prometheusClient.query(`sum(increase(shadowsocks_data_bytes{dir=~"p>t|pt|p, - private keyConfig: JsonConfig, - usageMetrics: UsageMetrics, - private toMetricsId: (accessKeyId: AccessKeyId) => AccessKeyMetricsId, - private metricsCollector: MetricsCollectorClient) { + private clock: Clock, + private serverConfig: JsonConfig, + private keyConfig: JsonConfig, + usageMetrics: UsageMetrics, + private toMetricsId: (accessKeyId: AccessKeyId) => AccessKeyMetricsId, + private metricsCollector: MetricsCollectorClient + ) { // Start timer this.reportStartTimestampMs = this.clock.now(); @@ -219,14 +222,14 @@ export class OutlineSharedMetricsPublisher implements SharedMetricsPublisher { userReports.push({ userId: this.toMetricsId(keyUsage.accessKeyId) || '', bytesTransferred: keyUsage.inboundBytes, - countries: [...keyUsage.countries] + countries: [...keyUsage.countries], }); } const report = { serverId: this.serverConfig.data().serverId, startUtcMs: this.reportStartTimestampMs, endUtcMs: reportEndTimestampMs, - userReports + userReports, } as HourlyServerMetricsReportJson; this.reportStartTimestampMs = reportEndTimestampMs; @@ -244,8 +247,8 @@ export class OutlineSharedMetricsPublisher implements SharedMetricsPublisher { timestampUtcMs: this.clock.now(), dataLimit: { enabled: !!this.serverConfig.data().accessKeyDataLimit, - perKeyLimitCount: keys.filter(key => !!key.dataLimit).length - } + perKeyLimitCount: keys.filter((key) => !!key.dataLimit).length, + }, }; await this.metricsCollector.collectFeatureMetrics(featureMetricsReport); } diff --git a/src/shadowbox/shadowbox_config.json b/src/shadowbox/shadowbox_config.json index 40b81023..9043b57b 100644 --- a/src/shadowbox/shadowbox_config.json +++ b/src/shadowbox/shadowbox_config.json @@ -1 +1 @@ -{"users":[]} \ No newline at end of file +{"users": []} diff --git a/src/shadowbox/tsconfig.json b/src/shadowbox/tsconfig.json index 6cf26dd2..016a6cd2 100644 --- a/src/shadowbox/tsconfig.json +++ b/src/shadowbox/tsconfig.json @@ -9,13 +9,6 @@ "resolveJsonModule": true, "sourceMap": true }, - "include": [ - "server/main.ts", - "**/*.spec.ts", - "types/**/*.d.ts" - ], - "exclude": [ - "build", - "node_modules" - ] -} \ No newline at end of file + "include": ["server/main.ts", "**/*.spec.ts", "types/**/*.d.ts"], + "exclude": ["build", "node_modules"] +} diff --git a/src/shadowbox/types/node.d.ts b/src/shadowbox/types/node.d.ts index 2871fe79..bd742a3c 100644 --- a/src/shadowbox/types/node.d.ts +++ b/src/shadowbox/types/node.d.ts @@ -21,9 +21,11 @@ declare module 'dns' { // https://nodejs.org/dist/latest-v8.x/docs/api/child_process.html#child_process_child_process_exec_command_options_callback declare module 'child_process' { - export interface ExecError { code: number; } + export interface ExecError { + code: number; + } export function exec( - command: string, - callback?: (error: ExecError|undefined, stdout: string, stderr: string) => - void): ChildProcess; + command: string, + callback?: (error: ExecError | undefined, stdout: string, stderr: string) => void + ): ChildProcess; } diff --git a/src/shadowbox/webpack.config.js b/src/shadowbox/webpack.config.js index d8bcf27e..b0e4de8c 100644 --- a/src/shadowbox/webpack.config.js +++ b/src/shadowbox/webpack.config.js @@ -26,14 +26,14 @@ const config = { module: {rules: [{test: /\.ts(x)?$/, use: 'ts-loader'}]}, node: { // Use the regular node behavior, the directory name of the output file when run. - __dirname: false + __dirname: false, }, plugins: [ // WORKAROUND: some of our (transitive) dependencies use node-gently, which hijacks `require`. // Setting global.GENTLY to false makes these dependencies use standard require. - new webpack.DefinePlugin({'global.GENTLY': false}) + new webpack.DefinePlugin({'global.GENTLY': false}), ], - resolve: {extensions: ['.tsx', '.ts', '.js']} + resolve: {extensions: ['.tsx', '.ts', '.js']}, }; module.exports = config; diff --git a/third_party/shellcheck/README.md b/third_party/shellcheck/README.md index bea49104..a70b9494 100644 --- a/third_party/shellcheck/README.md +++ b/third_party/shellcheck/README.md @@ -1,10 +1,11 @@ # Outline Shellcheck Wrapper -This directory is used to lint our scripts using [Shellcheck](https://www.shellcheck.net/). To ensure consistency across developer systems, the included script -* Attempts to identify the developer's OS (Linux, macOS, or Windows) -* Downloads a pinned version of Shellcheck into `./download` -* Checks the archive hash -* Extracts the executable -* Runs the executable +This directory is used to lint our scripts using [Shellcheck](https://www.shellcheck.net/). To ensure consistency across developer systems, the included script -The executable is cached on the developer's system after the first download. To clear the cache, run `rm download` (or `npm run clean` in the repository root). +- Attempts to identify the developer's OS (Linux, macOS, or Windows) +- Downloads a pinned version of Shellcheck into `./download` +- Checks the archive hash +- Extracts the executable +- Runs the executable + +The executable is cached on the developer's system after the first download. To clear the cache, run `rm download` (or `npm run clean` in the repository root). diff --git a/tsconfig.json b/tsconfig.json index a7ffe792..3bf3da27 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,9 +6,9 @@ "noImplicitThis": true, "moduleResolution": "Node", "sourceMap": true, - "experimentalDecorators":true, + "experimentalDecorators": true, "allowJs": true, "resolveJsonModule": true, - "noUnusedLocals": true, + "noUnusedLocals": true } } diff --git a/tslint.json b/tslint.json index c46f0724..f3fce9f2 100644 --- a/tslint.json +++ b/tslint.json @@ -3,7 +3,8 @@ "rules": { "array-type": [true, "array-simple"], "arrow-return-shorthand": true, - "ban-types": [true, + "ban-types": [ + true, ["Object", "Use {} instead."], ["String", "Use 'string' instead."], ["Number", "Use 'number' instead."], From 6f339e163ae7332ee35821ec0a47c8aace76f6f0 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 9 Mar 2022 23:12:12 +0000 Subject: [PATCH 03/11] Remove clang-format --- package-lock.json | 40 ---------------------------------------- package.json | 1 - 2 files changed, 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46f6a078..36beb9a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ ], "devDependencies": { "@types/jasmine": "^3.5.10", - "clang-format": "^1.2.2", "generate-license-file": "^1.2.0", "husky": "^1.3.1", "jasmine": "^3.5.0", @@ -2946,12 +2945,6 @@ "node": ">=0.10.0" } }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, "node_modules/async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -4303,22 +4296,6 @@ "resolved": "git+ssh://git@github.com/HatScripts/circle-flags.git#f1913a16694e8d19daa88d59e61886b762313c2d", "license": "MIT" }, - "node_modules/clang-format": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.5.0.tgz", - "integrity": "sha512-C1LucFX7E+ABVYcPEbBHM4PYQ2+WInXsqsLpFlQ9cmRfSbk7A7b1I06h/nE4bQ3MsyEkb31jY2gC0Dtc76b4IA==", - "dev": true, - "dependencies": { - "async": "^1.5.2", - "glob": "^7.0.0", - "resolve": "^1.1.6" - }, - "bin": { - "check-clang-format": "bin/check-clang-format.js", - "clang-format": "index.js", - "git-clang-format": "bin/git-clang-format" - } - }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -24101,12 +24078,6 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, "async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -25206,17 +25177,6 @@ "version": "git+ssh://git@github.com/HatScripts/circle-flags.git#f1913a16694e8d19daa88d59e61886b762313c2d", "from": "circle-flags@https://github.com/HatScripts/circle-flags" }, - "clang-format": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.5.0.tgz", - "integrity": "sha512-C1LucFX7E+ABVYcPEbBHM4PYQ2+WInXsqsLpFlQ9cmRfSbk7A7b1I06h/nE4bQ3MsyEkb31jY2gC0Dtc76b4IA==", - "dev": true, - "requires": { - "async": "^1.5.2", - "glob": "^7.0.0", - "resolve": "^1.1.6" - } - }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", diff --git a/package.json b/package.json index 052d10c6..802bf2a9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "private": true, "devDependencies": { "@types/jasmine": "^3.5.10", - "clang-format": "^1.2.2", "generate-license-file": "^1.2.0", "husky": "^1.3.1", "jasmine": "^3.5.0", From 41b8d54bc7903beab418e9f45c89a1afe4919239 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 9 Mar 2022 23:16:11 +0000 Subject: [PATCH 04/11] Rename Prettier config --- .prettierrc => .prettierrc.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .prettierrc => .prettierrc.json (100%) diff --git a/.prettierrc b/.prettierrc.json similarity index 100% rename from .prettierrc rename to .prettierrc.json From 737b6255b8822c7c843bea36cd2a7f7a99efc6b4 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 9 Mar 2022 23:24:39 +0000 Subject: [PATCH 05/11] Fix lint error --- src/metrics_server/connection_metrics.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/metrics_server/connection_metrics.ts b/src/metrics_server/connection_metrics.ts index 4469c56e..d3c510bf 100644 --- a/src/metrics_server/connection_metrics.ts +++ b/src/metrics_server/connection_metrics.ts @@ -59,9 +59,7 @@ function getConnectionRowsFromReport(report: HourlyConnectionMetricsReport): Con // Returns true iff testObject contains a valid HourlyConnectionMetricsReport. // tslint:disable-next-line:no-any -export function isValidConnectionMetricsReport( - testObject: any -): testObject is HourlyConnectionMetricsReport { +export function isValidConnectionMetricsReport(testObject: any): testObject is HourlyConnectionMetricsReport { if (!testObject) { return false; } From 094562252ffd4c16d615a20ccd0f43a833fe60b6 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 9 Mar 2022 23:27:09 +0000 Subject: [PATCH 06/11] Fix package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 802bf2a9..acfa14f5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "action:help": "npm run action", "action:list": "npm run action", "clean": "rm -rf src/*/node_modules/ build/ node_modules/ src/server_manager/install_scripts/do_install_script.ts src/server_manager/install_scripts/gcp_install_script.ts third_party/shellcheck/download/", - "format": "prettier \"**/*.{cjs,html,js,json,md,ts}\" --write", + "format": "pretty-quick --staged --pattern \"**/*.{cjs,html,js,json,md,ts}\"", "lint": "npm run lint:sh && npm run lint:ts", "lint:sh": "bash ./scripts/shellcheck.sh", "lint:ts": "npx tslint 'src/**/*.ts' -e '**/node_modules/**'", @@ -31,7 +31,7 @@ ], "husky": { "hooks": { - "pre-commit": "npm run lint && npx git-clang-format && npx pretty-quick --staged --pattern \"**/*.{cjs,html,js,json,md,ts}\"" + "pre-commit": "npm run lint && npm run format" } } } From 0f5bf1f89a3a6277865b4af58cfc9a7302535191 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 9 Mar 2022 23:31:41 +0000 Subject: [PATCH 07/11] Reformat connection_metrics --- src/metrics_server/connection_metrics.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/metrics_server/connection_metrics.ts b/src/metrics_server/connection_metrics.ts index d3c510bf..9cd0070a 100644 --- a/src/metrics_server/connection_metrics.ts +++ b/src/metrics_server/connection_metrics.ts @@ -58,8 +58,10 @@ function getConnectionRowsFromReport(report: HourlyConnectionMetricsReport): Con } // Returns true iff testObject contains a valid HourlyConnectionMetricsReport. -// tslint:disable-next-line:no-any -export function isValidConnectionMetricsReport(testObject: any): testObject is HourlyConnectionMetricsReport { +export function isValidConnectionMetricsReport( + // tslint:disable-next-line:no-any + testObject: any +): testObject is HourlyConnectionMetricsReport { if (!testObject) { return false; } From 17d1a63f2d8b337e5d789c806fb2c3f680d08911 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 9 Mar 2022 23:39:07 +0000 Subject: [PATCH 08/11] Add format:all --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index acfa14f5..691ea83b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "action:list": "npm run action", "clean": "rm -rf src/*/node_modules/ build/ node_modules/ src/server_manager/install_scripts/do_install_script.ts src/server_manager/install_scripts/gcp_install_script.ts third_party/shellcheck/download/", "format": "pretty-quick --staged --pattern \"**/*.{cjs,html,js,json,md,ts}\"", + "format:all": "prettier --write \"**/*.{cjs,html,js,json,md,ts}\"", "lint": "npm run lint:sh && npm run lint:ts", "lint:sh": "bash ./scripts/shellcheck.sh", "lint:ts": "npx tslint 'src/**/*.ts' -e '**/node_modules/**'", From 19d7a5a9d81951289dca2187caaae83949600754 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Thu, 10 Mar 2022 02:06:10 +0000 Subject: [PATCH 09/11] Remove server_manager prettier config --- package-lock.json | 365 ----------------------------- src/server_manager/.prettierignore | 2 - src/server_manager/package.json | 7 - 3 files changed, 374 deletions(-) delete mode 100644 src/server_manager/.prettierignore diff --git a/package-lock.json b/package-lock.json index 36beb9a7..24b0cd0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21227,8 +21227,6 @@ "postcss": "^7.0.29", "postcss-rtl": "^1.7.3", "posthtml-postcss": "^0.2.6", - "prettier": "1.19.1", - "pretty-quick": "2.0.1", "puppeteer": "^5.5.0", "style-loader": "^1.2.1", "ts-loader": "^7.0.1", @@ -21237,214 +21235,6 @@ "webpack-dev-server": "^3.10.3" } }, - "src/server_manager/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "src/server_manager/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "src/server_manager/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "src/server_manager/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "src/server_manager/node_modules/execa": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", - "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^3.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": "^8.12.0 || >=9.7.0" - } - }, - "src/server_manager/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "src/server_manager/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "src/server_manager/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "src/server_manager/node_modules/ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "src/server_manager/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "src/server_manager/node_modules/npm-run-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", - "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "src/server_manager/node_modules/p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "src/server_manager/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "src/server_manager/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "src/server_manager/node_modules/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=4" - } - }, - "src/server_manager/node_modules/pretty-quick": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-2.0.1.tgz", - "integrity": "sha512-y7bJt77XadjUr+P1uKqZxFWLddvj3SKY6EU4BuQtMxmmEFSMpbN132pUWdSG1g1mtUfO0noBvn7wBf0BVeomHg==", - "dev": true, - "dependencies": { - "chalk": "^2.4.2", - "execa": "^2.1.0", - "find-up": "^4.1.0", - "ignore": "^5.1.4", - "mri": "^1.1.4", - "multimatch": "^4.0.0" - }, - "bin": { - "pretty-quick": "bin/pretty-quick.js" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "prettier": ">=1.8.0" - } - }, - "src/server_manager/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "src/shadowbox": { "name": "outline-server", "version": "1.6.2", @@ -32707,8 +32497,6 @@ "postcss": "^7.0.29", "postcss-rtl": "^1.7.3", "posthtml-postcss": "^0.2.6", - "prettier": "1.19.1", - "pretty-quick": "2.0.1", "puppeteer": "^5.5.0", "request": "^2.87.0", "style-loader": "^1.2.1", @@ -32717,159 +32505,6 @@ "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "execa": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", - "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^3.0.0", - "onetime": "^5.1.0", - "p-finally": "^2.0.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "npm-run-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", - "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true - }, - "pretty-quick": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-2.0.1.tgz", - "integrity": "sha512-y7bJt77XadjUr+P1uKqZxFWLddvj3SKY6EU4BuQtMxmmEFSMpbN132pUWdSG1g1mtUfO0noBvn7wBf0BVeomHg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "execa": "^2.1.0", - "find-up": "^4.1.0", - "ignore": "^5.1.4", - "mri": "^1.1.4", - "multimatch": "^4.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "outline-metrics-server": { diff --git a/src/server_manager/.prettierignore b/src/server_manager/.prettierignore deleted file mode 100644 index 99a5749e..00000000 --- a/src/server_manager/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -/* -!ui_components/ \ No newline at end of file diff --git a/src/server_manager/package.json b/src/server_manager/package.json index ad88d991..9d0e7027 100644 --- a/src/server_manager/package.json +++ b/src/server_manager/package.json @@ -95,8 +95,6 @@ "postcss": "^7.0.29", "postcss-rtl": "^1.7.3", "posthtml-postcss": "^0.2.6", - "prettier": "1.19.1", - "pretty-quick": "2.0.1", "puppeteer": "^5.5.0", "style-loader": "^1.2.1", "ts-loader": "^7.0.1", @@ -104,11 +102,6 @@ "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" }, - "prettier": { - "printWidth": 120, - "bracketSpacing": false, - "trailingComma": "es5" - }, "resolutions": { "inherits": "2.0.3", "samsam": "1.1.3", From 920e5bce1cc4ce70b3bdcacbb519296ea9ed48d5 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Thu, 10 Mar 2022 02:09:59 +0000 Subject: [PATCH 10/11] Format server_manager/*.{md,json} --- src/server_manager/README.md | 9 +++++++-- src/server_manager/electron_app/tsconfig.json | 14 +++----------- src/server_manager/tsconfig.json | 6 ++---- .../web_app/ui_components/licenses/README.md | 12 ++++++------ 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/server_manager/README.md b/src/server_manager/README.md index a3d940bf..ac6bf08a 100644 --- a/src/server_manager/README.md +++ b/src/server_manager/README.md @@ -3,11 +3,13 @@ ## Running To run the Outline Manager Electron app: + ``` npm run action server_manager/electron_app/start ``` To run the Outline Manager Electron app with a development build (code not minified): + ``` BUILD_ENV=development npm run action server_manager/electron_app/start ``` @@ -22,7 +24,7 @@ npm run action server_manager/web_app/start ## Gallery Server for UI Development -We have a server app to for quickly iterating on UI components. To spin it up, run +We have a server app to for quickly iterating on UI components. To spin it up, run ``` npm run action server_manager/web_app/start_gallery @@ -38,6 +40,7 @@ This will enable the Developer menu on the application window. ## Packaging To build the app binary: + ``` npm run action server_manager/electron_app/package_${PLATFORM} ``` @@ -53,6 +56,7 @@ The per-platform standalone apps will be at `build/electron_app/static/dist`. ## Releases To perform a release, use + ``` npm run action server_manager/electron_app/release ``` @@ -62,7 +66,8 @@ This will perform a clean and reinstall all dependencies to make sure the build ## Error reporting To enable error reporting through [Sentry](https://sentry.io/) for local builds, run: -``` bash + +```bash export SENTRY_DSN=[Sentry development API key] npm run action server_manager/electron_app/start ``` diff --git a/src/server_manager/electron_app/tsconfig.json b/src/server_manager/electron_app/tsconfig.json index 487d666a..7bf1a31b 100644 --- a/src/server_manager/electron_app/tsconfig.json +++ b/src/server_manager/electron_app/tsconfig.json @@ -5,17 +5,9 @@ "noImplicitAny": true, "module": "commonjs", "rootDir": ".", - "lib": [ - "dom", - "es2016" - ] + "lib": ["dom", "es2016"] }, - "include": [ - "*.ts", - "../types/*.d.ts" - ], - "exclude": [ - "node_modules" - ], + "include": ["*.ts", "../types/*.d.ts"], + "exclude": ["node_modules"], "compileOnSave": true } diff --git a/src/server_manager/tsconfig.json b/src/server_manager/tsconfig.json index 05a987ac..0cc57500 100644 --- a/src/server_manager/tsconfig.json +++ b/src/server_manager/tsconfig.json @@ -1,12 +1,10 @@ { "compilerOptions": { - "target": "es2018", + "target": "es2018" }, "extends": "../../tsconfig.json", "rootDir": ".", - "include": [ - "**/*.ts" - ], + "include": ["**/*.ts"], "exclude": [ "node_modules", "web_app/galery_app", diff --git a/src/server_manager/web_app/ui_components/licenses/README.md b/src/server_manager/web_app/ui_components/licenses/README.md index a0a1eb1a..dadd46f7 100644 --- a/src/server_manager/web_app/ui_components/licenses/README.md +++ b/src/server_manager/web_app/ui_components/licenses/README.md @@ -2,12 +2,12 @@ ## Steps -* `cd` to the root of your clone of this repo -* Ensure `node_modules` is up to date and only include dependencies of the Electron app by running `npm ci && npm run action server_manager/web_app/build` -* `cd src/server_manager` -* `npx generate-license-file --input package.json --output web_app/ui_components/licenses/licenses.txt` -* `cd web_app/ui_components/licenses` -* `cat db-ip_license.txt >> licenses.txt` +- `cd` to the root of your clone of this repo +- Ensure `node_modules` is up to date and only include dependencies of the Electron app by running `npm ci && npm run action server_manager/web_app/build` +- `cd src/server_manager` +- `npx generate-license-file --input package.json --output web_app/ui_components/licenses/licenses.txt` +- `cd web_app/ui_components/licenses` +- `cat db-ip_license.txt >> licenses.txt` Done! From 8afb069c2580d5f4dd44f0e927e261af5e86f7c2 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Thu, 10 Mar 2022 17:07:39 +0000 Subject: [PATCH 11/11] Add newline --- .prettierignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index d250d45c..9e0427c4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ /build/ node_modules/ -/src/server_manager/messages/ \ No newline at end of file +/src/server_manager/messages/