diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..ee1bee2bc --- /dev/null +++ b/.clang-format @@ -0,0 +1,2 @@ +BasedOnStyle: Google +ColumnLimit: 100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..d40623ab1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +**/bower_components +/build +/node_modules/ +/src/metrics_server/node_modules/ +/src/server_manager/node_modules/ +/src/server_manager/install_scripts/do_install_script.ts +yarn-error.log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..3a1fd607b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,135 @@ +language: node_js + +node_js: + - "8" + +cache: + yarn: true + directories: + - $HOME/.cache/bower + - $HOME/.cache/electron + - $HOME/.cache/electron-builder + +before_install: + # https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Travis-CI-supports-yarn + - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2 + - export PATH="$HOME/.yarn/bin:$PATH" + +stages: + - build and unit test + - integration test + - name: tag + if: type = cron + - name: deploy + if: tag =~ ^daily + - name: release + if: tag =~ ^v[0-9] + +# Stages with the same name define multiple jobs which run in parallel. +# To make it more apparent in the Travis UI exactly what each job is +# doing, we add a descriptive environment variable. +jobs: + include: + # Ideally, we would split this stage in some way, e.g. by component or by + # build/test commands, to make it clearer in the Travis UI exactly which + # command failed. However, since each stage incurs a significantly start-up + # cost, we combine test and build commands for all components into one fast + # stage. + - stage: build and unit test + script: + - yarn shadowbox_install + - yarn do shadowbox/server/build + - yarn do shadowbox/test + - yarn do server_manager/electron_app/build + - yarn do server_manager/web_app/build + - yarn do server_manager/web_app/test + + - stage: integration test + sudo: required + services: docker + script: + # https://docs.travis-ci.com/user/docker/ + - | + sudo rm -f /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.17.1/docker-compose-$(uname -s)-$(uname -m) > docker-compose + chmod +x docker-compose + sudo mv docker-compose /usr/local/bin + - yarn shadowbox_install + - yarn do shadowbox/integration_test/run + + - stage: tag + script: + - RELEASE_NAME=daily-$(date -I) + - curl --data '{"tag_name":"'$RELEASE_NAME'","name":"'$RELEASE_NAME'","prerelease":true}' https://api.github.com/repos/Jigsaw-Code/outline-server/releases?access_token=$CI_USER_TOKEN + + - stage: deploy + env: + - DESC=shadowbox docker image + sudo: required + services: docker + script: + - yarn shadowbox_install + - yarn do shadowbox/docker/build + - docker login quay.io -u="$QUAY_IO_USERNAME" -p="$QUAY_IO_PASSWORD" + - docker tag outline/shadowbox quay.io/outline/shadowbox:$TRAVIS_TAG + - docker push quay.io/outline/shadowbox:$TRAVIS_TAG + - docker tag outline/shadowbox quay.io/outline/shadowbox:daily + - docker push quay.io/outline/shadowbox:daily + + - stage: deploy + env: + - DESC=linux manager + addons: + apt: + packages: + - rpm + script: yarn do server_manager/electron_app/package_linux + + # https://www.electron.build/multi-platform-build + - stage: deploy + env: + - DESC=windows manager + sudo: required + services: docker + script: + - yarn do server_manager/electron_app/build + - docker pull electronuserland/builder:wine + - docker run --rm + -v ${PWD}:/project + -v ~/.cache/electron:/root/.cache/electron + -v ~/.cache/electron-builder:/root/.cache/electron-builder + electronuserland/builder:wine + /bin/bash -c "yarn do server_manager/electron_app/package_only_windows" || travis_terminate $? + + - stage: deploy + env: + - DESC=macos manager + os: osx + script: yarn do server_manager/electron_app/package_macos + + # Note that because we cannot currently build signed macOS or Windows + # binaries on Travis, those builds must be manually built and uploaded + # to the release page. + - stage: release + env: + - DESC=linux manager + addons: + apt: + packages: + - rpm + script: yarn do server_manager/electron_app/release_linux + +deploy: + provider: releases + api_key: + secure: "a7JJwbwgWQpXAaGCKbMf/HySyhiBOPeyjZZkSeZBECpqS671j6rbZ2MHvXp7QfU3LZ+Z7MYwkl/DTgbdOZ8ndbwWMn5yJjeIBnreQqZlbyR5jh1G66vu+r55aBxd6+svGp2VynGlWLI+1+4L6U33VHNXnkH6D7fwSKze1glu1XqeUzUkNEPRkWCg/Y6/WGMh19yOgoxulN3mTL65s5FzFpXKdDT7F8J6BPzoz5cWXTMiZM+fs0BjTIfNNabkIWdRvVFJ5s2Cx3EJM0BU1NDRVxGeYJsvli/gkYW82ZTeQdXfn9KQxAK1n6lQsUQJUnErH6jSfrv6QJkSnnKjVogXcP/SSj0p73UAcZuUZ7hRW/TX0HAtgCxnY7dkMaxyBHiNwxprSm4+83VRHIALzUqmcJ28b6VvXo1znD3r9frDsY5PuNAmew3VbpQyxit515tZRpiRXzzSnrFqAovWl6wY0UIkQEFLpi18PGOhUFcewKBpN4W4PszGvWkPgdPNBq5nizUtaHX62lPFoPjGkhcunD4Tn9RIrUcbRTfWVokI5qT8oLZM3mgBS8H85N80ngq1tQBUCJ1Xfd3vqJ+xE0x6boIzYHs4cz0Qoao1vmo1wsycRhr1PAqvtBztAkq+pwOwd9qI9VL8QCJErHBd56I3jkPTsPgMx8K3M8N4/EtOsA0=" + file_glob: true + file: "build/server_manager/electron_app/static/dist/*.*" + skip_cleanup: true + on: + tags: true + +env: + global: + - ELECTRON_CACHE=$HOME/.cache/electron + - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..6d364e1dd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +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 diff --git a/LICENSE b/LICENSE index 261eeb9e9..8dada3eda 100644 --- a/LICENSE +++ b/LICENSE @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index cdd32fcc7..d45427f8b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,115 @@ -# outline-server -Create and manage access to Outline servers +# Outline Server Creator + +[![Build Status](https://travis-ci.com/Jigsaw-Code/outline-server.svg?token=HiP4RTme8LSvyrP9kNJq&branch=master)](https://travis-ci.com/Jigsaw-Code/outline-server) + +This repository has all the code needed to create and manage Outline servers on DigitalOcean. An Outline server runs +instances of Shadowsocks proxies and provides an API used by the Outline Manager application. + + +## Components + +The system comprises the following components: + +- **Server Manager Electron App:** an Election application that wraps the Server Manager web application and runs + natively on Desktop. It adds some extra functionality, like validation of the server self-signed certificate and interception of the DigitalOcean registration flow. + + Code: `src/server_manager/electron_app` +- **Proxy Server**: a server that runs the Shadowsocks instances and a REST API to manage its users. Used as backend by the + Server Manager app. + + Code: `src/shadowbox` + +## Server Manager + +### Setup + +Ensure you have the following installed: + - [Node](https://nodejs.org/) + - [Yarn](https://yarnpkg.com/en/docs/install) + - [Wine](https://www.winehq.org/download), if you would like to generate binaries for Windows. + +Install dependencies: +``` +yarn +``` + +### Electron App + +To run the electron app: +``` +yarn do server_manager/electron_app/run +``` + +To build the app for all platforms: +``` +yarn do server_manager/electron_app/package +``` + +The per-platform standalone apps will be at `build/electron_app/bundled`. + +The per-platform standalone apps packaged for distribution will be at +`build/electron_app/packaged` in the following formats: + +- Windows: zip files. Only generated if you have [wine](https://www.winehq.org/download) installed. +- Linux: tar.gz files. +- macOS: dmg files if built from macOS, zip files otherwise. + +To perform a release, use +``` +yarn do server_manager/electron_app/release +``` + +This will perform a clean and reinstall all dependencies to make sure the build is not tainted. + + +## Proxy Server + +See [`src/shadowbox/README.md`](src/shadowbox/README.md). + +## Unit Tests + +To run all the tests, run `yarn test` + + +## Build System + +We have a very simple build system based on package.json scripts that are called called using `yarn` +and a thin wrapper for what we call build "actions". + +We've defined a `do` package.json script that takes an `action` parameter: +```shell +yarn do $ACTION +``` + +This command will define a `do_action()` function and call `${ACTION}_action.sh`, which must exist. +The called action script can use `do_action` to call its dependencies. The $ACTION parameter is +always resolved from the project root, regardless of the caller location. + +The idea of `do_action` is to keep the build logic next to where the relevant code is. +It also defines two environmental variables: + +- ROOT_DIR: the root directory of the project, as an absolute path. +- BUILD_DIR: where the build output should go, as an absolute path. + +### 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 + - `static/`: The Manager Electron app to run with the electron command-line + - `bundled/`: The Electron app bundled to run standalone on each platform + - `packaged/`: The Electron app bundles packaged as single files for distribution +- `invite_page`: the Invite Page + - `static`: The standalone static files to be deployed +- `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: +``` +yarn run clean +``` diff --git a/jasmine.json b/jasmine.json new file mode 100644 index 000000000..f80f1ee8d --- /dev/null +++ b/jasmine.json @@ -0,0 +1,9 @@ +{ + "spec_dir": ".", + "spec_files": [ + "build/**/*.spec.js" + ], + "helpers": ["src/base64mocks.js"], + "stopSpecOnExpectationFailure": false, + "random": false +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..025a50793 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "outline-server", + "devDependencies": { + "@types/jasmine": "^2.5.53", + "clang-format": "^1.2.2", + "husky": "^0.14.3", + "jasmine": "^2.6.0", + "tslint": "^5.9.1", + "typescript": "^2.6.2" + }, + "scripts": { + "postinstall": "yarn run server_manager_install", + "server_manager_install": "cd src/server_manager && yarn install --modules-folder ../../node_modules && yarn bower install", + "clean": "rm -rf src/server_manager/bower_components/ src/{metrics_server,server_manager}/node_modules/ build/ node_modules/ src/server_manager/install_scripts/do_install_script.ts", + "shadowbox_install": "cd src/shadowbox && yarn install --modules-folder ../../node_modules --no-bin-links", + "metrics_server_install": "cd src/metrics_server && yarn install --modules-folder ../../node_modules", + "do": "bash ./scripts/do_action.sh", + "precommit": "for i in . src/server_manager/electron_app src/metrics_server src/shadowbox; do tslint -p $i --fix; done; git-clang-format" + } +} diff --git a/scripts/do_action.sh b/scripts/do_action.sh new file mode 100755 index 000000000..40604eb45 --- /dev/null +++ b/scripts/do_action.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eux + +# TODO: Because Node.js on Cygwin doesn't handle absolute paths very +# well, it would be worth pushd-ing to ROOT_DIR before invoking +# them and making BUILD_DIR a relative path, viz. just "build". + +export ROOT_DIR=${ROOT_DIR:-$(git rev-parse --show-toplevel)} +export BUILD_DIR=${BUILD_DIR:-$ROOT_DIR/build} + +function do_action() { + readonly STYLE_BOLD_WHITE='\033[1;37m' + readonly STYLE_RESET='\033[0m' + local action=$1 + echo -e "$STYLE_BOLD_WHITE[Running $action]$STYLE_RESET" + shift + $ROOT_DIR/src/${action}_action.sh "$@" + echo -e "$STYLE_BOLD_WHITE[Done $action]$STYLE_RESET" +} +export -f do_action + +do_action "$@" diff --git a/src/metrics_server/README.md b/src/metrics_server/README.md new file mode 100644 index 000000000..e873ee994 --- /dev/null +++ b/src/metrics_server/README.md @@ -0,0 +1,51 @@ +# Outline Metrics Server + +The Outline Metrics Server is built using [Google Cloud Functions](https://cloud.google.com/functions/), which lets us write a simple Node HTTP server. By deploying this server to the uproxysite Google project, we gain permission to write to uproxysite's BigQuery tables. + +## Requirements +* Install `gcloud` from https://cloud.google.com/sdk/docs/ +* Node 6.11.1 or greater (for testing via Cloud Functions Emulator) +* You must run `yarn metrics_server_install` explicitly to install metrics_server dependencies. + +## Building +Run `yarn do metrics_server/build` + +## Deploying +To deploy +* Authenticate with gcloud: `gcloud auth login` +* Select the gcloud project to uproxysite: `gcloud config set project uproxysite` +* Run the deploy script: + * to deploy to test: `yarn do metrics_server/deploy_test` + * to deploy to prod: `yarn do metrics_server/deploy_prod` + +## Testing with the Cloud Functions Emulator +You can test with the Google Cloud Functions Emulator by running `yarn do metrics_server/test `, e.g.: +``` +yarn do metrics_server/test '{"serverId":"12345","startUtcMs":1502486354823,"endUtcMs":1502499314823,"userReports":[{"userId":"1","bytesTransferred":60,"countries":["US","NL"]},{"userId":"2","bytesTransferred":100,"countries":["UK"]}]}' +``` + +## Testing with Node +You can test the metrics server code using Node: + +`cd build/metrics_server` + +run `node`, then you can test the post_server_report module as follows: +``` +post_server_report = require('./post_server_report.js'); + +serverReport = { + serverId: "123", + startUtcMs: 1502486354823, + endUtcMs: 1502499314823, + userReports: [ + {userId: "1", bytesTransferred: 60, countries: ["US", "NL"]}, + {userId: "2", bytesTransferred: 100, countries: ["CN"]} + ] +} + +post_server_report.postServerReport('uproxy_metrics_test', 'connections_v1', serverReport) + .then(() => { console.log('success') }) + .catch((e) => { console.error('failure: ' + e) }) +``` + +You can then view this inserted data at https://bigquery.cloud.google.com/table/uproxysite:uproxy_metrics_test.connections_v1 diff --git a/src/metrics_server/build.sh b/src/metrics_server/build.sh new file mode 100755 index 000000000..5e60ed90b --- /dev/null +++ b/src/metrics_server/build.sh @@ -0,0 +1,33 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if (( $# <= 1 )); then + echo "Invalid arguments, usage:" + echo "build.sh " + exit 1; +fi + +readonly MODULE_DIR=$(dirname $0) +readonly OUT_DIR=$1 +readonly CONFIG_FILE=$2 + +# Compile the server. +rm -rf $OUT_DIR +tsc -p $MODULE_DIR/tsconfig.json --outDir $OUT_DIR +cp -r $MODULE_DIR/package.json $OUT_DIR + +# Copy config file. +cp -r $CONFIG_FILE $OUT_DIR/config.json diff --git a/src/metrics_server/config_prod.json b/src/metrics_server/config_prod.json new file mode 100644 index 000000000..7c7056efa --- /dev/null +++ b/src/metrics_server/config_prod.json @@ -0,0 +1,4 @@ +{ + "datasetName": "uproxy_metrics", + "tableName": "connections_v1" +} diff --git a/src/metrics_server/config_test.json b/src/metrics_server/config_test.json new file mode 100644 index 000000000..3071fd06d --- /dev/null +++ b/src/metrics_server/config_test.json @@ -0,0 +1,4 @@ +{ + "datasetName": "uproxy_metrics_test", + "tableName": "connections_v1" +} diff --git a/src/metrics_server/deploy_prod_action.sh b/src/metrics_server/deploy_prod_action.sh new file mode 100755 index 000000000..e35cec8a9 --- /dev/null +++ b/src/metrics_server/deploy_prod_action.sh @@ -0,0 +1,25 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +readonly MODULE_DIR=$(dirname $0) +readonly OUT_DIR=$BUILD_DIR/metrics_server/prod +readonly CONFIG_FILE=$MODULE_DIR/config_prod.json + +# Build the server +$MODULE_DIR/build.sh $OUT_DIR $CONFIG_FILE + +# Deploy as "reportHourlyConnectionMetrics" +gcloud beta functions deploy reportHourlyConnectionMetrics --stage-bucket uproxy-cloud-functions --trigger-http --source=$OUT_DIR --entry-point=reportHourlyConnectionMetrics diff --git a/src/metrics_server/deploy_test_action.sh b/src/metrics_server/deploy_test_action.sh new file mode 100755 index 000000000..e3a09579b --- /dev/null +++ b/src/metrics_server/deploy_test_action.sh @@ -0,0 +1,25 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +readonly MODULE_DIR=$(dirname $0) +readonly OUT_DIR=$BUILD_DIR/metrics_server/test +readonly CONFIG_FILE=$MODULE_DIR/config_test.json + +# Build the server +$MODULE_DIR/build.sh $OUT_DIR $CONFIG_FILE + +# Deploy as "reportHourlyConnectionMetricsTest" +gcloud beta functions deploy reportHourlyConnectionMetricsTest --stage-bucket uproxy-cloud-functions --trigger-http --source=$OUT_DIR --entry-point=reportHourlyConnectionMetrics diff --git a/src/metrics_server/index.ts b/src/metrics_server/index.ts new file mode 100644 index 000000000..1a8beb1bd --- /dev/null +++ b/src/metrics_server/index.ts @@ -0,0 +1,55 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as express from 'express'; +import * as fs from 'fs'; +import * as path from 'path'; +import {HourlyServerMetricsReport, isValidServerReport, postServerReport} from './post_server_report'; + +// Accepts hourly connection metrics and inserts them into BigQuery. +// Request body should contain an HourlyServerMetricsReport. +exports.reportHourlyConnectionMetrics = (req: express.Request, res: express.Response) => { + if (req.method !== 'POST') { + res.status(405).send('Method not allowed'); + return; + } + if (!isValidServerReport(req.body)) { + res.status(400).send('Invalid request'); + return; + } + + const serverReport: HourlyServerMetricsReport = { + serverId: req.body.serverId, + startUtcMs: req.body.startUtcMs, + endUtcMs: req.body.endUtcMs, + userReports: req.body.userReports + }; + postServerReport(config.datasetName, config.tableName, serverReport).then(() => { + res.status(200).send('OK'); + }).catch((err: Error) => { + res.status(500).send('Error: ' + err); + }); +}; + +interface Config { + datasetName: string; + tableName: string; +} + +function loadConfig(): Config { + const configText = fs.readFileSync(path.join(__dirname, 'config.json'), {encoding: 'utf8'}); + return JSON.parse(configText); +} + +const config = loadConfig(); diff --git a/src/metrics_server/package.json b/src/metrics_server/package.json new file mode 100644 index 000000000..36ba3ee89 --- /dev/null +++ b/src/metrics_server/package.json @@ -0,0 +1,15 @@ +{ + "name": "outline-metrics-server", + "private": true, + "version": "0.1.0", + "description": "Outline metrics server", + "author": "Outline", + "license": "Apache", + "dependencies": { + "@google-cloud/bigquery": "^0.9.6" + }, + "devDependencies": { + "@google-cloud/functions-emulator": "1.0.0-alpha.23", + "@types/express": "^4.0.36" + } +} diff --git a/src/metrics_server/post_server_report.ts b/src/metrics_server/post_server_report.ts new file mode 100644 index 000000000..1a07b8645 --- /dev/null +++ b/src/metrics_server/post_server_report.ts @@ -0,0 +1,118 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as bigquery from '@google-cloud/bigquery'; + +// TODO(dborkan): HourlyServerMetricsReport and HourlyUserMetricsReport are +// copied from src/shadowbox/server/metrics.ts - find a way to share these +// definitions between shadowbox and the metrics_server. +export interface HourlyServerMetricsReport { + serverId: string; + startUtcMs: number; + endUtcMs: number; + userReports: HourlyUserMetricsReport[]; +} +interface HourlyUserMetricsReport { + userId: string; + countries: string[]; + bytesTransferred: number; +} + +interface ConnectionRow { + serverId: string; + startTimestamp: string; // ISO formatted string. + endTimestamp: string; // ISO formatted string. + userId: string; + bytesTransferred: number; + countries: string[]; +} + +// Instantiates a client +const bigqueryProject = bigquery({ + projectId: 'uproxysite' +}); + +export function postServerReport(datasetName: string, tableName: string, serverReport: HourlyServerMetricsReport) { + const dataset = bigqueryProject.dataset(datasetName); + const table = dataset.table(tableName); + const rows = getConnectionRowsFromServerReport(serverReport); + return new Promise((fulfill, reject) => { + table.insert(rows, (err: Error) => { + if (err) { + reject(err); + } else { + fulfill(); + } + }); + }); +} + +function getConnectionRowsFromServerReport(serverReport: HourlyServerMetricsReport): ConnectionRow[] { + const startTimestampStr = new Date(serverReport.startUtcMs).toISOString(); + const endTimestampStr = new Date(serverReport.endUtcMs).toISOString(); + const rows = []; + for (const userReport of serverReport.userReports) { + rows.push({ + serverId: serverReport.serverId, + startTimestamp: startTimestampStr, + endTimestamp: endTimestampStr, + userId: userReport.userId, + bytesTransferred: userReport.bytesTransferred, + countries: userReport.countries + }); + } + return rows; +} + +// Returns true iff testObject contains a valid HourlyServerMetricsReport. +// tslint:disable-next-line:no-any +export function isValidServerReport(testObject: any): boolean { + // Check that all required fields are present. + const requiredServerReportFields = ['serverId', 'startUtcMs', 'endUtcMs', 'userReports']; + for (const fieldName of requiredServerReportFields) { + if (!testObject[fieldName]) { + return false; + } + } + + // Check that startUtcMs is not after endUtcMs. + if (testObject.startUtcMs >= testObject.endUtcMs) { + return false; + } + + // Check that userReports is an array of 1 or more item. + if (!(testObject.userReports.length >= 1)) { + return false; + } + + const requiredUserReportFields = ['userId', 'countries', 'bytesTransferred']; + const MIN_BYTES_TRANSFERRED = 0; + const MAX_BYTES_TRANSFERRED = 500 * Math.pow(2, 30); // 500 GB. + for (const userReport of testObject.userReports) { + // Test that each userReport contains valid fields. + for (const fieldName of requiredUserReportFields) { + if (!userReport[fieldName]) { + return false; + } + } + // Check that bytesTransferred is between min and max transfer limits + if (userReport.bytesTransferred < MIN_BYTES_TRANSFERRED || + userReport.bytesTransferred > MAX_BYTES_TRANSFERRED) { + return false; + } + } + + // Request is a valid HourlyServerMetricsReport. + return true; +} diff --git a/src/metrics_server/test_action.sh b/src/metrics_server/test_action.sh new file mode 100755 index 000000000..888b8b2cd --- /dev/null +++ b/src/metrics_server/test_action.sh @@ -0,0 +1,34 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if (( $# <= 0 )); then + echo "No test data specified" + exit 1; +fi + +readonly MODULE_DIR=$(dirname $0) +readonly OUT_DIR=$BUILD_DIR/metrics_server/test +readonly CONFIG_FILE=$MODULE_DIR/config_test.json + +# Build the server +$MODULE_DIR/build.sh $OUT_DIR $CONFIG_FILE + +# TODO(dborkan): figure out why the functions binary isn't installed at $ROOT_DIR/node_modules/.bin/ +readonly FUNCTIONS_EMULATOR=$ROOT_DIR/node_modules/@google-cloud/functions-emulator/bin/functions + +$FUNCTIONS_EMULATOR start +$FUNCTIONS_EMULATOR deploy reportHourlyConnectionMetrics --trigger-http --local-path=$OUT_DIR --entry-point=reportHourlyConnectionMetrics +$FUNCTIONS_EMULATOR call reportHourlyConnectionMetrics --data=$1 diff --git a/src/metrics_server/tsconfig.json b/src/metrics_server/tsconfig.json new file mode 100644 index 000000000..41064ab2a --- /dev/null +++ b/src/metrics_server/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2016", + "removeComments": false, + "noImplicitAny": true, + "module": "commonjs", + "rootDir": ".", + "lib": [ + "dom", + "es2016" + ] + }, + "include": [ + "index.ts", + "post_server_report.ts", + "types/**/*.d.ts" + ], + "exclude": [ + "node_modules" + ], + "compileOnSave": true +} diff --git a/src/metrics_server/types/@google-cloud.d.ts b/src/metrics_server/types/@google-cloud.d.ts new file mode 100644 index 000000000..0e080c939 --- /dev/null +++ b/src/metrics_server/types/@google-cloud.d.ts @@ -0,0 +1,19 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Definitions missing from @types/node. + +// TODO(dborkan): Find a *.d.ts file with this definition. The API is defined at +// https://googlecloudplatform.github.io/google-cloud-node/#/docs/bigquery/0.9.6/bigquery +declare module '@google-cloud/bigquery'; diff --git a/src/metrics_server/yarn.lock b/src/metrics_server/yarn.lock new file mode 100644 index 000000000..40f38a1f9 --- /dev/null +++ b/src/metrics_server/yarn.lock @@ -0,0 +1,1981 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@google-cloud/bigquery@^0.9.6": + version "0.9.6" + resolved "https://registry.yarnpkg.com/@google-cloud/bigquery/-/bigquery-0.9.6.tgz#beac6d486c45f8010a480438de4945252fc4ceae" + dependencies: + "@google-cloud/common" "^0.13.0" + arrify "^1.0.0" + duplexify "^3.5.0" + extend "^3.0.0" + is "^3.0.1" + stream-events "^1.0.1" + string-format-obj "^1.0.0" + +"@google-cloud/common@^0.13.0": + version "0.13.4" + resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-0.13.4.tgz#75bb7f60931cfc9d94da0b5d408950d0bbf0e979" + dependencies: + array-uniq "^1.0.3" + arrify "^1.0.1" + concat-stream "^1.6.0" + create-error-class "^3.0.2" + duplexify "^3.5.0" + ent "^2.2.0" + extend "^3.0.0" + google-auto-auth "^0.7.1" + is "^3.2.0" + log-driver "^1.2.5" + methmeth "^1.1.0" + modelo "^4.2.0" + request "^2.79.0" + retry-request "^2.0.0" + split-array-stream "^1.0.0" + stream-events "^1.0.1" + string-format-obj "^1.1.0" + through2 "^2.0.3" + +"@google-cloud/functions-emulator@1.0.0-alpha.23": + version "1.0.0-alpha.23" + resolved "https://registry.yarnpkg.com/@google-cloud/functions-emulator/-/functions-emulator-1.0.0-alpha.23.tgz#5f2916934a31369318dc81c23eb917a0400f6ead" + dependencies: + "@google-cloud/storage" "1.2.1" + adm-zip "0.4.7" + ajv "5.2.2" + body-parser "1.17.2" + cli-table2 "0.2.0" + colors "1.1.2" + configstore "3.1.1" + express "4.15.4" + google-proto-files "0.12.1" + googleapis "20.1.0" + got "7.1.0" + grpc "1.4.1" + http-proxy "1.16.2" + lodash "4.17.4" + prompt "1.0.0" + rimraf "2.6.1" + semver "5.4.1" + serializerr "1.0.3" + tmp "0.0.31" + uuid "3.1.0" + winston "2.3.1" + yargs "8.0.2" + +"@google-cloud/storage@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-1.2.1.tgz#a0f2e20871b862f0ea64a90ac48fc08845cf9505" + dependencies: + "@google-cloud/common" "^0.13.0" + arrify "^1.0.0" + async "^2.0.1" + concat-stream "^1.5.0" + create-error-class "^3.0.2" + duplexify "^3.5.0" + extend "^3.0.0" + gcs-resumable-upload "^0.8.0" + hash-stream-validation "^0.2.1" + is "^3.0.1" + mime-types "^2.0.8" + once "^1.3.1" + pumpify "^1.3.3" + stream-events "^1.0.1" + string-format-obj "^1.0.0" + through2 "^2.0.0" + +"@types/express-serve-static-core@*": + version "4.0.49" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.49.tgz#3438d68d26e39db934ba941f18e3862a1beeb722" + dependencies: + "@types/node" "*" + +"@types/express@^4.0.36": + version "4.0.36" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.36.tgz#14eb47de7ecb10319f0a2fb1cf971aa8680758c2" + dependencies: + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.1.tgz#2cf42972d0931c1060c7d5fa6627fce6bd876f2f" + +"@types/node@*": + version "8.0.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.22.tgz#9c6bfee1f45f5e9952ff6b487e657ecca48c7777" + +"@types/serve-static@*": + version "1.7.31" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.7.31.tgz#15456de8d98d6b4cff31be6c6af7492ae63f521a" + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +abbrev@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" + +accepts@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +adm-zip@0.4.7: + version "0.4.7" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.7.tgz#8606c2cbf1c426ce8c8ec00174447fd49b6eafc1" + +ajv@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +aproba@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arguejs@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/arguejs/-/arguejs-0.2.3.tgz#b6f939f5fe0e3cd1f3f93e2aa9262424bf312af7" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-uniq@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arrify@^1.0.0, arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +ascli@~1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ascli/-/ascli-1.0.1.tgz#bcfa5974a62f18e81cabaeb49732ab4a88f906bc" + dependencies: + colour "~0.7.1" + optjs "~3.2.2" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +async@^2.0.1, async@^2.3.0, async@^2.4.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + +async@~0.9.0: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + +async@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + +async@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.3.0.tgz#1013d1051047dd320fe24e494d5c66ecaf6147d9" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64url@2.0.0, base64url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +body-parser@1.17.2: + version "1.17.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" + dependencies: + bytes "2.4.0" + content-type "~1.0.2" + debug "2.6.7" + depd "~1.1.0" + http-errors "~1.6.1" + iconv-lite "0.4.15" + on-finished "~2.3.0" + qs "6.4.0" + raw-body "~2.2.0" + type-is "~1.6.15" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + +buffer-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +bytebuffer@~5: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" + dependencies: + long "~3" + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + +camelcase@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +capture-stack-trace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +cli-table2@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97" + dependencies: + lodash "^3.10.1" + string-width "^1.0.1" + optionalDependencies: + colors "^1.1.2" + +cliui@^3.0.3, cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +colors@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +colors@1.1.2, colors@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +colour@~0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.5.0, concat-stream@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +configstore@3.1.1, configstore@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90" + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +create-error-class@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + dependencies: + capture-stack-trace "^1.0.0" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +debug@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" + dependencies: + ms "2.0.0" + +debug@2.6.8, debug@^2.2.0: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +decamelize@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decompress-response@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + dependencies: + mimic-response "^1.0.0" + +deep-equal@~0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-0.2.2.tgz#84b745896f34c684e98f2ce0e42abaf43bba017d" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@1.1.1, depd@~1.1.0, depd@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + dependencies: + is-obj "^1.0.0" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + +duplexify@^3.1.2, duplexify@^3.5.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.1.tgz#4e1516be68838bc90a49994f0b39a6e5960befcd" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ecdsa-sig-formatter@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" + dependencies: + base64url "^2.0.0" + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" + dependencies: + once "^1.4.0" + +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +etag@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +express@4.15.4: + version "4.15.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.2" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.8" + depd "~1.1.1" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + finalhandler "~1.0.4" + fresh "0.5.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.5" + qs "6.5.0" + range-parser "~1.2.0" + send "0.15.4" + serve-static "1.12.4" + setprototypeof "1.0.3" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.0" + vary "~1.1.1" + +extend@^3.0.0, extend@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extsprintf@1.3.0, extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +finalhandler@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7" + dependencies: + debug "2.6.8" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.1" + statuses "~1.3.1" + unpipe "~1.0.0" + +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +fresh@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gcp-metadata@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.2.0.tgz#62dafca65f3a631bc8ce2ec3b77661f5f9387a0a" + dependencies: + extend "^3.0.0" + retry-request "^2.0.0" + +gcs-resumable-upload@^0.8.0: + version "0.8.1" + resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-0.8.1.tgz#bb9eb7dfbacc8d77f2136b99661e693058fa3be3" + dependencies: + buffer-equal "^1.0.0" + configstore "^3.0.0" + google-auto-auth "^0.7.1" + pumpify "^1.3.3" + request "^2.81.0" + stream-events "^1.0.1" + through2 "^2.0.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob@^7.0.5: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +google-auth-library@^0.10.0, google-auth-library@~0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e" + dependencies: + gtoken "^1.2.1" + jws "^3.1.4" + lodash.noop "^3.0.1" + request "^2.74.0" + +google-auto-auth@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/google-auto-auth/-/google-auto-auth-0.7.1.tgz#c8260444912dd8ceeccd838761d56f462937bd02" + dependencies: + async "^2.3.0" + gcp-metadata "^0.2.0" + google-auth-library "^0.10.0" + request "^2.79.0" + +google-p12-pem@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" + dependencies: + node-forge "^0.7.1" + +google-proto-files@0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/google-proto-files/-/google-proto-files-0.12.1.tgz#6434dc7e025a0d0c82e5f04e615c737d6a4c4387" + +googleapis@20.1.0: + version "20.1.0" + resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-20.1.0.tgz#efb2541f0cab123492bc8ccfe09fa6baaf2b84ca" + dependencies: + async "~2.3.0" + google-auth-library "~0.10.0" + string-template "~1.0.0" + +got@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" + dependencies: + decompress-response "^3.2.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-plain-obj "^1.1.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + isurl "^1.0.0-alpha5" + lowercase-keys "^1.0.0" + p-cancelable "^0.3.0" + p-timeout "^1.1.1" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + url-parse-lax "^1.0.0" + url-to-options "^1.0.1" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +grpc@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.4.1.tgz#3ee4a8346a613f2823928c9f8f99081b6368ec7c" + dependencies: + arguejs "^0.2.3" + lodash "^4.15.0" + nan "^2.0.0" + node-pre-gyp "^0.6.35" + protobufjs "^5.0.0" + +gtoken@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.2.tgz#172776a1a9d96ac09fc22a00f5be83cee6de8820" + dependencies: + google-p12-pem "^0.1.0" + jws "^3.0.0" + mime "^1.2.11" + request "^2.72.0" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +has-symbol-support-x@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.0.tgz#442d89b1d0ac6cf5ff2f7b916ee539869b93a256" + +has-to-string-tag-x@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/has-to-string-tag-x/-/has-to-string-tag-x-1.4.0.tgz#49d7bcde85c2409be38ac327e3e119a451657c7b" + dependencies: + has-symbol-support-x "^1.4.0" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +hash-stream-validation@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.1.tgz#ecc9b997b218be5bb31298628bb807869b73dcd1" + dependencies: + through2 "^2.0.0" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +http-errors@~1.6.1, http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-proxy@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +i@0.3.x: + version "0.3.5" + resolved "https://registry.yarnpkg.com/i/-/i-0.3.5.tgz#1d2b854158ec8169113c6cb7f6b6801e99e211d5" + +iconv-lite@0.4.15: + version "0.4.15" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@~1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ipaddr.js@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" + +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-retry-allowed@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + +is-stream-ended@^0.1.0: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.3.tgz#a0473b267c756635486beedc7e3344e549d152ac" + +is-stream@^1.0.0, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is@^3.0.1, is@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isstream@0.1.x, isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +isurl@^1.0.0-alpha5: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" + dependencies: + has-to-string-tag-x "^1.2.0" + is-object "^1.0.1" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" + dependencies: + base64url "2.0.0" + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.9" + safe-buffer "^5.0.1" + +jws@^3.0.0, jws@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" + dependencies: + base64url "^2.0.0" + jwa "^1.1.4" + safe-buffer "^5.0.1" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.noop@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" + +lodash@4.17.4, lodash@^4.14.0, lodash@^4.15.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +lodash@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +log-driver@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" + +long@~3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + +lowercase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + dependencies: + pify "^2.3.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methmeth@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/methmeth/-/methmeth-1.1.0.tgz#e80a26618e52f5c4222861bb748510bd10e29089" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@~1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" + +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" + dependencies: + mime-db "~1.29.0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +mime@^1.2.11: + version "1.3.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +mimic-response@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" + +minimatch@^3.0.0, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +modelo@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/modelo/-/modelo-4.2.0.tgz#3b4b420023a66ca7e32bdba16e710937e14d1b0b" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mute-stream@~0.0.4: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + +nan@^2.0.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" + +ncp@1.0.x: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-1.0.1.tgz#d15367e5cb87432ba117d2bf80fdf45aecfb4246" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-forge@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" + +node-pre-gyp@^0.6.35: + version "0.6.36" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" + dependencies: + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "^2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +optjs@~3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-cancelable@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-timeout@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.0.tgz#9820f99434c5817868b4f34809ee5291660d5b6c" + dependencies: + p-finally "^1.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pkginfo@0.3.x: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" + +pkginfo@0.x.x: + version "0.4.0" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65" + +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +prompt@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prompt/-/prompt-1.0.0.tgz#8e57123c396ab988897fb327fd3aedc3e735e4fe" + dependencies: + colors "^1.1.2" + pkginfo "0.x.x" + read "1.0.x" + revalidator "0.1.x" + utile "0.3.x" + winston "2.1.x" + +protobufjs@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-5.0.2.tgz#59748d7dcf03d2db22c13da9feb024e16ab80c91" + dependencies: + ascli "~1" + bytebuffer "~5" + glob "^7.0.5" + yargs "^3.10.0" + +protochain@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/protochain/-/protochain-1.0.5.tgz#991c407e99de264aadf8f81504b5e7faf7bfa260" + +proxy-addr@~1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.4.0" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +pump@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.3.5" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.3.5.tgz#1b671c619940abcaeac0ad0e3a3c164be760993b" + dependencies: + duplexify "^3.1.2" + inherits "^2.0.1" + pump "^1.0.0" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@6.4.0, qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +qs@6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" + dependencies: + bytes "2.4.0" + iconv-lite "0.4.15" + unpipe "1.0.0" + +rc@^1.1.7: + version "1.2.1" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +read@1.0.x: + version "1.0.7" + resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + dependencies: + mute-stream "~0.0.4" + +readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +request@^2.72.0, request@^2.74.0, request@^2.79.0, request@^2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +requires-port@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +retry-request@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-2.0.5.tgz#d089a14a15db9ed60685b8602b40f4dcc0d3fb3c" + dependencies: + request "^2.81.0" + through2 "^2.0.0" + +revalidator@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b" + +rimraf@2, rimraf@2.6.1, rimraf@2.x.x, rimraf@^2.5.1, rimraf@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +"semver@2 || 3 || 4 || 5", semver@5.4.1, semver@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +send@0.15.4: + version "0.15.4" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9" + dependencies: + debug "2.6.8" + depd "~1.1.1" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + fresh "0.5.0" + http-errors "~1.6.2" + mime "1.3.4" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serializerr@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91" + dependencies: + protochain "^1.0.5" + +serve-static@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.15.4" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +split-array-stream@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/split-array-stream/-/split-array-stream-1.0.3.tgz#d2b75a8e5e0d824d52fdec8b8225839dc2e35dfa" + dependencies: + async "^2.4.0" + is-stream-ended "^0.1.0" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +stream-events@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.2.tgz#abf39f66c0890a4eb795bc8d5e859b2615b590b2" + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +string-format-obj@^1.0.0, string-format-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/string-format-obj/-/string-format-obj-1.1.0.tgz#7635610b1ef397013e8478be98a170e04983d068" + +string-template@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + +tar-pack@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +through2@^2.0.0, through2@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + +tmp@0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + dependencies: + os-tmpdir "~1.0.1" + +tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-is@~1.6.15: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + dependencies: + prepend-http "^1.0.1" + +url-to-options@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +utile@0.3.x: + version "0.3.0" + resolved "https://registry.yarnpkg.com/utile/-/utile-0.3.0.tgz#1352c340eb820e4d8ddba039a4fbfaa32ed4ef3a" + dependencies: + async "~0.9.0" + deep-equal "~0.2.1" + i "0.3.x" + mkdirp "0.x.x" + ncp "1.0.x" + rimraf "2.x.x" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +uuid@3.1.0, uuid@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +vary@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" + +winston@2.1.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.1.1.tgz#3c9349d196207fd1bdff9d4bc43ef72510e3a12e" + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + pkginfo "0.3.x" + stack-trace "0.0.x" + +winston@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.1.tgz#0b48420d978c01804cf0230b648861598225a119" + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + stack-trace "0.0.x" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.1.0.tgz#1769f4b551eedce419f0505deae2e26763542d37" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + +xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.0, y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@^3.10.0: + version "3.32.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" + dependencies: + camelcase "^2.0.1" + cliui "^3.0.3" + decamelize "^1.1.1" + os-locale "^1.4.0" + string-width "^1.0.1" + window-size "^0.1.4" + y18n "^3.2.0" diff --git a/src/server_manager/bower.json b/src/server_manager/bower.json new file mode 100644 index 000000000..9f5ac8814 --- /dev/null +++ b/src/server_manager/bower.json @@ -0,0 +1,37 @@ +{ + "name": "outline-launcher", + "description": "Launches Outline Shadowsocks servers on the cloud", + "main": "", + "authors": [ + "Outline" + ], + "license": "Apache", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "app-layout": "PolymerElements/app-layout#^2.0.4", + "iron-autogrow-textarea": "PolymerElements/iron-autogrow-textarea#^2.1.1", + "iron-icons": "PolymerElements/iron-icons#^2.0.1", + "iron-fit-behavior": "PolymerElements/iron-fit-behavior#^2.1.0", + "iron-pages": "PolymerElements/iron-pages#^2.0.1", + "paper-button": "PolymerElements/paper-button#^2.0.0", + "paper-dialog": "PolymerElements/paper-dialog#^2.0.1", + "paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^2.1.0", + "paper-dropdown-menu": "PolymerElements/paper-dropdown-menu#^2.0.2", + "paper-icon-button": "PolymerElements/paper-icon-button#^2.0.1", + "paper-input": "PolymerElements/paper-input#^2.1.0", + "paper-item": "PolymerElements/paper-item#^2.0.0", + "paper-listbox": "PolymerElements/paper-listbox#^2.0.0", + "paper-progress": "PolymerElements/paper-progress#^2.0.1", + "paper-toast": "PolymerElements/paper-toast#^2.0.0", + "paper-toggle-button": "PolymerElements/paper-toggle-button#^2.0.0", + "web-animations-js": "web-animations/web-animations-js", + "webcomponentsjs": "webcomponents/webcomponentsjs#^v1.1.0" + } +} diff --git a/src/server_manager/cloud/digitalocean_api.ts b/src/server_manager/cloud/digitalocean_api.ts new file mode 100644 index 000000000..2c4ad2973 --- /dev/null +++ b/src/server_manager/cloud/digitalocean_api.ts @@ -0,0 +1,228 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as events from 'events'; + +import * as errors from '../infrastructure/errors'; + +export interface DigitalOceanDropletSpecification { + installCommand: string; + size: string; + image: string; + tags: string[]; +} + +// Returns an OAuth redirect URL for DigitalOcean. +export function getOauthUrl(clientId: string, redirectUri: string, state?: string): string { + return 'https://cloud.digitalocean.com/v1/oauth/authorize?' + + 'client_id=' + clientId + '&' + + 'response_type=token&' + + 'redirect_uri=' + encodeURIComponent(redirectUri) + '&' + + 'state=' + encodeURIComponent(state || '') + '&' + + 'scope=read%20write'; +} + +// See definition and example at +// https://developers.digitalocean.com/documentation/v2/#retrieve-an-existing-droplet-by-id +export type DropletInfo = Readonly < { + id: number; + status: 'new'|'active'; + tags: string[]; + region: {readonly slug: string;}; + size: Readonly < { + transfer: number; + price_monthly: number; + } + > ; + networks: Readonly < { + v4: ReadonlyArray < Readonly < { + type: string; + ip_address: string; + } + >> ; + } + > ; +} +> ; + +// Reference: +// https://developers.digitalocean.com/documentation/v2/#get-user-information +export type Account = Readonly < { + email: string; + uuid: string; + email_verified: boolean; + status: string; +} +> ; + +// Reference: +// https://developers.digitalocean.com/documentation/v2/#regions +export type RegionInfo = Readonly < { + slug: string; + name: string; + sizes: string[]; + available: boolean; + features: string[]; +} +> ; + +// Marker class for errors due to network or authentication. +// See below for more details on when this is raised. +export class XhrError extends errors.OutlineError { + constructor() { + // No message because XMLHttpRequest.onerror provides no useful info. + super(); + } +} + +// This class contains methods to interact with DigitalOcean on behalf of a user. +export interface DigitalOceanSession { + accessToken: string; + getAccount(): Promise; + createDroplet( + displayName: string, region: string, publicKeyForSSH: string, + dropletSpec: DigitalOceanDropletSpecification): Promise<{droplet: DropletInfo}>; + deleteDroplet(dropletId: number): Promise; + getRegionInfo(): Promise; + getDroplet(dropletId: number): Promise; + getDropletTags(dropletId: number): Promise; + getDropletsByTag(tag: string): Promise; + getDroplets(): Promise; +} + +export function createDigitalOceanSession(accessToken: string): DigitalOceanSession { + return new RestApiSession(accessToken); +} + +class RestApiSession implements DigitalOceanSession { + // Constructor takes a DigitalOcean access token, which should have + // read+write permissions. + constructor(public accessToken: string) {} + + public getAccount(): Promise { + return this.request<{account: Account}>('GET', 'account/').then((response) => { + return response.account; + }); + } + + public createDroplet( + displayName: string, region: string, publicKeyForSSH: string, + dropletSpec: DigitalOceanDropletSpecification): Promise<{droplet: DropletInfo}> { + const dropletName = makeValidDropletName(displayName); + // Register a key with DigitalOcean, so the user will not get a potentially + // confusing email with their droplet password, which could get mistaken for + // an invite. + return this.registerKey_(dropletName, publicKeyForSSH).then((keyId: number) => { + return this.request<{droplet: DropletInfo}>('POST', 'droplets', { + name: dropletName, + region, + size: dropletSpec.size, + image: dropletSpec.image, + ssh_keys: [keyId], + user_data: dropletSpec.installCommand, + tags: dropletSpec.tags, + ipv6: true, + }); + }); + } + + public deleteDroplet(dropletId: number): Promise { + return this.request('DELETE', 'droplets/' + dropletId); + } + + public getRegionInfo(): Promise { + return this.request<{regions: RegionInfo[]}>('GET', 'regions').then((response) => { + return response.regions; + }); + } + + // Registers a SSH key with DigitalOcean. + private registerKey_(keyName: string, publicKeyForSSH: string): Promise { + return this + .request<{ssh_key: {id: number}}>( + 'POST', 'account/keys', {name: keyName, public_key: publicKeyForSSH}) + .then((response) => { + return response.ssh_key.id; + }); + } + + public getDroplet(dropletId: number): Promise { + return this.request<{droplet: DropletInfo}>('GET', 'droplets/' + dropletId).then((response) => { + return response.droplet; + }); + } + + public getDropletTags(dropletId: number): Promise { + return this.getDroplet(dropletId).then((droplet: DropletInfo) => { + return droplet.tags; + }); + } + + public getDropletsByTag(tag: string): Promise { + return this.request<{droplets: DropletInfo[]}>('GET', `droplets/?tag_name=${encodeURI(tag)}`) + .then((response) => { + return response.droplets; + }); + } + + public getDroplets(): Promise { + return this.request<{droplets: DropletInfo[]}>('GET', 'droplets/').then((response) => { + return response.droplets; + }); + } + + // Makes an XHR request to DigitalOcean's API, returns a promise which fulfills + // with the parsed object if successful. + private request(method: string, actionPath: string, data?: {}): Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open(method, `https://api.digitalocean.com/v2/${actionPath}`); + xhr.setRequestHeader('Authorization', `Bearer ${this.accessToken}`); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.onload = () => { + // DigitalOcean may return any 2xx status code for success. + if (xhr.status >= 200 && xhr.status <= 299) { + // Parse JSON response if available. For requests like DELETE + // this.response may be empty. + const responseObj = (xhr.response ? JSON.parse(xhr.response) : {}); + resolve(responseObj); + } else { + // this.response is a JSON object, whose message is an error string. + const responseJson = JSON.parse(xhr.response); + reject(new Error( + `XHR ${responseJson.id} failed with ${xhr.status}: ${responseJson.message}`)); + } + }; + xhr.onerror = () => { + // This is raised for both network-level and CORS (authentication) + // problems. Since there is, by design for security reasons, no way + // to programmatically distinguish the two (the error instance + // passed to this handler has *no* useful information), we should + // prompt the user for whether to retry or re-authenticate against + // DigitalOcean (this isn't so bad because application-level + // errors, e.g. bad request parameters and even 404s, do *not* raise + // an onerror event). + reject(new XhrError()); + }; + xhr.send(data ? JSON.stringify(data) : undefined); + }); + } +} + +// Removes invalid characters from input name so it can be used with +// DigitalOcean APIs. +function makeValidDropletName(name: string): string { + // Remove all characters outside of A-Z, a-z, 0-9 and '-'. + return name.replace(/[^A-Za-z0-9\-]/g, ''); +} diff --git a/src/server_manager/electron_app/build_action.sh b/src/server_manager/electron_app/build_action.sh new file mode 100755 index 000000000..146d43887 --- /dev/null +++ b/src/server_manager/electron_app/build_action.sh @@ -0,0 +1,53 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Builds the Electron App + +readonly OUT_DIR=$BUILD_DIR/server_manager/electron_app +rm -rf $OUT_DIR + +readonly NODE_MODULES_BIN_DIR=$ROOT_DIR/src/server_manager/node_modules/.bin + +# Build the Web App. +do_action server_manager/web_app/build + +# Compile the Electron app source. +# Since Node.js on Cygwin doesn't like absolute Unix-style paths, +# we'll use relative paths here. +tsc -p src/server_manager/electron_app/tsconfig.json --outDir build/server_manager/electron_app/js + +# Assemble everything together. +readonly MODULE_DIR=$(dirname $0) +readonly STATIC_DIR=$OUT_DIR/static +mkdir -p $STATIC_DIR +mkdir -p $STATIC_DIR/server_manager +cp -r $OUT_DIR/js/* $STATIC_DIR +cp -r $BUILD_DIR/server_manager/web_app/static $STATIC_DIR/server_manager/web_app/ +cp $MODULE_DIR/config.json $STATIC_DIR +# Our electron app assumes all HTML files will be in the web_app directory. +cp $MODULE_DIR/loading.html $STATIC_DIR/server_manager/web_app/ + +# Electron requires a package.json file for the app's name, etc. +# We also need to install NPMs at this location for require() +# in order for require() to work right in the renderer process, which +# is loaded via a custom protocol. +cp src/server_manager/package.json yarn.lock $STATIC_DIR +cd $STATIC_DIR +yarn install --prod --ignore-scripts + +# Icons. +cd $ROOT_DIR +$NODE_MODULES_BIN_DIR/electron-icon-maker --input=src/server_manager/images/launcher-icon.png --output=build/server_manager/electron_app/static diff --git a/src/server_manager/electron_app/config.json b/src/server_manager/electron_app/config.json new file mode 100644 index 000000000..98997caa0 --- /dev/null +++ b/src/server_manager/electron_app/config.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "releaseDataUrl": "https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/release/release_data.json" +} diff --git a/src/server_manager/electron_app/digital_ocean_modifications.ts b/src/server_manager/electron_app/digital_ocean_modifications.ts new file mode 100644 index 000000000..f05d326d6 --- /dev/null +++ b/src/server_manager/electron_app/digital_ocean_modifications.ts @@ -0,0 +1,476 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as electron from 'electron'; + +const ipcRenderer = electron.ipcRenderer; + +export function modifyUiIfDigitalOcean() { + // Wait for load event, to ensure that the currentUser object is loaded. + // For most signed-in DigitalOcean pages, currentUser is set via inline + // + + Loading... + + diff --git a/src/server_manager/electron_app/loading_window.ts b/src/server_manager/electron_app/loading_window.ts new file mode 100644 index 000000000..be9585665 --- /dev/null +++ b/src/server_manager/electron_app/loading_window.ts @@ -0,0 +1,49 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as electron from 'electron'; + +export class LoadingWindow { + private loadingBrowserWindow: electron.BrowserWindow; + private timeoutId: NodeJS.Timer; + + public constructor(private mainWindow: Electron.BrowserWindow, private url: string) {} + + public showInMs(delayMs: number) { + if (this.timeoutId) { + // Timeout is already set - cancel it. + clearTimeout(this.timeoutId); + } + + this.timeoutId = global.setTimeout(() => { + this.timeoutId = null; + this.loadingBrowserWindow = new electron.BrowserWindow(); + this.loadingBrowserWindow.loadURL(this.url); + this.loadingBrowserWindow.setBounds(this.mainWindow.getBounds()); + this.mainWindow.hide(); + }, delayMs); + } + + public hide() { + if (this.timeoutId) { + // loadingBrowserWindow has not been displayed yet, cancel the timeout. + clearTimeout(this.timeoutId); + } + if (this.loadingBrowserWindow) { + this.loadingBrowserWindow.close(); + this.loadingBrowserWindow = null; + } + this.mainWindow.show(); + } +} diff --git a/src/server_manager/electron_app/menu.ts b/src/server_manager/electron_app/menu.ts new file mode 100644 index 000000000..650080e9a --- /dev/null +++ b/src/server_manager/electron_app/menu.ts @@ -0,0 +1,45 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as electron from 'electron'; + +// We only want a menu if we're running on macOS or in debug mode, or both. +// Note that when invoked via the electron command line tool, default_app +// adds a menu. We can't disable that. +export function getMenuTemplate(debugMode: boolean): Electron.MenuItemConstructorOptions[] { + const template: Electron.MenuItemConstructorOptions[] = []; + + if (process.platform === 'darwin') { + template.push( + // From default_app's main.js. + { + submenu: [ + {role: 'about'}, {type: 'separator'}, {role: 'services', submenu: []}, + {type: 'separator'}, {role: 'hide'}, {role: 'hideothers'}, {role: 'unhide'}, + {type: 'separator'}, {role: 'quit'} + ] + }, + // editMenu is required for copy+paste keyboard shortcuts to work on Mac. + {role: 'editMenu'}); + } + + if (debugMode) { + template.push({ + label: 'Developer', + submenu: [{role: 'reload'}, {role: 'forcereload'}, {role: 'toggledevtools'}] + }); + } + + return template; +} \ No newline at end of file diff --git a/src/server_manager/electron_app/package_linux_action.sh b/src/server_manager/electron_app/package_linux_action.sh new file mode 100755 index 000000000..61d76c1fc --- /dev/null +++ b/src/server_manager/electron_app/package_linux_action.sh @@ -0,0 +1,37 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +yarn do server_manager/electron_app/build + +$ROOT_DIR/src/server_manager/node_modules/.bin/electron-builder \ + --projectDir=build/server_manager/electron_app/static \ + --publish=never \ + --x64 \ + --linux AppImage \ + --config.linux.icon=icons/png \ + --config.linux.category=Network \ + --config.artifactName='Outline-Manager.${ext}' + +for arch in ia32 x64; do + $ROOT_DIR/src/server_manager/node_modules/.bin/electron-builder \ + --projectDir=build/server_manager/electron_app/static \ + --publish=never \ + --$arch \ + --linux deb rpm tar.gz \ + --config.linux.icon=icons/png \ + --config.linux.category=Network \ + --config.artifactName='Outline-Manager-'${arch}'.${ext}' +done diff --git a/src/server_manager/electron_app/package_macos_action.sh b/src/server_manager/electron_app/package_macos_action.sh new file mode 100755 index 000000000..2632aa69e --- /dev/null +++ b/src/server_manager/electron_app/package_macos_action.sh @@ -0,0 +1,24 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +yarn do server_manager/electron_app/build + +$ROOT_DIR/src/server_manager/node_modules/.bin/electron-builder \ + --projectDir=build/server_manager/electron_app/static \ + --publish=never \ + --mac dmg \ + --config.mac.icon=icons/mac/icon.icns \ + --config.artifactName='Outline-Manager.${ext}' diff --git a/src/server_manager/electron_app/package_only_windows_action.sh b/src/server_manager/electron_app/package_only_windows_action.sh new file mode 100755 index 000000000..e44cb736a --- /dev/null +++ b/src/server_manager/electron_app/package_only_windows_action.sh @@ -0,0 +1,27 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script is mostly intended for the continuous build, which takes place +# in a Docker container. *Building* is not supported by that Docker image +# so we build separately. + +$ROOT_DIR/src/server_manager/node_modules/.bin/electron-builder \ + --projectDir=build/server_manager/electron_app/static \ + --publish=never \ + --ia32 \ + --win nsis \ + --config.win.icon=icons/win/icon.ico \ + --config.artifactName='Outline-Manager.${ext}' diff --git a/src/server_manager/electron_app/preload.ts b/src/server_manager/electron_app/preload.ts new file mode 100644 index 000000000..aa3f8ab0e --- /dev/null +++ b/src/server_manager/electron_app/preload.ts @@ -0,0 +1,34 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as electron from 'electron'; +import * as digital_ocean_modifications from './digital_ocean_modifications'; + +const ipcRenderer = electron.ipcRenderer; + +interface ElectronGlobal extends NodeJS.Global { + whitelistCertificate: (fingerprint: string) => void; + clearDigitalOceanCookies: () => void; +} + +process.once('loaded', () => { + (global as ElectronGlobal).whitelistCertificate = (fingerprint: string) => { + return ipcRenderer.sendSync('whitelist-certificate', fingerprint); + }; + (global as ElectronGlobal).clearDigitalOceanCookies = () => { + return ipcRenderer.sendSync('clear-digital-ocean-cookies'); + }; +}); + +digital_ocean_modifications.modifyUiIfDigitalOcean(); diff --git a/src/server_manager/electron_app/release_action.sh b/src/server_manager/electron_app/release_action.sh new file mode 100755 index 000000000..52196c2fd --- /dev/null +++ b/src/server_manager/electron_app/release_action.sh @@ -0,0 +1,80 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Get INCREMENT_TYPE +if (( $# <= 0 )); then + echo "usage: ./release_action.sh [major|minor|patch]" + exit 1; +fi +readonly INCREMENT_TYPE=$1 + +readonly MODULE_DIR=$(dirname $0) +readonly SCRIPTS_DIR=$MODULE_DIR/release_scripts/ +readonly CONFIG_FILE=$MODULE_DIR/config.json + +# Check that we are on the latest $BRANCH_FOR_RELEASE branch with no changes. +readonly BRANCH_FOR_RELEASE="master" +if [[ $(git rev-parse --abbrev-ref HEAD) != "$BRANCH_FOR_RELEASE" ]]; then + echo "Must be on $BRANCH_FOR_RELEASE branch" + exit 1 +fi +if [[ $(git diff) != "" ]]; then + echo "Must have no local changes" + exit 1 +fi +git pull origin $BRANCH_FOR_RELEASE + +# Create a new branch for this release - will be deleted after this script is +# run regardless of whether the release is successful or not. +readonly BRANCH_NAME="release-$(date "+%Y%m%d-%H%M%S")" +git checkout -b $BRANCH_NAME + +# Set trap to return to $BRANCH_FOR_RELEASE branch and delete temporary branch. +function finish { + git checkout $BRANCH_FOR_RELEASE + git branch -D $BRANCH_NAME +} +trap finish EXIT + +function getConfigVersion { + echo $(node $SCRIPTS_DIR/get_config_version.js $CONFIG_FILE) +} + +# Update electron_app/config.json and store old and new versions. +readonly OLD_VERSION=$(getConfigVersion) +node $SCRIPTS_DIR/bump_electron_version.js $CONFIG_FILE $INCREMENT_TYPE +readonly NEW_VERSION=$(getConfigVersion) + +# Create packages and copy them into releases directory. +yarn run clean +yarn +do_action server_manager/electron_app/package +cp -f ${BUILD_DIR}/server_manager/electron_app/static/dist/*.* releases/ + +# Update releases/release_data.json +sed -i "" "s/$OLD_VERSION/$NEW_VERSION/g" releases/release_data.json + +# Push to github +git add . +git commit -m "new release" +git push origin $BRANCH_NAME +readonly TAG_NAME="v$NEW_VERSION" +git tag $TAG_NAME +git push origin $TAG_NAME + +# Open a pull request in github. +readonly GITHUB_URL="https://github.com/Jigsaw-Code/outline-server/compare/$BRANCH_NAME?expand=1" +open $GITHUB_URL diff --git a/src/server_manager/electron_app/release_linux_action.sh b/src/server_manager/electron_app/release_linux_action.sh new file mode 100755 index 000000000..12e7c2e0c --- /dev/null +++ b/src/server_manager/electron_app/release_linux_action.sh @@ -0,0 +1,17 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +yarn do server_manager/electron_app/package_linux diff --git a/src/server_manager/electron_app/release_macos_action.sh b/src/server_manager/electron_app/release_macos_action.sh new file mode 100755 index 000000000..395726bf7 --- /dev/null +++ b/src/server_manager/electron_app/release_macos_action.sh @@ -0,0 +1,24 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +readonly CSC_LINK=${CSC_LINK:-unset} + +if [ "$CSC_LINK" == "unset" ]; then + echo "must specify CSC_LINK" + exit 1 +fi + +yarn do server_manager/electron_app/package_macos diff --git a/src/server_manager/electron_app/release_scripts/bump_electron_version.js b/src/server_manager/electron_app/release_scripts/bump_electron_version.js new file mode 100644 index 000000000..1f5402511 --- /dev/null +++ b/src/server_manager/electron_app/release_scripts/bump_electron_version.js @@ -0,0 +1,39 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Increments the "version" field in the Outline Manager Electron app's +// config.json file. + +const fs = require('fs'); +const semver = require('semver'); + +const configFilename = process.argv[2]; +const bumpType = process.argv[3]; +if (!configFilename || !bumpType) { + console.error('usage: node bump_electron_version.js <[major|minor|patch]>'); + process.exit(1); +} else if (!(new Set(['major', 'minor', 'patch'])).has(bumpType)) { + console.error('bumpType must be major, minor, or patch'); + process.exit(1); +} + +// Read the config file. +const configText = fs.readFileSync(configFilename, {encoding: 'utf8'}); +const configObj = JSON.parse(configText); + +// Write new config file. +configObj.version = semver.inc(configObj.version, bumpType); +const newConfigJson = JSON.stringify(configObj, null, 2); +fs.writeFileSync(configFilename, newConfigJson, {encoding: 'utf8'}); +console.log('Updated ' + configFilename + ' to version ' + configObj.version); diff --git a/src/server_manager/electron_app/release_scripts/get_config_version.js b/src/server_manager/electron_app/release_scripts/get_config_version.js new file mode 100644 index 000000000..8e8c1c686 --- /dev/null +++ b/src/server_manager/electron_app/release_scripts/get_config_version.js @@ -0,0 +1,28 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Returns the version from the Outline Manager Electron app's config. + +const fs = require('fs'); + +const configFilename = process.argv[2]; +if (!configFilename) { + console.error('usage: node get_config_version.js '); + process.exit(1); +} + +// Read the config file version. +const configText = fs.readFileSync(configFilename, {encoding: 'utf8'}); +const configObj = JSON.parse(configText); +console.log(configObj.version) diff --git a/src/server_manager/electron_app/release_windows_action.sh b/src/server_manager/electron_app/release_windows_action.sh new file mode 100755 index 000000000..676405896 --- /dev/null +++ b/src/server_manager/electron_app/release_windows_action.sh @@ -0,0 +1,26 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +yarn do server_manager/electron_app/build + +$ROOT_DIR/src/server_manager/node_modules/.bin/electron-builder \ + --projectDir=build/server_manager/electron_app/static \ + --publish=never \ + --ia32 \ + --win nsis \ + --config.win.icon=icons/win/icon.ico \ + --config.win.certificateSubjectName='Jigsaw Operations LLC' \ + --config.artifactName='Outline-Manager.${ext}' diff --git a/src/server_manager/electron_app/run_action.sh b/src/server_manager/electron_app/run_action.sh new file mode 100755 index 000000000..cb5600ddc --- /dev/null +++ b/src/server_manager/electron_app/run_action.sh @@ -0,0 +1,25 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +do_action server_manager/electron_app/build + +readonly NODE_MODULES_BIN_DIR=$ROOT_DIR/src/server_manager/node_modules/.bin + +cd $BUILD_DIR/server_manager/electron_app/static +OUTLINE_DEBUG=true \ +SB_METRICS_URL=https://metrics-test.uproxy.org \ +SENTRY_DSN=https://ee9db4eb185b471ca08c8eb5efbf61f1@sentry.io/214597 \ +$NODE_MODULES_BIN_DIR/electron . diff --git a/src/server_manager/electron_app/tsconfig.json b/src/server_manager/electron_app/tsconfig.json new file mode 100644 index 000000000..218ab400a --- /dev/null +++ b/src/server_manager/electron_app/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es2016", + "removeComments": false, + "noImplicitAny": true, + "module": "commonjs", + "rootDir": ".", + "lib": [ + "dom", + "es2016" + ] + }, + "include": [ + "index.ts", + "preload.ts", + "digital_ocean_modifications.ts", + "../types/*.d.ts" + ], + "exclude": [ + "node_modules" + ], + "compileOnSave": true +} diff --git a/src/server_manager/electron_app/update_checker.ts b/src/server_manager/electron_app/update_checker.ts new file mode 100644 index 000000000..d848a816a --- /dev/null +++ b/src/server_manager/electron_app/update_checker.ts @@ -0,0 +1,76 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as electron from 'electron'; +import * as fs from 'fs'; +import * as https from 'https'; +import * as os from 'os'; +import * as request from 'request-lite'; +import * as semver from 'semver'; +import * as url from 'url'; + +interface ReleaseData { + location?: string; + version?: string; + buildTimestamp?: number; + md5?: string; +} + +function getManagerReleaseData(releaseDataUrl: string): Promise { + const releaseName = `outline-manager-${os.platform()}-${os.arch()}`; + return new Promise((fulfill, reject) => { + request(releaseDataUrl, (error, response, body) => { + if (error) { + return reject(error); + } + try { + const data: ReleaseData = JSON.parse(body)['latestVersions'][releaseName]; + if (!data.version || !data.location) { + return reject('invalid managerReleaseData ' + data); + } + return fulfill(data); + } catch (e) { + return reject('Unable to fetch latest manager info ' + e); + } + }); + }); +} + +export function checkForUpdates(currentVersion: string, releaseDataUrl: string) { + getManagerReleaseData(releaseDataUrl) + .then((managerReleaseData) => { + const latestVersion = managerReleaseData.version; + const semverDiff = semver.diff(latestVersion, currentVersion); + // Only prompt for major and minor updates, ignoring patch, etc. + if ((semverDiff === 'major' || semverDiff === 'minor') && + semver.gt(latestVersion, currentVersion)) { + electron.dialog.showMessageBox( + { + message: `A new version of the Outline Manager is available.\n\nCurrent version: ${ + currentVersion}\nNew version: ${latestVersion}`, + buttons: ['Download', 'Cancel'] + }, + (clickedButtonIndex: number) => { + if (clickedButtonIndex === 0) { + electron.shell.openExternal(managerReleaseData.location); + } + }); + } + }) + .catch((e: Error) => { + // Print error and prevent from propagating as we can still run the manager + // without checking for new releases. + console.error('Unable to fetch latest manager info', e); + }); +} diff --git a/src/server_manager/images/back.svg b/src/server_manager/images/back.svg new file mode 100644 index 000000000..695c08950 --- /dev/null +++ b/src/server_manager/images/back.svg @@ -0,0 +1,15 @@ + + + + back + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/check.svg b/src/server_manager/images/check.svg new file mode 100644 index 000000000..c658d45e0 --- /dev/null +++ b/src/server_manager/images/check.svg @@ -0,0 +1,15 @@ + + + + check copy 2 + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/connect-tip-2x.png b/src/server_manager/images/connect-tip-2x.png new file mode 100644 index 000000000..902cafb83 Binary files /dev/null and b/src/server_manager/images/connect-tip-2x.png differ diff --git a/src/server_manager/images/connected_large.png b/src/server_manager/images/connected_large.png new file mode 100644 index 000000000..b4cbef1d6 Binary files /dev/null and b/src/server_manager/images/connected_large.png differ diff --git a/src/server_manager/images/digital_ocean_logo.svg b/src/server_manager/images/digital_ocean_logo.svg new file mode 100644 index 000000000..6214fb9e0 --- /dev/null +++ b/src/server_manager/images/digital_ocean_logo.svg @@ -0,0 +1,19 @@ + + + + DO_Logo_icon_white + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/empty_user.png b/src/server_manager/images/empty_user.png new file mode 100644 index 000000000..9ff64ed89 Binary files /dev/null and b/src/server_manager/images/empty_user.png differ diff --git a/src/server_manager/images/favicon.png b/src/server_manager/images/favicon.png new file mode 100644 index 000000000..af770b166 Binary files /dev/null and b/src/server_manager/images/favicon.png differ diff --git a/src/server_manager/images/flags/canada.png b/src/server_manager/images/flags/canada.png new file mode 100755 index 000000000..2faff870e Binary files /dev/null and b/src/server_manager/images/flags/canada.png differ diff --git a/src/server_manager/images/flags/germany.png b/src/server_manager/images/flags/germany.png new file mode 100755 index 000000000..fd0d48b5c Binary files /dev/null and b/src/server_manager/images/flags/germany.png differ diff --git a/src/server_manager/images/flags/india.png b/src/server_manager/images/flags/india.png new file mode 100644 index 000000000..4585269d1 Binary files /dev/null and b/src/server_manager/images/flags/india.png differ diff --git a/src/server_manager/images/flags/netherlands.png b/src/server_manager/images/flags/netherlands.png new file mode 100755 index 000000000..5f8d6a148 Binary files /dev/null and b/src/server_manager/images/flags/netherlands.png differ diff --git a/src/server_manager/images/flags/singapore.png b/src/server_manager/images/flags/singapore.png new file mode 100755 index 000000000..bbac9ab1d Binary files /dev/null and b/src/server_manager/images/flags/singapore.png differ diff --git a/src/server_manager/images/flags/uk.png b/src/server_manager/images/flags/uk.png new file mode 100755 index 000000000..3bab1bb13 Binary files /dev/null and b/src/server_manager/images/flags/uk.png differ diff --git a/src/server_manager/images/flags/us.png b/src/server_manager/images/flags/us.png new file mode 100755 index 000000000..03dd52008 Binary files /dev/null and b/src/server_manager/images/flags/us.png differ diff --git a/src/server_manager/images/footer_logo.png b/src/server_manager/images/footer_logo.png new file mode 100644 index 000000000..48713390a Binary files /dev/null and b/src/server_manager/images/footer_logo.png differ diff --git a/src/server_manager/images/github-icon.png b/src/server_manager/images/github-icon.png new file mode 100644 index 000000000..7b0609fa4 Binary files /dev/null and b/src/server_manager/images/github-icon.png differ diff --git a/src/server_manager/images/ic_done_white_24dp.svg b/src/server_manager/images/ic_done_white_24dp.svg new file mode 100644 index 000000000..076f89762 --- /dev/null +++ b/src/server_manager/images/ic_done_white_24dp.svg @@ -0,0 +1,16 @@ + + + + Group + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/intro-DigitalOcean.png b/src/server_manager/images/intro-DigitalOcean.png new file mode 100644 index 000000000..838207ce1 Binary files /dev/null and b/src/server_manager/images/intro-DigitalOcean.png differ diff --git a/src/server_manager/images/intro-uProxy.png b/src/server_manager/images/intro-uProxy.png new file mode 100644 index 000000000..722f0249a Binary files /dev/null and b/src/server_manager/images/intro-uProxy.png differ diff --git a/src/server_manager/images/jigsaw-logo.svg b/src/server_manager/images/jigsaw-logo.svg new file mode 100644 index 000000000..178dcb161 --- /dev/null +++ b/src/server_manager/images/jigsaw-logo.svg @@ -0,0 +1,20 @@ + + + + Jigsaw + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/key-avatar.svg b/src/server_manager/images/key-avatar.svg new file mode 100644 index 000000000..e1a2c3e88 --- /dev/null +++ b/src/server_manager/images/key-avatar.svg @@ -0,0 +1,17 @@ + + + + key-avater + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/key-avater.svg b/src/server_manager/images/key-avater.svg new file mode 100644 index 000000000..e1a2c3e88 --- /dev/null +++ b/src/server_manager/images/key-avater.svg @@ -0,0 +1,17 @@ + + + + key-avater + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/key-empty.svg b/src/server_manager/images/key-empty.svg new file mode 100644 index 000000000..ca144449a --- /dev/null +++ b/src/server_manager/images/key-empty.svg @@ -0,0 +1,19 @@ + + + + key-empty + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/key-tip-2x.png b/src/server_manager/images/key-tip-2x.png new file mode 100644 index 000000000..2472c1e36 Binary files /dev/null and b/src/server_manager/images/key-tip-2x.png differ diff --git a/src/server_manager/images/launcher-icon.png b/src/server_manager/images/launcher-icon.png new file mode 100644 index 000000000..1a2141c73 Binary files /dev/null and b/src/server_manager/images/launcher-icon.png differ diff --git a/src/server_manager/images/manager-about-logo2x.png b/src/server_manager/images/manager-about-logo2x.png new file mode 100644 index 000000000..c5e2d2eb4 Binary files /dev/null and b/src/server_manager/images/manager-about-logo2x.png differ diff --git a/src/server_manager/images/manager-key-avatar.svg b/src/server_manager/images/manager-key-avatar.svg new file mode 100644 index 000000000..b7746a9a4 --- /dev/null +++ b/src/server_manager/images/manager-key-avatar.svg @@ -0,0 +1,15 @@ + + + + manager-key-avatar + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/manager-profile-2x.png b/src/server_manager/images/manager-profile-2x.png new file mode 100644 index 000000000..0d45dedbd Binary files /dev/null and b/src/server_manager/images/manager-profile-2x.png differ diff --git a/src/server_manager/images/metrics.png b/src/server_manager/images/metrics.png new file mode 100644 index 000000000..e8cb9435b Binary files /dev/null and b/src/server_manager/images/metrics.png differ diff --git a/src/server_manager/images/reddit-icon.png b/src/server_manager/images/reddit-icon.png new file mode 100644 index 000000000..38bf21c29 Binary files /dev/null and b/src/server_manager/images/reddit-icon.png differ diff --git a/src/server_manager/images/server_avatar.png b/src/server_manager/images/server_avatar.png new file mode 100644 index 000000000..cba5017cb Binary files /dev/null and b/src/server_manager/images/server_avatar.png differ diff --git a/src/server_manager/images/share_chat.svg b/src/server_manager/images/share_chat.svg new file mode 100755 index 000000000..de6133378 --- /dev/null +++ b/src/server_manager/images/share_chat.svg @@ -0,0 +1,43 @@ + + + + Group 51 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/share_copy.svg b/src/server_manager/images/share_copy.svg new file mode 100755 index 000000000..8cc100388 --- /dev/null +++ b/src/server_manager/images/share_copy.svg @@ -0,0 +1,43 @@ + + + + Group 54 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/share_email.svg b/src/server_manager/images/share_email.svg new file mode 100755 index 000000000..c23e67dfc --- /dev/null +++ b/src/server_manager/images/share_email.svg @@ -0,0 +1,46 @@ + + + + Group 52 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/terminal.svg b/src/server_manager/images/terminal.svg new file mode 100644 index 000000000..c1265beed --- /dev/null +++ b/src/server_manager/images/terminal.svg @@ -0,0 +1,18 @@ + + + + terminal + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/images/uproxy-black-and-white.png b/src/server_manager/images/uproxy-black-and-white.png new file mode 100644 index 000000000..5ca667064 Binary files /dev/null and b/src/server_manager/images/uproxy-black-and-white.png differ diff --git a/src/server_manager/images/uproxy.png b/src/server_manager/images/uproxy.png new file mode 100644 index 000000000..f659abf56 Binary files /dev/null and b/src/server_manager/images/uproxy.png differ diff --git a/src/server_manager/index.html b/src/server_manager/index.html new file mode 100644 index 000000000..79c29dd04 --- /dev/null +++ b/src/server_manager/index.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Outline Manager + + + + + + diff --git a/src/server_manager/infrastructure/crypto.ts b/src/server_manager/infrastructure/crypto.ts new file mode 100644 index 000000000..9358025b0 --- /dev/null +++ b/src/server_manager/infrastructure/crypto.ts @@ -0,0 +1,32 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as forge from 'node-forge'; + +// Keys are in OpenSSH format +export class KeyPair { + public: string; + private: string; +} + +// Generates an RSA keypair using forge +export function generateKeyPair(): KeyPair { + const pair = forge.pki.rsa.generateKeyPair({bits: 1538}); + // trim() the string because forge adds a trailing space to + // public keys which really messes things up later. + return { + public: forge.ssh.publicKeyToOpenSSH(pair.publicKey, '').trim(), + private: forge.ssh.privateKeyToOpenSSH(pair.privateKey, '').trim() + }; +} diff --git a/src/server_manager/infrastructure/errors.ts b/src/server_manager/infrastructure/errors.ts new file mode 100644 index 000000000..59a805f92 --- /dev/null +++ b/src/server_manager/infrastructure/errors.ts @@ -0,0 +1,44 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export class OutlineError extends Error { + constructor(message?: string) { + // ref: + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + super(message); // 'Error' breaks prototype chain here + Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain + this.name = new.target.name; + } +} + +// Error thrown when a shadowbox server cannot be reached (e.g. due to Firewall) +export class UnreachableServerError extends OutlineError { + constructor(message?: string) { + super(message); + } +} + +// Error thrown when trying to access a server that has been deleted. +export class DeletedServerError extends OutlineError { + constructor(message?: string) { + super(message); + } +} + +// Error thrown when server installation failed. +export class ServerInstallFailedError extends OutlineError { + constructor(message?: string) { + super(message); + } +} diff --git a/src/server_manager/infrastructure/hex_encoding.ts b/src/server_manager/infrastructure/hex_encoding.ts new file mode 100644 index 000000000..28f9e49ad --- /dev/null +++ b/src/server_manager/infrastructure/hex_encoding.ts @@ -0,0 +1,40 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export function asciiToHex(text: string) { + // Assumes that text is no more than 8 bits per char, i.e. no unicode. + const hexBytes: string[] = []; + for (let i = 0; i < text.length; ++i) { + const charCode = text.charCodeAt(i); + if (charCode > 0xFF) { + // Consider supporting non-ascii characters: + // http://monsur.hossa.in/2012/07/20/utf-8-in-javascript.html + throw new Error(`Cannot encode wide character with value ${charCode}`); + } + hexBytes.push(('0' + charCode.toString(16)).slice(-2)); + } + return hexBytes.join(''); +} + +export function hexToString(hexString: string) { + const bytes: string[] = []; + if (hexString.length % 2 !== 0) { + throw new Error('hexString has odd length, ignoring: ' + hexString); + } + for (let i = 0; i < hexString.length; i += 2) { + const hexByte = hexString.slice(i, i + 2); + bytes.push(String.fromCharCode(parseInt(hexByte, 16))); + } + return bytes.join(''); +} diff --git a/src/server_manager/install_scripts/build_install_script_ts.node.js b/src/server_manager/install_scripts/build_install_script_ts.node.js new file mode 100644 index 000000000..4da2d950f --- /dev/null +++ b/src/server_manager/install_scripts/build_install_script_ts.node.js @@ -0,0 +1,29 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const tarballBinary = fs.readFileSync(process.argv[2]); +const base64Tarball = new Buffer(tarballBinary).toString('base64'); +const scriptText = ` +(base64 --decode | tar --extract --gzip ) <&1 >$SHADOWBOX_DIR/install-shadowbox-output + +# Initialize sentry log file. +export SENTRY_LOG_FILE="$SHADOWBOX_DIR/sentry-log-file.txt" +> $SENTRY_LOG_FILE +function log_for_sentry() { + echo [$(date "+%Y-%m-%d@%H:%M:%S")] "do_install_server.sh" "$@" >>$SENTRY_LOG_FILE +} +function post_sentry_report() { + if [[ -n "$SENTRY_API_URL" ]]; then + # Get JSON formatted string. This command replaces newlines with literal '\n' + # but otherwise assumes that there are no other characters to escape for JSON. + # If we need better escaping, we can install the jq command line tool. + readonly SENTRY_PAYLOAD_BYTE_LIMIT=8000 + SENTRY_PAYLOAD="{\"message\": \"Install error:\n$(cat $SENTRY_LOG_FILE | awk '{printf "%s\\n", $0}' | tail --bytes $SENTRY_PAYLOAD_BYTE_LIMIT)\"}" + # See Sentry documentation at: + # https://media.readthedocs.org/pdf/sentry/7.1.0/sentry.pdf + curl "$SENTRY_API_URL" -H "Origin: shadowbox" --data-binary "$SENTRY_PAYLOAD" + fi +} + +# For backward-compatibility: +readonly DO_ACCESS_TOKEN="${DO_ACCESS_TOKEN:-ACCESS_TOKEN}" + +if [[ -z "${DO_ACCESS_TOKEN}" ]]; then + echo "Access token must be supplied" + exit 1 +fi + +# DigitalOcean's Metadata API base url. +# This URL only supports HTTP (not HTTPS) requests, however it is a local link +# address so not at risk for man-in-the-middle attacks or eavesdropping. +# More detail at https://serverfault.com/questions/427018/what-is-this-ip-address-169-254-169-254 +readonly DO_METADATA_URL="http://169.254.169.254/metadata/v1" + +function cloud::public_ip() { + curl ${DO_METADATA_URL}/interfaces/public/0/ipv4/address +} + +# Applies a tag to this droplet. +function cloud::add_tag() { + local tag="$1" + declare -a base_flags=(-X POST -H 'Content-Type: application/json') + base_flags+=(-H "Authorization: Bearer ${DO_ACCESS_TOKEN}") + local TAGS_URL='https://api.digitalocean.com/v2/tags' + # Create the tag + curl "${base_flags[@]}" -d "{\"name\":\"${tag}\"}" "${TAGS_URL}" + local droplet_id="$(curl ${DO_METADATA_URL}/id)" + printf -v droplet_obj ' +{ + "resources": [{ + "resource_id": "%s", + "resource_type": "droplet" + }] +}' "${droplet_id}" + # Link the tag to this droplet + curl "${base_flags[@]}" -d "${droplet_obj}" "${TAGS_URL}/${tag}/resources" +} + +# Adds a key-value tag to the droplet. +# Takes the key as the only argument and reads the value from stdin. +# add_kv_tag() converts the input value to hex, because (1) DigitalOcean +# tags may only contain letters, numbers, : - and _, and (2) there is +# currently a bug that makes tags case-insensitive, so we can't use base64. +function cloud::add_kv_tag() { + local key="$1" + local value="$(xxd -p -c 255)" + cloud::add_tag "kv:${key}:${value}" +} + +# Adds a key-value tag where the value is already hex-encoded. +function cloud::add_encoded_kv_tag() { + local key="$1" + read value + cloud::add_tag "kv:${key}:${value}" +} + +log_for_sentry "Starting install" + +# DigitalOcean's docker image comes with ufw enabled by default, disable so when +# can serve the shadowbox manager and instances on arbitrary high number ports. +log_for_sentry "Disabling ufw" +ufw disable + +# Recent DigitalOcean Ubuntu droplets have unattended-upgrades configured from +# the outset but we want to enable automatic rebooting so that critical updates +# are applied without the Outline user's intervention. +readonly UNATTENDED_UPGRADES_CONFIG=/etc/apt/apt.conf.d/50unattended-upgrades +if [ -f $UNATTENDED_UPGRADES_CONFIG ]; then + log_for_sentry "Configuring auto-updates" + cat >> $UNATTENDED_UPGRADES_CONFIG << EOF + +// Enabled by Outline manager installer. +Unattended-Upgrade::Automatic-Reboot "true"; +EOF +fi + +log_for_sentry "Getting SB_PUBLIC_IP" +export SB_PUBLIC_IP=$(cloud::public_ip) + +log_for_sentry "Initializing ACCESS_CONFIG" +export ACCESS_CONFIG="$SHADOWBOX_DIR/access.txt" +> $ACCESS_CONFIG + +# Set trap which publishes an error tag and sentry report only if there is an error. +function finish { + INSTALL_SERVER_EXIT_CODE=$? + log_for_sentry "In EXIT trap, exit code $INSTALL_SERVER_EXIT_CODE" + if [[ -z $(grep apiUrl $ACCESS_CONFIG) ]] || [[ -z $(grep certSha256 $ACCESS_CONFIG) ]]; then + echo "INSTALL_SCRIPT_FAILED: $INSTALL_SERVER_EXIT_CODE" | cloud::add_kv_tag "install-error" + # Post error report to sentry. + post_sentry_report + fi +} +trap finish EXIT + +# Run install script asynchronously, so tags can be written as soon as they are ready. +log_for_sentry "Running install_server.sh" +./install_server.sh& +install_pid=$! + +# Save tags for access information. +log_for_sentry "Reading tags from ACCESS_CONFIG" +tail -f $ACCESS_CONFIG --pid=$install_pid | while IFS=: read key value; do + case "$key" in + certSha256) + # Bypass encoding + log_for_sentry "Writing certSha256 tag" + echo $value | cloud::add_encoded_kv_tag "$key" + ;; + apiUrl) + log_for_sentry "Writing apiUrl tag" + echo -n $value | cloud::add_kv_tag "$key" + ;; + esac +done + +# Wait for install script to finish, so that if there is any error in install_server.sh, +# the finish trap in this file will be able to access its error code. +wait $install_pid diff --git a/src/server_manager/install_scripts/install_server.sh b/src/server_manager/install_scripts/install_server.sh new file mode 100755 index 000000000..fc5b97804 --- /dev/null +++ b/src/server_manager/install_scripts/install_server.sh @@ -0,0 +1,247 @@ +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Script to install a shadowbox docker container, a watchtower docker container +# (to automatically update shadowbox), and to create a new shadowbox user. + +# You may set the following environment variables, overriding their defaults: +# SB_IMAGE: Shadowbox Docker image to install, e.g. quay.io/outline/shadowbox:nightly +# SB_API_PORT: The port number of the management API. +# SHADOWBOX_DIR: Directory for persistent Shadowbox state. +# SB_PUBLIC_IP: The public IP address for Shadowbox. +# ACCESS_CONFIG: The location of the access config text file. +# SB_DEFAULT_SERVER_NAME: Default name for this server, e.g. "Outline Server New York". +# This name will be used for the server until the admins updates the name +# via the REST API. +# SENTRY_LOG_FILE: File for writing logs which may be reported to Sentry, in case +# of an install error. No PII should be written to this file. Intended to be set +# only by do_install_server.sh. + +# Requires curl and docker to be installed + +set -euo pipefail + +readonly SENTRY_LOG_FILE=${SENTRY_LOG_FILE:-} + +function log_error() { + readonly ERROR_TEXT="\033[0;31m" # red + readonly NO_COLOR="\033[0m" + >&2 printf "${ERROR_TEXT}${1}${NO_COLOR}\n" +} + +# Pretty prints text to stdout, and also writes to sentry log file if set. +function log_step() { + log_for_sentry $1 + str="> $1" + okStr=" OK" + lineLength=50 + echo -n "$str" + numDots=$(expr $lineLength - ${#str} - ${#okStr} - 1) + if [[ $numDots > 0 ]]; then + echo -n " " + for i in $(seq 1 "$numDots"); do echo -n .; done + fi + echo "$okStr" +} + +function command_exists { + command -v "$@" > /dev/null 2>&1 +} + +function log_for_sentry() { + if [[ -n "$SENTRY_LOG_FILE" ]]; then + echo [$(date "+%Y-%m-%d@%H:%M:%S")] "install_server.sh" "$@" >>$SENTRY_LOG_FILE + fi +} + +# Check to see if docker is installed. +log_step "Verifying that Docker is installed" +if ! command_exists docker; then + log_error "Docker must be installed, please run \"curl -sS https://get.docker.com/ | sh\" or visit https://www.docker.com/" + exit 1 +fi + +# Set trap which publishes error tag only if there is an error. +function finish { + EXIT_CODE=$? + if [[ $EXIT_CODE -ne 0 ]] + then + log_error "\nSorry! Something went wrong. If you can't figure this out, please copy and paste all this output into the Outline Manager screen, and send it to us, to see if we can help you." + fi +} +trap finish EXIT + +function get_random_port { + local num=0 # Init to an invalid value, to prevent "unbound variable" errors. + until (( 1024 <= num && num < 65536)); do + num=$(( $RANDOM + ($RANDOM % 2) * 32768 )); + done; + echo $num; +} + +install_shadowbox() { + log_for_sentry "Creating shadowbox directory" + export SHADOWBOX_DIR="${SHADOWBOX_DIR:-${HOME:-/root}/.shadowbox}" + mkdir -p $SHADOWBOX_DIR + + log_for_sentry "Setting API port" + readonly SB_API_PORT="${SB_API_PORT:-$(get_random_port)}" + readonly ACCESS_CONFIG=${ACCESS_CONFIG:-$SHADOWBOX_DIR/access.txt} + readonly SB_IMAGE=${SB_IMAGE:-quay.io/outline/shadowbox:stable} + + log_for_sentry "Setting SB_PUBLIC_IP" + # TODO(fortuna): Make sure this is IPv4 + readonly SB_PUBLIC_IP=${SB_PUBLIC_IP:-$(curl -4s https://ipinfo.io/ip)} + + log_step "Setting up key functions and constants" + + if [[ ! "$SB_PUBLIC_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid IP lookup result: $SB_PUBLIC_IP" + log_for_sentry "Invalid IP lookup result" + exit 1 + fi + + function output_config() { + echo "$@" >> $ACCESS_CONFIG + } + + # If $ACCESS_CONFIG already exists, copy it to backup then clear it. + # Note we can't do "mv" here as do_install_server.sh may already be tailing + # this file. + log_for_sentry "Initializing ACCESS_CONFIG" + [[ -f $ACCESS_CONFIG ]] && cp $ACCESS_CONFIG $ACCESS_CONFIG.bak && > $ACCESS_CONFIG + + # Set watchtower to refresh every 30 seconds if a custom SB_IMAGE is used (for + # testing). Otherwise refresh every hour. + readonly WATCHTOWER_REFRESH_SECONDS=$([ $SB_IMAGE ] && echo 30 || echo 3600) + + # Make a directory for persistent state + log_step "Creating persistent state dir" + readonly STATE_DIR="$SHADOWBOX_DIR/persisted-state" + mkdir -p "${STATE_DIR}" + + log_step "Setting up directory" + + # Generate a secret key for access to the shadowbox API and store it in a tag. + # 16 bytes = 128 bits of entropy should be plenty for this use. + function safe_base64() { + # Implements URL-safe base64 of stdin, stripping trailing = chars. + # Writes result to stdout. + # TODO: this gives the following errors on Mac: + # base64: invalid option -- w + # tr: illegal option -- - + local url_safe="$(base64 -w 0 - | tr '/+' '_-')" + echo -n "${url_safe%%=*}" # Strip trailing = chars + } + readonly SB_API_PREFIX=$(head -c 16 /dev/urandom | safe_base64) + + log_step "Generating secret key" + + # Generate self-signed cert and store it in the persistent state directory. + readonly CERTIFICATE_NAME="${STATE_DIR}/shadowbox-selfsigned" + readonly SB_CERTIFICATE_FILE="${CERTIFICATE_NAME}.crt" + readonly SB_PRIVATE_KEY_FILE="${CERTIFICATE_NAME}.key" + declare -a openssl_req_flags=( + -x509 -nodes -days 36500 -newkey rsa:2048 + -subj "/CN=${SB_PUBLIC_IP}" + -keyout "${SB_PRIVATE_KEY_FILE}" -out "${SB_CERTIFICATE_FILE}" + ) + openssl req "${openssl_req_flags[@]}" >/dev/null 2>&1 + + log_step "Generating self-signed certificate" + + # Add a tag with the SHA-256 fingerprint of the certificate. + # (Electron uses SHA-256 fingerprints: https://github.com/electron/electron/blob/9624bc140353b3771bd07c55371f6db65fd1b67e/atom/common/native_mate_converters/net_converter.cc#L60) + # Example format: "SHA256 Fingerprint=BD:DB:C9:A4:39:5C:B3:4E:6E:CF:18:43:61:9F:07:A2:09:07:37:35:63:67" + CERT_OPENSSL_FINGERPRINT=$(openssl x509 -in "${SB_CERTIFICATE_FILE}" -noout -sha256 -fingerprint) + # Example format: "BDDBC9A4395CB34E6ECF1843619F07A2090737356367" + CERT_HEX_FINGERPRINT=$(echo ${CERT_OPENSSL_FINGERPRINT#*=} | tr --delete :) + output_config "certSha256:$CERT_HEX_FINGERPRINT" + + log_step "Generating SHA-256 fingerprint" + + # Start Shadowbox. + declare -a docker_shadowbox_flags=( + --name shadowbox --restart=always --net=host + -v "${STATE_DIR}:${STATE_DIR}" + -e "SB_STATE_DIR=${STATE_DIR}" + -e "SB_PUBLIC_IP=${SB_PUBLIC_IP}" + -e "SB_API_PORT=${SB_API_PORT}" + -e "SB_API_PREFIX=${SB_API_PREFIX}" + -e "SB_CERTIFICATE_FILE=${SB_CERTIFICATE_FILE}" + -e "SB_PRIVATE_KEY_FILE=${SB_PRIVATE_KEY_FILE}" + -e "SB_METRICS_URL=${SB_METRICS_URL:-}" + -e "SB_DEFAULT_SERVER_NAME=${SB_DEFAULT_SERVER_NAME:-}" + ) + docker run -d "${docker_shadowbox_flags[@]}" "${SB_IMAGE}" >/dev/null + + log_step "Starting Shadowbox" + + # TODO(dborkan): if the script fails after docker run, it will continue to fail + # as the names shadowbox and watchtower will already be in use. Consider + # deleting the container in the case of failure (e.g. using a trap, or + # deleting existing containers on each run). + + # Start watchtower to automatically fetch docker image updates. + # TODO(fortuna): Don't wait for Shadowbox to run this. + declare -a docker_watchtower_flags=(--name watchtower --restart=always) + docker_watchtower_flags+=(-v /var/run/docker.sock:/var/run/docker.sock) + docker run -d "${docker_watchtower_flags[@]}" v2tec/watchtower --cleanup --tlsverify --interval $WATCHTOWER_REFRESH_SECONDS >/dev/null + + log_step "Starting Watchtower" + + readonly SB_API_URL="https://${SB_PUBLIC_IP}:${SB_API_PORT}/${SB_API_PREFIX}" + # Wait for server to be ready + until curl --cacert "${SB_CERTIFICATE_FILE}" -s "${SB_API_URL}/access-keys" >/dev/null; do sleep 1; done + # Create a new user + curl --cacert "${SB_CERTIFICATE_FILE}" -X POST -s "${SB_API_URL}/access-keys" >/dev/null + + log_step "Creating first user" + + # API is ready. Output the config. + output_config "apiUrl:${SB_API_URL}" + + log_step "Generating final output" + + # TODO: Figure out how to perform more firewall tests. Unfortunately making a + # request to the manager from within the host machine may not detect + # a firewall, even if the public IP address is used. + if command_exists ufw && [[ $(ufw status) != "Status: inactive" ]]; then + log_error "You have ufw enabled on your machine, please check your configuration to ensure access to high numbered ports." + else + log_step "Cursory firewalls detection" + fi + + # Echos the value of the specified field from ACCESS_CONFIG. + # e.g. if ACCESS_CONFIG contains the line "certSha256:1234", + # calling $(get_field_value certSha256) will echo 1234. + function get_field_value { + grep "$1" $ACCESS_CONFIG | sed "s/$1://" + } + + # Output JSON. This relies on apiUrl and certSha256 (hex characters) requiring + # no string escaping. TODO: look for a way to generate JSON that doesn't + # require new dependencies. + cat <; + + // List the access keys for this server, including the admin. + listAccessKeys(): Promise; + + // Returns stats for bytes transferred across all access keys of this server. + getDataUsage(): Promise; + + // Adds a new access key to this server. + addAccessKey(): Promise; + + // Renames the access key given by id. + renameAccessKey(accessKeyId: AccessKeyId, name: string): Promise; + + // Removes the access key given by id. + removeAccessKey(accessKeyId: AccessKeyId): Promise; + + // Returns whether metrics are enabled. + getMetricsEnabled(): boolean; + + // Updates whether metrics are enabled. + setMetricsEnabled(metricsEnabled: boolean): Promise; + + // Get the server's unique ID, used for metrics reporting. + getServerId(): string; + + // Checks if the server is healthy. + isHealthy(): Promise; + + // Gets the date when this server was created. + getCreatedDate(): Date; +} + +// Manual servers are servers which the user has independently setup to run +// shadowbox, and can be on any cloud provider. +export interface ManualServer extends Server { forget(): void; } + +// Managed servers are servers created by the Outline Manager through our +// "magic" user experience, e.g. DigitalOcean. +export interface ManagedServer extends Server { + // Returns a promise that fulfills once installation is complete. + // If resetTimeout is true, this will reset the server state and might + // wait until the timeout occurs to reconnect to the server. + waitOnInstall(resetTimeout: boolean): Promise; + // Returns server host object. + getHost(): ManagedServerHost; + // Returns true when installation is complete. + isInstallCompleted(): boolean; +} + +// The managed machine where the Outline Server is running. +export interface ManagedServerHost { + // Returns the monthly transfer limit. + getMonthlyTransferLimit(): DataAmount; + // Returns the monthly cost. + getMonthlyCost(): MonetaryCost; + // Returns the server region. + getRegionId(): RegionId; + // Deletes the server - cannot be undone. + delete(): Promise; +} + +export class DataAmount { terabytes: number; } + +export class MonetaryCost { + // Value in US dollars. + usd: number; +} + +export type RegionId = string; + +// Keys are cityIds like "nyc". Values are regions like ["nyc1", "nyc3"]. +export type RegionMap = { + [cityId: string]: RegionId[] +}; + +// Repository of ManagedServer objects. These servers are created by the server +// manager on cloud providers where we can provide a "magical" user experience, +// e.g. DigitalOcean. +export interface ManagedServerRepository { + // Lists all existing Shadowboxes. + listServers(): Promise; + // Return a map of regions that are available and support our target machine size. + getRegionMap(): Promise>; + // Creates a server and returning it when it becomes active (i.e. the server has + // created, not necessarily once shadowbox installation has finished). + createServer(region: RegionId): Promise; +} + +// Configuration for manual servers. This is the output emitted from the +// shadowbox install script, which is needed for the manager connect to +// shadowbox. +export interface ManualServerConfig { + apiUrl: string; + certSha256: string; +} + +// Repository of ManualServer objects. These are servers the user has setup +// themselves, and configured to run shadowbox, outside of the manager. +export interface ManualServerRepository { + // Lists all existing Shadowboxes. + listServers(): Promise; + // Adds a manual server using the config (e.g. user input). + addServer(config: ManualServerConfig): Promise; +} + +export type AccessKeyId = string; + +export interface AccessKey { + id: AccessKeyId; + name: string; + accessUrl: string; +} + +// Byte transfer stats for the past 30 days, including both inbound and outbound. +// TODO: this is copied at src/shadowbox/model/metrics.ts. Both copies should +// be kept in sync, until we can find a way to share code between the web_app +// and shadowbox. +export interface DataUsageByAccessKey { + // The accessKeyId should be of type AccessKeyId, however that results in the tsc + // error TS1023: An index signature parameter type must be 'string' or 'number'. + // See https://github.com/Microsoft/TypeScript/issues/2491 + // TODO: this still says "UserId", changing to "AccessKeyId" will require + // a change on the shadowbox server. + bytesTransferredByUserId: {[accessKeyId: string]: number}; +} diff --git a/src/server_manager/package.json b/src/server_manager/package.json new file mode 100644 index 000000000..b509e699e --- /dev/null +++ b/src/server_manager/package.json @@ -0,0 +1,30 @@ +{ + "name": "outline-manager", + "productName": "Outline Manager", + "version": "1.0.0", + "description": "Create and manage access to Outline servers", + "homepage": "https://getoutline.org/", + "author": { + "name": "The Outline authors", + "email": "info@getoutline.org" + }, + "dependencies": { + "bytes": "^3.0.0", + "clipboard-polyfill": "^2.4.6", + "eventemitter3": "^2.0.3", + "node-forge": "^0.7.1", + "raven-js": "^3.17.0", + "request-lite": "^2.40.1", + "semver": "^5.4.1" + }, + "devDependencies": { + "@types/bytes": "^2.5.1", + "@types/node-forge": "^0.6.9", + "@types/semver": "^5.4.0", + "bower": "^1.8.0", + "browserify": "^14.5.0", + "electron": "^1.7.9", + "electron-builder": "^20.4.1", + "electron-icon-maker": "^0.0.4" + } +} diff --git a/src/server_manager/types/node-forge.d.ts b/src/server_manager/types/node-forge.d.ts new file mode 100644 index 000000000..bf13eba2b --- /dev/null +++ b/src/server_manager/types/node-forge.d.ts @@ -0,0 +1,20 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Extension to @types/node-forge to add the missing definitions that we need. +declare module 'node-forge' { + namespace ssh { + function publicKeyToOpenSSH(privateKey?: string, passphrase?: string): string; + } +} diff --git a/src/server_manager/types/request-lite.d.ts b/src/server_manager/types/request-lite.d.ts new file mode 100644 index 000000000..ae3ad5e79 --- /dev/null +++ b/src/server_manager/types/request-lite.d.ts @@ -0,0 +1,24 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// TODO(dborkan): request-lite should be the same type as defined in the +// @types/request module. We should re-use those typings if possible, or just +// use another lightweight request module with it's own @type supplied. +declare module 'request-lite' { + // tslint:disable-next-line:no-any + function request( + url: string, callback: (error: Error, repsonse: any, body: string) => void): void; + namespace request {} + export = request; +} diff --git a/src/server_manager/ui_components/app-root.html b/src/server_manager/ui_components/app-root.html new file mode 100644 index 000000000..7c680c22b --- /dev/null +++ b/src/server_manager/ui_components/app-root.html @@ -0,0 +1,307 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/server_manager/ui_components/cities.html b/src/server_manager/ui_components/cities.html new file mode 100644 index 000000000..4d696e762 --- /dev/null +++ b/src/server_manager/ui_components/cities.html @@ -0,0 +1,28 @@ + + \ No newline at end of file diff --git a/src/server_manager/ui_components/cloud-install-styles.html b/src/server_manager/ui_components/cloud-install-styles.html new file mode 100644 index 000000000..cd44a4cd8 --- /dev/null +++ b/src/server_manager/ui_components/cloud-install-styles.html @@ -0,0 +1,212 @@ + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/ui_components/licenses/README.md b/src/server_manager/ui_components/licenses/README.md new file mode 100644 index 000000000..0c611886d --- /dev/null +++ b/src/server_manager/ui_components/licenses/README.md @@ -0,0 +1,25 @@ +# HOWTO re-generate `license.txt` + +## Requirements + +* `yarn` +* `https://github.com/Jigsaw-Code/bower-disclaimer` + +## Steps + +* `cd` to the root of your clone of this repo +* Ensure `bower_components` and `node_modules` are up to date and only include dependencies of the Electron app by running `yarn run clean && yarn && yarn do yarn do server_manager/web_app/build` +* `cd src/server_manager` +* `yarn licenses generate-disclaimer --prod > /tmp/yarn` +* `node /build > /tmp/bower` +* `cat /tmp/{yarn,bower} > ui_components/licenses/licenses.txt` + +Done! + +## Check + +To quickly test for non-compliant licenses: + +```bash +yarn licenses list --prod|grep License:|sort -u +``` diff --git a/src/server_manager/ui_components/licenses/licenses.txt b/src/server_manager/ui_components/licenses/licenses.txt new file mode 100644 index 000000000..94da80e77 --- /dev/null +++ b/src/server_manager/ui_components/licenses/licenses.txt @@ -0,0 +1,2312 @@ +THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF THE OUTLINE MANAGER PRODUCT. + +----- + +The following software may be included in this product: bytes. A copy of the source code may be downloaded from https://github.com/visionmedia/bytes.js.git. This software contains the following license and notice below: + +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----- + +The following software may be included in this product: clipboard-polyfill. A copy of the source code may be downloaded from https://github.com/lgarron/clipboard-polyfill. This software contains the following license and notice below: + +# License + +The MIT License (MIT) + +Copyright (c) 2014 Lucas Garron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----- + +The following software may be included in this product: es6-promise. A copy of the source code may be downloaded from git://github.com/stefanpenner/es6-promise.git. This software contains the following license and notice below: + +Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----- + +The following software may be included in this product: eventemitter3. A copy of the source code may be downloaded from git://github.com/primus/eventemitter3.git. This software contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Arnout Kazemier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----- + +The following software may be included in this product: forever-agent, request-lite. A copy of the source code may be downloaded from https://github.com/mikeal/forever-agent (forever-agent), https://github.com/jmervine/request-lite.git (request-lite). This software contains the following license and notice below: + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +----- + +The following software may be included in this product: json-stringify-safe, semver. A copy of the source code may be downloaded from git://github.com/isaacs/json-stringify-safe (json-stringify-safe), https://github.com/npm/node-semver (semver). This software contains the following license and notice below: + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +----- + +The following software may be included in this product: mime-types. A copy of the source code may be downloaded from https://github.com/expressjs/mime-types.git. This software contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----- + +The following software may be included in this product: node-forge. A copy of the source code may be downloaded from https://github.com/digitalbazaar/forge. This software contains the following license and notice below: + +You may use the Forge project under the terms of either the BSD License or the +GNU General Public License (GPL) Version 2. + +The BSD License is recommended for most projects. It is simple and easy to +understand and it places almost no restrictions on what you can do with the +Forge project. + +If the GPL suits your project better you are also free to use Forge under +that license. + +You don't have to do anything special to choose one license or the other and +you don't have to notify anyone which license you are using. You are free to +use this project in commercial projects as long as the copyright header is +left intact. + +If you are a commercial entity and use this set of libraries in your +commercial software then reasonable payment to Digital Bazaar, if you can +afford it, is not required but is expected and would be appreciated. If this +library saves you time, then it's saving you money. The cost of developing +the Forge software was on the order of several hundred hours and tens of +thousands of dollars. We are attempting to strike a balance between helping +the development community while not being taken advantage of by lucrative +commercial entities for our efforts. + +------------------------------------------------------------------------------- +New BSD License (3-clause) +Copyright (c) 2010, Digital Bazaar, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Digital Bazaar, Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------- + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +----- + +The following software may be included in this product: node-uuid. A copy of the source code may be downloaded from https://github.com/broofa/node-uuid.git. This software contains the following license and notice below: + +The MIT License (MIT) + +Copyright (c) 2010-2012 Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----- + +The following software may be included in this product: qs. A copy of the source code may be downloaded from https://github.com/hapijs/qs.git. This software contains the following license and notice below: + +Copyright (c) 2014 Nathan LaFreniere and other contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of any contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * * * + +The complete list of contributors can be found at: https://github.com/hapijs/qs/graphs/contributors + +----- + +The following software may be included in this product: raven-js. A copy of the source code may be downloaded from git://github.com/getsentry/raven-js.git. This software contains the following license and notice below: + +Copyright (c) 2014 Matt Robenolt and other contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The following software may be included in this product: app-layout. A copy of the source code may be downloaded from https://github.com/PolymerElements/app-layout. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-a11y-announcer. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-a11y-announcer.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: font-roboto. A copy of the source code may be downloaded from https://github.com/PolymerElements/font-roboto/. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-a11y-keys-behavior. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-a11y-keys-behavior.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-autogrow-textarea. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-autogrow-textarea. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-behaviors. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-behaviors.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-checked-element-behavior. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-checked-element-behavior. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-fit-behavior. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-fit-behavior.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-dropdown. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-dropdown. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-flex-layout. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-flex-layout.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-form-element-behavior. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-form-element-behavior.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-icon. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-icon.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-icons. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-icons. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-iconset-svg. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-iconset-svg.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-input. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-input. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-menu-behavior. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-menu-behavior. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-media-query. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-media-query. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-meta. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-meta.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-overlay-behavior. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-overlay-behavior.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-pages. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-pages.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-range-behavior. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-range-behavior.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-resizable-behavior. A copy of the source code may be downloaded from git://github.com/PolymerElements/iron-resizable-behavior.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-scroll-target-behavior. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-scroll-target-behavior. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-selector. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-selector. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: iron-validatable-behavior. A copy of the source code may be downloaded from https://github.com/PolymerElements/iron-validatable-behavior. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: neon-animation. A copy of the source code may be downloaded from https://github.com/PolymerElements/neon-animation. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-behaviors. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-behaviors. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-button. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-button. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-dialog. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-dialog. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-dialog-behavior. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-dialog-behavior. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-dropdown-menu. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-dropdown-menu. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-dialog-scrollable. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-dialog-scrollable. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-icon-button. A copy of the source code may be downloaded from git://github.com/PolymerElements/paper-icon-button.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-input. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-input. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-item. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-item. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-listbox. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-listbox. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-menu-button. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-menu-button. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-progress. A copy of the source code may be downloaded from git://github.com/PolymerElements/paper-progress.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-ripple. A copy of the source code may be downloaded from git://github.com/PolymerElements/paper-ripple.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-styles. A copy of the source code may be downloaded from https://github.com/polymerelements/paper-styles/. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-toast. A copy of the source code may be downloaded from git://github.com/PolymerElements/paper-toast.git. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: paper-toggle-button. A copy of the source code may be downloaded from https://github.com/PolymerElements/paper-toggle-button. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: polymer. A copy of the source code may be downloaded from undefined. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: shadycss. A copy of the source code may be downloaded from https://webcomponents.org/polyfills. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: webcomponentsjs. A copy of the source code may be downloaded from http://webcomponents.org. This software contains the following license and notice below: + +// Copyright (c) 2014 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +----- + +The following software may be included in this product: web-animations-js. A copy of the source code may be downloaded from https://github.com/web-animations/web-animations-js. This software contains the following license and notice below: + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/src/server_manager/ui_components/outline-about-dialog.html b/src/server_manager/ui_components/outline-about-dialog.html new file mode 100644 index 000000000..22f61a1b2 --- /dev/null +++ b/src/server_manager/ui_components/outline-about-dialog.html @@ -0,0 +1,90 @@ + + + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-feedback-dialog.html b/src/server_manager/ui_components/outline-feedback-dialog.html new file mode 100644 index 000000000..b4d93dd49 --- /dev/null +++ b/src/server_manager/ui_components/outline-feedback-dialog.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-help-bubble.html b/src/server_manager/ui_components/outline-help-bubble.html new file mode 100644 index 000000000..79ddd7355 --- /dev/null +++ b/src/server_manager/ui_components/outline-help-bubble.html @@ -0,0 +1,146 @@ + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-intro-step.html b/src/server_manager/ui_components/outline-intro-step.html new file mode 100644 index 000000000..5d237cc4f --- /dev/null +++ b/src/server_manager/ui_components/outline-intro-step.html @@ -0,0 +1,189 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/ui_components/outline-manual-server-entry.html b/src/server_manager/ui_components/outline-manual-server-entry.html new file mode 100644 index 000000000..34fcc8bfd --- /dev/null +++ b/src/server_manager/ui_components/outline-manual-server-entry.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-metrics-option-dialog.html b/src/server_manager/ui_components/outline-metrics-option-dialog.html new file mode 100644 index 000000000..782f55a24 --- /dev/null +++ b/src/server_manager/ui_components/outline-metrics-option-dialog.html @@ -0,0 +1,49 @@ + + + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-modal-dialog.html b/src/server_manager/ui_components/outline-modal-dialog.html new file mode 100644 index 000000000..6d76e5d04 --- /dev/null +++ b/src/server_manager/ui_components/outline-modal-dialog.html @@ -0,0 +1,71 @@ + + + + + + + + diff --git a/src/server_manager/ui_components/outline-progress-spinner.html b/src/server_manager/ui_components/outline-progress-spinner.html new file mode 100644 index 000000000..0487d6da8 --- /dev/null +++ b/src/server_manager/ui_components/outline-progress-spinner.html @@ -0,0 +1,82 @@ + + + + + + + + diff --git a/src/server_manager/ui_components/outline-region-picker-step.html b/src/server_manager/ui_components/outline-region-picker-step.html new file mode 100644 index 000000000..e6ee8e344 --- /dev/null +++ b/src/server_manager/ui_components/outline-region-picker-step.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server_manager/ui_components/outline-server-creator.html b/src/server_manager/ui_components/outline-server-creator.html new file mode 100644 index 000000000..abcc566bd --- /dev/null +++ b/src/server_manager/ui_components/outline-server-creator.html @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-server-progress-step.html b/src/server_manager/ui_components/outline-server-progress-step.html new file mode 100644 index 000000000..4bb252030 --- /dev/null +++ b/src/server_manager/ui_components/outline-server-progress-step.html @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-server-settings.html b/src/server_manager/ui_components/outline-server-settings.html new file mode 100644 index 000000000..337456a88 --- /dev/null +++ b/src/server_manager/ui_components/outline-server-settings.html @@ -0,0 +1,188 @@ + + + + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-server-view.html b/src/server_manager/ui_components/outline-server-view.html new file mode 100644 index 000000000..e2b10221d --- /dev/null +++ b/src/server_manager/ui_components/outline-server-view.html @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/server_manager/ui_components/outline-share-dialog.html b/src/server_manager/ui_components/outline-share-dialog.html new file mode 100644 index 000000000..4dc735736 --- /dev/null +++ b/src/server_manager/ui_components/outline-share-dialog.html @@ -0,0 +1,143 @@ + + + + + + + + diff --git a/src/server_manager/ui_components/shadowsocks-clients.html b/src/server_manager/ui_components/shadowsocks-clients.html new file mode 100644 index 000000000..28e5099b5 --- /dev/null +++ b/src/server_manager/ui_components/shadowsocks-clients.html @@ -0,0 +1,34 @@ + + + diff --git a/src/server_manager/ui_components/style.css b/src/server_manager/ui_components/style.css new file mode 100644 index 000000000..eff423c23 --- /dev/null +++ b/src/server_manager/ui_components/style.css @@ -0,0 +1,142 @@ +/* + * Copyright 2018 The Outline Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Copy of style.css with changes for new Cloud Installation Page */ + +/* If Polymer fails to load, this prevents the styles in elements + from being erroneously displayed to the user. */ +core-style { display: none; } + +paper-dropdown { + box-shadow: 0px 0px 20px #999999; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +body { + font-family: "Roboto", sans-serif; + font-size: 14px; + font-weight: 400; + color:rgba(0,0,0,0.54); + background-color: #FAFAFA; +} + +div#wrapper { + min-height: 100%; + display: flex; + display: -webkit-flex; + flex-direction: column; + -webkit-flex-direction: column; +} + +section:not(#header):not(:last-of-type) { + border-bottom: 1px solid #eee; + padding: 100px 40px; +} + +@media screen and (max-width: 630px) { + section:not(#header):not(:last-of-type) { + padding: 70px 40px; + } +} + +section:last-of-type { + border-bottom: none; + padding: 100px 40px 0; +} + +/* headings */ + + +h1, h2, h3, h4 { + color: rgba(0,0,0,0.87); + line-height: 1.5em; +} + +h1 { + font-size: 40px; + padding-top: 12px; + margin-bottom: 17px; +} + +h2 { + font-weight: 500; + font-size: 16px; +} + +h3 { + font-weight: 400; + line-height: 28px; + font-size: 16px; + margin: 0px 0px 14px 0px; + padding: 14px 12% 32px 12%; +} + +h3 span{ + font-weight: 500; +} + +h4 { + font-size: 14px; + font-weight:500; +} + +/* normal text and links */ +p { + color: rgba(0,0,0,0.54); + font-size: 14px; + line-height: 1.5em; +} + +a { + text-decoration: none; +} + +a:hover { + color: #00AC9B; +} + +p a { + color: #808080; +} + +/* pre */ +pre { + font-family: monospace; +} + +/* "logo" stuff */ + +/* header */ +#header { + padding: 15px 0 8px 20px; + background-color: #263238; + text-align: left; + height: 133px; +} + +#header img { + height: 48px; +} + +paper-menu { + white-space: nowrap; +} diff --git a/src/server_manager/web_app/app.spec.ts b/src/server_manager/web_app/app.spec.ts new file mode 100644 index 000000000..64da5bb9c --- /dev/null +++ b/src/server_manager/web_app/app.spec.ts @@ -0,0 +1,318 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as digitalocean_api from '../cloud/digitalocean_api'; +import * as server from '../model/server'; + +import {App} from './app'; +import {TokenManager} from './digitalocean_oauth'; + +const TOKEN_WITH_NO_SERVERS = 'no-server-token'; +const TOKEN_WITH_ONE_SERVER = 'one-server-token'; + +describe('App', () => { + it('Shows intro when starting with no manual servers or DigitalOcean token', (done) => { + const polymerAppRoot = new FakePolymerAppRoot(); + const app = createTestApp( + polymerAppRoot, new InMemoryDigitalOceanTokenManager()); + app.start().then(() => { + expect(polymerAppRoot.currentScreen).toEqual(AppRootScreen.INTRO); + done(); + }); + }); + + it('Shows region picker when no servers exist but a DigitalOcean token is available', (done) => { + const polymerAppRoot = new FakePolymerAppRoot(); + const tokenManager = new InMemoryDigitalOceanTokenManager(); + tokenManager.token = TOKEN_WITH_NO_SERVERS; + const app = createTestApp(polymerAppRoot, tokenManager); + app.start().then(() => { + expect(polymerAppRoot.currentScreen).toEqual(AppRootScreen.REGION_PICKER); + done(); + }); + }); + + it('Will not create a manual server with invalid input', (done) => { + // Create a new app with no existing servers or DigitalOcean token. + const polymerAppRoot = new FakePolymerAppRoot(); + const app = createTestApp(polymerAppRoot, new InMemoryDigitalOceanTokenManager()); + app.start().then(() => { + app.createManualServer('bad input').catch(done); + }); + }); + + it('Creates a manual server with valid input', (done) => { + // Create a new app with no existing servers or DigitalOcean token. + const polymerAppRoot = new FakePolymerAppRoot(); + const app = createTestApp(polymerAppRoot, new InMemoryDigitalOceanTokenManager()); + app.start().then(() => { + app.createManualServer(JSON.stringify({certSha256: 'cert', apiUrl: 'url'})).then(() => { + expect(polymerAppRoot.currentScreen).toEqual(AppRootScreen.SERVER_VIEW); + done(); + }); + }); + }); + + it('App initially shows already created manual servers', (done) => { + // Create a fake manual server before creating the app. + const manualServerRepo = new FakeManualServerRepository(); + const serverConfig = {certSha256: 'cert', apiUrl: 'url'}; + manualServerRepo.addServer(serverConfig).then((manualServer) => { + const polymerAppRoot = new FakePolymerAppRoot(); + const app = + createTestApp(polymerAppRoot, new InMemoryDigitalOceanTokenManager(), manualServerRepo); + app.start().then(() => { + expect(polymerAppRoot.currentScreen).toEqual(AppRootScreen.SERVER_VIEW); + expect(polymerAppRoot.serverView.serverId).toEqual(manualServer.getServerId()); + done(); + }); + }); + }); + + it('Shows progress screen once DigitalOcean droplets are created', (done) => { + // Start the app with a fake DigitalOcean token. + const polymerAppRoot = new FakePolymerAppRoot(); + const tokenManager = new InMemoryDigitalOceanTokenManager(); + tokenManager.token = TOKEN_WITH_NO_SERVERS; + const app = createTestApp(polymerAppRoot, tokenManager); + app.start().then(() => { + app.createDigitalOceanServer('fake2').then(() => { + expect(polymerAppRoot.currentScreen).toEqual(AppRootScreen.INSTALL_PROGRESS); + done(); + }); + }); + }); + + it('Shows progress screen when starting with DigitalOcean servers still being created', (done) => { + // Start the app with a fake DigitalOcean token. + const polymerAppRoot = new FakePolymerAppRoot(); + const tokenManager = new InMemoryDigitalOceanTokenManager(); + tokenManager.token = TOKEN_WITH_ONE_SERVER; + const app = createTestApp(polymerAppRoot, tokenManager); + app.start().then(() => { + // Servers should initially show the progress screen, until their + // "waitOnInstall" promise fulfills. For DigitalOcean, server objects + // are returned by the repository as soon as the droplet exists with the + // "shadowbox" tag, however shadowbox installation may not yet be complete. + // This is needed in case the user restarts the manager after the droplet + // is created but before shadowbox installation finishes. + expect(polymerAppRoot.currentScreen).toEqual(AppRootScreen.INSTALL_PROGRESS); + done(); + }); + }); +}); + +function createTestApp( + polymerAppRoot: FakePolymerAppRoot, digitalOceanTokenManager: InMemoryDigitalOceanTokenManager, + manualServerRepo?: server.ManualServerRepository) { + const WEB_APP_URL = 'outline://fakefakefake/'; + const VERSION = '0.0.1'; + const fakeDigitalOceanSessionFactory = (accessToken: string) => { + return new FakeDigitalOceanSession(accessToken); + }; + const fakeDigitalOceanServerRepositoryFactory = (session: digitalocean_api.DigitalOceanSession) => { + const repo = new FakeManagedServerRepository(); + if (session.accessToken === TOKEN_WITH_ONE_SERVER) { + repo.createServer(); // OK to ignore promise as the fake implementation is synchronous. + } + return repo; + }; + if (!manualServerRepo) { + manualServerRepo = new FakeManualServerRepository(); + } + return new App( + polymerAppRoot, WEB_APP_URL, VERSION, fakeDigitalOceanSessionFactory, + fakeDigitalOceanServerRepositoryFactory, manualServerRepo, digitalOceanTokenManager); +} + +enum AppRootScreen { + NONE = 0, + INTRO, + REGION_PICKER, + SERVER_VIEW, + INSTALL_PROGRESS +} + +// TODO: define the AppRoot type. Currently app.ts just defines the Polymer +// type as HTMLElement&any. +class FakePolymerAppRoot { + currentScreen = AppRootScreen.NONE; + serverView = {setServerTransferredData: () => {}, serverId: ''}; + + getAndShowServerCreator() { + return { + showIntro: () => { + this.currentScreen = AppRootScreen.INTRO; + }, + getAndShowRegionPicker: () => { + this.currentScreen = AppRootScreen.REGION_PICKER; + return {}; + }, + showProgress: () => { + this.currentScreen = AppRootScreen.INSTALL_PROGRESS; + } + }; + } + + getAndShowServerView() { + this.currentScreen = AppRootScreen.SERVER_VIEW; + return this.serverView; + } + + // Methods like setAttribute, addEventListener, and others are currently + // no-ops, since we are not yet testing this functionality. + // These don't return Promise.reject(..) as that would print error trace, + // and throwing an exception would result in breakage. + setAttribute() {} + addEventListener() {} +} + +class FakeServer implements server.Server { + private name = 'serverName'; + private metricsEnabled = false; + private id: string; + constructor() { + this.id = Math.random().toString(); + } + getName() { + return this.name; + } + setName(name: string) { + this.name = name; + return Promise.resolve(); + } + listAccessKeys() { + return Promise.resolve([]); + } + getMetricsEnabled() { + return this.metricsEnabled; + } + setMetricsEnabled(metricsEnabled: boolean) { + this.metricsEnabled = metricsEnabled; + return Promise.resolve(); + } + getServerId() { + return this.id; + } + isHealthy() { + return Promise.resolve(true); + } + getCreatedDate() { + return new Date(); + } + getDataUsage() { + return Promise.resolve({bytesTransferredByUserId: {}}); + } + addAccessKey() { + return Promise.reject('FakeServer.addAccessKey not implemented'); + } + renameAccessKey(accessKeyId: server.AccessKeyId, name: string) { + return Promise.reject('FakeServer.renameAccessKey not implemented'); + } + removeAccessKey(accessKeyId: server.AccessKeyId) { + return Promise.reject('FakeServer.removeAccessKey not implemented'); + } +} + +class FakeManualServer extends FakeServer implements server.ManualServer { + forget() { + return Promise.reject('FakeManualServer.forget not implemented'); + } +} + +class FakeManualServerRepository implements server.ManualServerRepository { + private servers: server.ManualServer[] = []; + + addServer(config: server.ManualServerConfig) { + const newServer = new FakeManualServer(); + this.servers.push(newServer); + return Promise.resolve(newServer); + } + + listServers() { + return Promise.resolve(this.servers); + } +} + +class InMemoryDigitalOceanTokenManager implements TokenManager { + public token: string; + extractTokenFromUrl(): string { + return this.token; + } + removeTokenFromStorage() { + this.token = null; + } + writeTokenToStorage(token: string) { + this.token = token; + } +} + +class FakeDigitalOceanSession implements digitalocean_api.DigitalOceanSession { + constructor(public accessToken: string) {} + + // Return fake account data. + getAccount() { + return Promise.resolve({email: 'fake', uuid: 'fake', email_verified: false, status: 'fake'}); + } + + // Return an empty list of droplets by default. + getDropletsByTag = (tag: string) => Promise.resolve([]); + + // Return an empty list of regions by default. + getRegionInfo = () => Promise.resolve([]); + + // Other methods do not yet need implementations for tests to pass. + createDroplet = + (displayName: string, region: string, publicKeyForSSH: string, + dropletSpec: digitalocean_api.DigitalOceanDropletSpecification) => + Promise.reject('createDroplet not implemented'); + deleteDroplet = (dropletId: number) => Promise.reject('deleteDroplet not implemented'); + getDroplet = (dropletId: number) => Promise.reject('getDroplet not implemented'); + getDropletTags = (dropletId: number) => Promise.reject('getDropletTags not implemented'); + getDroplets = () => Promise.reject('getDroplets not implemented'); +} + +class FakeManagedServer extends FakeServer implements server.ManagedServer { + waitOnInstall(resetTimeout: boolean) { + // Return a promise which does not yet fulfill, to simulate long + // shadowbox install time. + return new Promise((fulfill, reject) => {}); + } + getHost() { + return { + getMonthlyTransferLimit: () => ({terabytes: 1}), + getMonthlyCost: () => ({usd: 5}), + getRegionId: () => 'fake-region', + delete: () => Promise.resolve(), + }; + } + isInstallCompleted() { + return false; + } +} + +class FakeManagedServerRepository implements server.ManagedServerRepository { + private servers: server.ManagedServer[] = []; + listServers() { + return Promise.resolve(this.servers); + } + getRegionMap() { + return Promise.resolve({'fake': ['fake1', 'fake2']}); + } + createServer() { + const newServer = new FakeManagedServer(); + this.servers.push(newServer); + return Promise.resolve(newServer); + } +} diff --git a/src/server_manager/web_app/app.ts b/src/server_manager/web_app/app.ts new file mode 100644 index 000000000..d53ab5027 --- /dev/null +++ b/src/server_manager/web_app/app.ts @@ -0,0 +1,733 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as digitalocean_api from '../cloud/digitalocean_api'; +import * as errors from '../infrastructure/errors'; +import * as server from '../model/server'; + +import {getOauthUrl, TokenManager} from './digitalocean_oauth'; +import * as digitalocean_server from './digitalocean_server'; +import {SentryErrorReporter} from './error_reporter'; +import {ManualServerRepository} from './manual_server'; + +// tslint:disable-next-line:no-any +type Polymer = HTMLElement&any; + +interface PolymerEvent extends Event { + // tslint:disable-next-line:no-any + detail: any; +} + +// The Outline DigitalOcean team's referral code: +// https://www.digitalocean.com/help/referral-program/ +const DIGITALOCEAN_REFERRAL_CODE = '5ddb4219b716'; + +// This function is defined in electron_app/preload.ts. +declare function clearDigitalOceanCookies(): boolean; + +interface UiAccessKey { + id: string; + placeholderName: string; + name: string; + accessUrl: string; + transferredBytes: number; + relativeTraffic: number; +} + +// Converts the access key from the remote service format to the +// format used by outline-server-view. +function convertToUiAccessKey(remoteAccessKey: server.AccessKey): UiAccessKey { + return { + id: remoteAccessKey.id, + placeholderName: 'Key ' + remoteAccessKey.id, + name: remoteAccessKey.name, + accessUrl: remoteAccessKey.accessUrl, + transferredBytes: 0, + relativeTraffic: 0 + }; +} + +const DIGITAL_OCEAN_CREATION_ERROR_MESSAGE = `Sorry! We couldn't create a server this time. + If this problem persists, it might be that your account needs to be reviewed by DigitalOcean. + Please log in to www.digitalocean.com and follow their instructions.`; + +function isManagedServer(testServer: server.Server): testServer is server.ManagedServer { + return !!(testServer as server.ManagedServer).getHost; +} + +function isManualServer(testServer: server.Server): testServer is server.ManualServer { + return !!(testServer as server.ManualServer).forget; +} + +type DigitalOceanSessionFactory = (accessToken: string) => digitalocean_api.DigitalOceanSession; +type DigitalOceanServerRepositoryFactory = (session: digitalocean_api.DigitalOceanSession) => + server.ManagedServerRepository; + +export class App { + private digitalOceanRepository: server.ManagedServerRepository; + private selectedServer: server.Server; + private runningServer: server.Server; + + constructor( + private appRoot: Polymer, private readonly appUrl: string, private readonly version: string, + private createDigitalOceanSession: DigitalOceanSessionFactory, + private createDigitalOceanServerRepository: DigitalOceanServerRepositoryFactory, + private manualServerRepository: server.ManualServerRepository, + private digitalOceanTokenManager: TokenManager) { + appRoot.setAttribute('outline-version', this.version); + + appRoot.addEventListener('SignOutRequested', (event: PolymerEvent) => { + this.clearCredentialsAndShowIntro(); + }); + + appRoot.addEventListener('ClearDigitalOceanCookiesRequested', (event: PolymerEvent) => { + this.signOutFromDigitalocean(); + }); + + appRoot.addEventListener('SetUpServerRequested', (event: PolymerEvent) => { + this.createDigitalOceanServer(event.detail.regionId); + }); + + appRoot.addEventListener('DeleteServerRequested', (event: PolymerEvent) => { + this.deleteSelectedServer(); + }); + + appRoot.addEventListener('ForgetServerRequested', (event: PolymerEvent) => { + this.forgetSelectedServer(); + }); + + appRoot.addEventListener('AddAccessKeyRequested', (event: PolymerEvent) => { + this.addAccessKey(); + }); + + appRoot.addEventListener('RemoveAccessKeyRequested', (event: PolymerEvent) => { + this.removeAccessKey(event.detail.accessKeyId); + }); + + appRoot.addEventListener('RenameAccessKeyRequested', (event: PolymerEvent) => { + this.renameAccessKey(event.detail.accessKeyId, event.detail.newName, event.detail.entry); + }); + + appRoot.addEventListener('ManualServerEntered', (event: PolymerEvent) => { + const userInputConfig = event.detail.userInputConfig; + const manualServerEntryEl = appRoot.getServerCreator().getManualServerEntry(); + this.createManualServer(userInputConfig) + .then(() => { + // Clear fields on outline-manual-server-entry (e.g. dismiss the connecting popup). + manualServerEntryEl.clear(); + }) + .catch((e: Error) => { + // Remove the "Attempting to connect..." display. + manualServerEntryEl.showConnection = false; + // Display either error dialog or feedback depending on error type. + if (e instanceof errors.UnreachableServerError) { + manualServerEntryEl.showError(e.message); + } else { + appRoot.openManualInstallFeedback( + e.message + (userInputConfig ? `\n${userInputConfig}` : '')); + } + }); + }); + + appRoot.addEventListener('EnableMetricsRequested', (event: PolymerEvent) => { + this.setMetricsEnabled(true); + }); + + appRoot.addEventListener('DisableMetricsRequested', (event: PolymerEvent) => { + this.setMetricsEnabled(false); + }); + + appRoot.addEventListener('SubmitFeedback', (event: PolymerEvent) => { + const detail = event.detail; + try { + SentryErrorReporter.report(detail.userFeedback, detail.feedbackCategory, detail.userEmail); + appRoot.showNotification('Thanks for helping us improve! We love hearing from you.'); + } catch (e) { + appRoot.showError('Failed to submit feedback. Please try again.'); + } + }); + + appRoot.addEventListener('ServerRenameRequested', (event: PolymerEvent) => { + this.renameServer(event.detail.newName); + }); + + appRoot.addEventListener('CancelServerCreationRequested', (event: PolymerEvent) => { + this.cancelServerCreation(this.selectedServer); + }); + } + + // Returns a Promise that fulfills once the correct UI screen is shown. + start(): Promise { + // Load manual servers from storage. + return this.manualServerRepository.listServers().then((manualServers) => { + // Show any manual servers if they exist. + if (manualServers.length > 0) { + this.showManualServerIfHealthy(manualServers[0]); + return; + } + + // User has no manual servers - check if they are logged into DigitalOcean. + const accessToken = this.digitalOceanTokenManager.extractTokenFromUrl(); + if (accessToken) { + return this.getDigitalOceanServerList(accessToken) + .then((serverList) => { + // Check if this user already has a shadowsocks server, if so show that. + // This assumes we only allow one shadowsocks server per DigitalOcean user. + if (serverList.length > 0) { + this.showManagedServer(serverList[0]); + } else { + this.showCreateServer(); + } + }) + .catch((e) => { + const msg = 'could not fetch account details and/or server list'; + console.error(msg, e); + SentryErrorReporter.logError(msg); + this.showIntro(); + }); + } + + // User has no manual servers or DigitalOcean token. + this.showIntro(); + }); + } + + private showManualServerIfHealthy(manualServer: server.ManualServer) { + manualServer.isHealthy().then((isHealthy) => { + if (isHealthy) { + this.showServer(manualServer); + return; + } + + // Error reaching manual server, request that the user to choose between + // forgetting the server and trying again. + this.appRoot + .showModalDialog( + null, // Don't display any title. + 'We are unable to reach your Outline server. Please check that it is still running and accessible.', + ['Forget this server', 'Try again']) + .then((clickedButtonIndex: number) => { + if (clickedButtonIndex === 0) { // user clicked 'Forget this server' + manualServer.forget(); + this.displayNotification('Server forgotten'); + this.showIntro(); + return; + } else if (clickedButtonIndex === 1) { // user clicked 'Try again'. + this.showManualServerIfHealthy(manualServer); + return; + } + }); + }); + } + + // Returns a Promise that fulfills once the correct UI screen is shown. + private getDigitalOceanServerList(accessToken: string): Promise { + // Save accessToken to storage. DigitalOcean tokens + // expire after 30 days, unless they are manually revoked by the user. + // After 30 days the user will have to sign into DigitalOcean again. + // Note we cannot yet use DigitalOcean refresh tokens, as they require + // a client_secret to be stored on a server and not visible to end users + // in client-side JS. More details at: + // https://developers.digitalocean.com/documentation/oauth/#refresh-token-flow + this.digitalOceanTokenManager.writeTokenToStorage(accessToken); + + // Fetch the user's email address and list of servers then change to + // either the region picker or management screen, depending on whether + // they have a server. + const digitalOceanSession = this.createDigitalOceanSession(accessToken); + return this + .digitalOceanRetry(() => { + return digitalOceanSession.getAccount().then((account) => { + this.appRoot.adminEmail = account.email; + + this.digitalOceanRepository = + this.createDigitalOceanServerRepository(digitalOceanSession); + return this.digitalOceanRepository.listServers(); + }); + }); + } + + // Intended to add a "retry or re-authenticate?" prompt to DigitalOcean + // operations. Specifically, any operation rejecting with an digitalocean_api.XhrError will + // result in a dialog asking the user whether to retry the operation or + // re-authenticate against DigitalOcean. + // This is necessary because an access token may expire or be revoked at + // any time and there's no way to programmatically distinguish network errors + // from CORS-type errors (see the comments in DigitalOceanSession for more + // information). + // TODO: It would be great if, once the user has re-authenticated, we could + // return the UI to its exact prior state. Fortunately, the most likely + // time to discover an invalid access token is when the application + // starts. + private digitalOceanRetry = (f: () => Promise): + Promise => { + return f().catch((e) => { + if (!(e instanceof digitalocean_api.XhrError)) { + return Promise.reject(e); + } + + return new Promise((resolve, reject) => { + this.appRoot.showConnectivityDialog((retry: boolean) => { + if (retry) { + this.digitalOceanRetry(f).then(resolve, reject); + } else { + this.clearCredentialsAndShowIntro(); + reject(e); + } + }); + }); + }); + } + + private displayError(message: string, cause: Error) { + console.error(`${message}: ${cause}`); + this.appRoot.showError(message); + SentryErrorReporter.logError(message); + } + + private displayNotification(message: string) { + this.appRoot.showNotification(message); + } + + // Shows the intro screen with overview and options to sign in or sign up. + private showIntro() { + this.appRoot.getAndShowServerCreator().showIntro( + getOauthUrl(this.appUrl), DIGITALOCEAN_REFERRAL_CODE); + } + + // Clears the credentials and returns to the intro screen. + private clearCredentialsAndShowIntro() { + this.signOutFromDigitalocean(); + // Remove credential from URL and local storage. + location.hash = ''; + this.digitalOceanTokenManager.removeTokenFromStorage(); + // Reset UI + this.appRoot.adminEmail = ''; + this.showIntro(); + } + + private signOutFromDigitalocean() { + if (typeof clearDigitalOceanCookies === 'function') { + clearDigitalOceanCookies(); + } else { + // Running outside of Electron, use old iframe logic. + // We load the logout page on an iframe so that the browser clears the + // credential cookies properly. We can't get the credential cookies cleared + // with a XHR. + const iframe = document.createElement('iframe'); + iframe.src = 'https://cloud.digitalocean.com/logout'; + iframe.onload = () => { + const msg = 'Signed out from DigitalOcean'; + console.log(msg); + SentryErrorReporter.logInfo(msg); + iframe.remove(); + }; + iframe.onerror = () => { + const msg = 'DigitalOcean iframe error'; + console.error(msg); + SentryErrorReporter.logError(msg); + iframe.remove(); + }; + document.body.appendChild(iframe); + } + } + + // Opens the screen to create a server. + private showCreateServer() { + const regionPicker = this.appRoot.getAndShowServerCreator().getAndShowRegionPicker(); + // The region picker initially shows all options as disabled. Options are enabled + // by this code, after checking which regions are available. + this.digitalOceanRetry(() => { + return this.digitalOceanRepository.getRegionMap(); + }) + .then( + (map) => { + // Change from a list of regions per location to just one region per location. + // Where there are multiple working regions in one location, arbitrarily use the + // first. + const availableRegionIds: {[cityId: string]: server.RegionId} = {}; + for (const cityId in map) { + if (map[cityId].length > 0) { + availableRegionIds[cityId] = map[cityId][0]; + } + } + regionPicker.availableRegionIds = availableRegionIds; + }, + (e) => { + this.displayError('Failed to get list of available regions', e); + }); + } + + private showServerCreationProgress(managedServer: server.ManagedServer) { + // Set name to the default server name for this region. Because the server + // is still being created, the getName REST API will not yet be available. + const regionId = managedServer.getHost().getRegionId(); + const serverName = digitalocean_server.MakeEnglishNameForServer(regionId); + // Set selected server, needed for cancel button. + this.selectedServer = managedServer; + // Update UI. Only show cancel button if the server has not yet finished + // installation, to prevent accidental deletion when restarting. + const showCancelButton = !managedServer.isInstallCompleted(); + this.appRoot.getAndShowServerCreator().showProgress(serverName, showCancelButton); + } + + private showManagedServer(managedServer: server.ManagedServer, tryAgain = false): void { + // Show creation progress only after we have a ManagedServer object, + // otherwise the cancel action will not be available. + this.showServerCreationProgress(managedServer); + + managedServer.waitOnInstall(tryAgain) + .then(() => { + this.showServer(managedServer); + }) + .catch((e) => { + if (e instanceof errors.DeletedServerError) { + // The user deleted this server, no need to show an error or delete + // it again. + return; + } + if (managedServer.isInstallCompleted()) { + // This server has already been successfully installed, however + // we may have failed to reach it - this could be due to bad wifi, + // firewalls, etc. Notify the user and only delete if the user requests it. + this.appRoot.showModalDialog( + null, // Don't display any title. + 'We are unable to reach your Outline server at the moment.', + ['Delete this server', 'Try again']) + .then((clickedButtonIndex: number) => { + if (clickedButtonIndex === 0) { // user clicked 'Delete this server' + this.cancelServerCreation(managedServer); + } else if (clickedButtonIndex === 1) { // user clicked 'Try again'. + this.showManagedServer(managedServer, true); + } + }); + return; + } + // An error occured while installing this server. + // Show an error and cancel the server creation (e.g. delete the droplet). + this.handleServerCreationFailure('Got an error while waiting on server installation', e); + this.cancelServerCreation(managedServer); + }); + } + + // Returns a promise which fulfills once the DigitalOcean droplet is created. + // Shadowbox may not be fully installed once this promise is fulfilled. + public createDigitalOceanServer(regionId: server.RegionId) { + return this + .digitalOceanRetry(() => { + return this.digitalOceanRepository.createServer(regionId); + }) + .then((managedServer) => { + this.showManagedServer(managedServer); + }) + .catch((e) => { + // Don't show a dialog on the login screen. + if (!(e instanceof digitalocean_api.XhrError)) { + this.handleServerCreationFailure('Failed to create DigitalOcean server', e); + } + }); + } + + // Displays `DIGITAL_OCEAN_CREATION_ERROR_MESSAGE` in a dialog that prompts the user to submit + // feedback. Logs `msg` and `error` to the console and Sentry. + private handleServerCreationFailure(msg: string, error: Error) { + console.error(msg, error); + SentryErrorReporter.logError(msg); + this.appRoot + .showModalDialog( + 'Failed to create server', DIGITAL_OCEAN_CREATION_ERROR_MESSAGE, + ['Cancel', 'Submit Feedback']) + .then((clickedButtonIndex: number) => { + if (clickedButtonIndex === 1) { + const feedbackDialog = this.appRoot.$.feedbackDialog; + feedbackDialog.open(null, null, feedbackDialog.feedbackCategories.INSTALLATION); + } + this.showCreateServer(); // Reset UI. + }); + } + + // Show the server management screen. + private showServer(selectedServer: server.Server): void { + this.selectedServer = selectedServer; + this.runningServer = selectedServer; + + // Show view and initialize fields from selectedServer. + const view = this.appRoot.getAndShowServerView(); + view.serverName = selectedServer.getName(); + + if (isManagedServer(selectedServer)) { + const host = selectedServer.getHost(); + view.monthlyCost = host.getMonthlyCost().usd; + view.deleteEnabled = true; + view.forgetEnabled = false; + // Set monthly transfer byte limit for UI. For UI simplicity we are: + // 1. Showing 1 TB as 1000 GB (not 1024) + // 2. Dividing the total transfer limit by 2 to account for inbound and + // outbound connections, i.e. if I download a 10 GB file, it has to + // first be download from destination to the Outline server, then from + // the Outline server to my client, and costs me 20 GB against my quota, + // in this case it's simpler to say I used 10/500GB instead of 20/1000GB. + const monthlyTransferGb = host.getMonthlyTransferLimit().terabytes * 1000 / 2; + view.monthlyTransferBytes = monthlyTransferGb * (2 ** 30); + } else { + // TODO(dborkan): consider using dom-if with restamp property + // https://www.polymer-project.org/1.0/docs/api/elements/dom-if + // or using template-repeat. Then we won't have to worry about clearing + // the server-view when we display a new server. This should be fixed + // once we support multiple servers. + view.monthlyCost = undefined; + view.monthlyTransferBytes = undefined; + view.deleteEnabled = false; + view.forgetEnabled = true; + } + + view.metricsEnabled = selectedServer.getMetricsEnabled(); + this.showMetricsOptInWhenNeeded(selectedServer, view); + view.serverId = selectedServer.getServerId(); + + // Load "My Connection" and other access keys. + selectedServer.listAccessKeys() + .then((serverAccessKeys: server.AccessKey[]) => { + view.accessKeyRows = serverAccessKeys.map(convertToUiAccessKey); + }) + .catch((error) => { + this.displayError('Could not load keys', error); + }); + + this.showTransferStats(selectedServer, view); + } + + private showMetricsOptInWhenNeeded(runningServer: server.Server, serverView: Polymer) { + const showMetricsOptInOnce = () => { + // Sanity check to make sure the running server is still displayed, i.e. + // it hasn't been deleted. + if (this.runningServer !== runningServer) { + return; + } + // Show the metrics opt in prompt if the server has not already opted in, + // and if they haven't seen the prompt yet according to localStorage. + const storageKey = runningServer.getServerId() + '-prompted-for-metrics'; + if (!runningServer.getMetricsEnabled() && !localStorage.getItem(storageKey)) { + serverView.showMetricsDialogForNewServer(); + localStorage.setItem(storageKey, 'true'); + } + }; + + // Calculate milliseconds passed since server creation. + const createdDate = runningServer.getCreatedDate(); + const now = new Date(); + const msSinceCreation = now.getTime() - createdDate.getTime(); + + // Show metrics opt-in once ONE_DAY_IN_MS has passed since server creation. + const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000; + if (msSinceCreation >= ONE_DAY_IN_MS) { + showMetricsOptInOnce(); + } else { + setTimeout(showMetricsOptInOnce, ONE_DAY_IN_MS - msSinceCreation); + } + } + + private showTransferStats(runningServer: server.Server, serverView: Polymer) { + const refreshTransferStats = () => { + runningServer.getDataUsage().then((stats) => { + // Calculate total bytes transferred. + let totalBytes = 0; + // tslint:disable-next-line:forin + for (const accessKeyId in stats.bytesTransferredByUserId) { + totalBytes += stats.bytesTransferredByUserId[accessKeyId]; + } + serverView.setServerTransferredData(totalBytes); + // tslint:disable-next-line:forin + for (const accessKeyId in stats.bytesTransferredByUserId) { + const transferredBytes = stats.bytesTransferredByUserId[accessKeyId]; + const relativeTraffic = totalBytes ? 100 * transferredBytes / totalBytes : 0; + serverView.updateAccessKeyRow(accessKeyId, {transferredBytes, relativeTraffic}); + } + }); + }; + refreshTransferStats(); + + // Get transfer stats once per minute for as long as server is selected. + const statsRefreshRateMs = 60 * 1000; + const intervalId = setInterval(() => { + if (this.selectedServer !== runningServer) { + // Server is no longer running, stop interval + clearInterval(intervalId); + return; + } + refreshTransferStats(); + }, statsRefreshRateMs); + } + + private addAccessKey() { + this.runningServer.addAccessKey() + .then((serverAccessKey: server.AccessKey) => { + const uiAccessKey = convertToUiAccessKey(serverAccessKey); + this.appRoot.getServerView().addAccessKey(uiAccessKey); + this.displayNotification('Key added'); + }) + .catch((error) => { + this.displayError('Failed to add key', error); + }); + } + + private renameAccessKey(accessKeyId: string, newName: string, entry: Polymer) { + const server = this.runningServer; + server.renameAccessKey(accessKeyId, newName) + .then(() => { + entry.commitName(); + }) + .catch((error) => { + this.displayError('Failed to rename key', error); + entry.revertName(); + }); + } + + // Returns promise which fulfills when the server is created successfully, + // or rejects with an error message that can be displayed to the user. + public createManualServer(userInputConfig: string): Promise { + // Parse and validate user input. + let serverConfig: server.ManualServerConfig; + try { + // Remove anything before the first '{' and after the last '}', in case + // the user accidentally copied extra from the install script. + userInputConfig = userInputConfig.substr(userInputConfig.indexOf('{')); + userInputConfig = userInputConfig.substr(0, userInputConfig.lastIndexOf('}') + 1); + serverConfig = JSON.parse(userInputConfig); + } catch (e) { + const msg = 'Invalid server configuration: could not parse JSON.'; + SentryErrorReporter.logError(msg); + return Promise.reject(new Error(msg)); + } + if (!serverConfig.apiUrl) { + const msg = 'Invalid server configuration: apiUrl is missing.'; + SentryErrorReporter.logError(msg); + return Promise.reject(new Error(msg)); + } else if (!serverConfig.certSha256) { + const msg = 'Invalid server configuration: certSha256 is missing.'; + SentryErrorReporter.logError(msg); + return Promise.reject(new Error(msg)); + } + + return this.manualServerRepository.addServer(serverConfig).then((manualServer) => { + return manualServer.isHealthy().then((isHealthy) => { + if (isHealthy) { + this.showServer(manualServer); + return Promise.resolve(); + } else { + // Remove inaccessible manual server from local storage. + manualServer.forget(); + SentryErrorReporter.logError('Manual server installed but unreachable.'); + return Promise.reject(new errors.UnreachableServerError( + 'Your Outline Server was installed correctly, but we are not able to connect to it. Most likely this is because your server\'s firewall rules are blocking incoming connections. Please review them and make sure to allow incoming TCP connections on ports ranging from 1024 to 65535.')); + } + }); + }); + } + + private removeAccessKey(accessKeyId: string) { + this.runningServer.removeAccessKey(accessKeyId) + .then(() => { + this.appRoot.getServerView().removeAccessKey(accessKeyId); + this.displayNotification('Key removed'); + }) + .catch((error) => { + this.displayError('Failed to remove key', error); + }); + } + + private deleteSelectedServer() { + const serverToDelete = this.selectedServer; + if (!isManagedServer(serverToDelete)) { + const msg = 'cannot delete non-ManagedServer'; + SentryErrorReporter.logError(msg); + throw new Error(msg); + } + + const confirmationTitle = 'Delete Server?'; + const confirmationText = 'Existing users will lose access. This action cannot be undone.'; + const confirmationButton = 'DELETE'; + this.appRoot.getConfirmation(confirmationTitle, confirmationText, confirmationButton, () => { + this.digitalOceanRetry(() => { + return serverToDelete.getHost().delete(); + }) + .then( + () => { + this.appRoot.getServerView().closeServerSettings(); + this.selectedServer = null; + this.showCreateServer(); + this.displayNotification('Server deleted'); + }, + (e) => { + // Don't show a toast on the login screen. + if (!(e instanceof digitalocean_api.XhrError)) { + this.displayError('Failed to delete server', e); + } + }); + }); + } + + private forgetSelectedServer() { + const serverToForget = this.selectedServer; + if (!isManualServer(serverToForget)) { + const msg = 'cannot delete non-ManualServer'; + SentryErrorReporter.logError(msg); + throw new Error(msg); + } + + const confirmationTitle = 'Forget Server?'; + const confirmationText = + 'This action removes your server from the Outline Manager, but does not block proxy access to users. You will still need to manually delete the Outline server from your host machine.'; + const confirmationButton = 'FORGET'; + this.appRoot.getConfirmation(confirmationTitle, confirmationText, confirmationButton, () => { + this.appRoot.getServerView().closeServerSettings(); + serverToForget.forget(); + this.selectedServer = null; + this.showIntro(); + this.displayNotification('Server forgotten'); + }); + } + + private setMetricsEnabled(metricsEnabled: boolean) { + this.runningServer.setMetricsEnabled(metricsEnabled) + .then(() => { + // Change metricsEnabled property on polymer element to update display. + this.appRoot.getServerView().metricsEnabled = metricsEnabled; + }) + .catch((error) => { + this.displayError('Error setting metrics enabled', error); + }); + } + + private renameServer(newName: string): void { + this.runningServer.setName(newName) + .then(() => { + this.appRoot.getServerView().serverName = newName; + }) + .catch((error) => { + this.displayError('Error renaming server', error); + }); + } + + private cancelServerCreation(serverToCancel: server.Server): void { + if (!isManagedServer(serverToCancel)) { + const msg = 'cannot cancel non-ManagedServer'; + SentryErrorReporter.logError(msg); + throw new Error(msg); + } + serverToCancel.getHost().delete().then(() => { + this.showCreateServer(); + }); + } +} diff --git a/src/server_manager/web_app/build_action.sh b/src/server_manager/web_app/build_action.sh new file mode 100755 index 000000000..b657e393b --- /dev/null +++ b/src/server_manager/web_app/build_action.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eux + +readonly NODE_MODULES_BIN_DIR=$ROOT_DIR/src/server_manager/node_modules/.bin + +readonly OUT_DIR=$BUILD_DIR/server_manager/web_app +rm -rf $OUT_DIR + +# Create do_install_script.ts, which has a variable with the content of do_install_server.sh. +mkdir -p $OUT_DIR/ts/server_manager/web_app +mkdir -p $OUT_DIR/sh/server_manager/web_app + +pushd $ROOT_DIR/src/server_manager/install_scripts +tar --create --gzip -f $OUT_DIR/sh/server_manager/web_app/scripts.tgz *.sh +popd + +# Node.js on Cygwin doesn't like absolute Unix-style paths. +# So, we'll use relative paths for a few steps such as Browserify. + +pushd $ROOT_DIR +node src/server_manager/install_scripts/build_install_script_ts.node.js \ + build/server_manager/web_app/sh/server_manager/web_app/scripts.tgz > $ROOT_DIR/src/server_manager/install_scripts/do_install_script.ts +popd + +# Compile Typescript +tsc + +# Browserify node_modules/ (just a couple of key NPMs) and app. +pushd $OUT_DIR +mkdir -p browserified/server_manager/web_app +$NODE_MODULES_BIN_DIR/browserify --require bytes --require clipboard-polyfill -o browserified/node_modules.js +$NODE_MODULES_BIN_DIR/browserify js/server_manager/web_app/main.js -s main -o browserified/server_manager/web_app/main.js +popd + +# Assemble the web app +readonly STATIC_DIR=$OUT_DIR/static +mkdir -p $STATIC_DIR + +# Copy built code +cp -r $OUT_DIR/browserified/* $STATIC_DIR/ + +# Copy static resources +cp -r $ROOT_DIR/src/server_manager/{bower_components,ui_components,index.html,images} $STATIC_DIR diff --git a/src/server_manager/web_app/digitalocean_oauth.ts b/src/server_manager/web_app/digitalocean_oauth.ts new file mode 100644 index 000000000..8509f0e36 --- /dev/null +++ b/src/server_manager/web_app/digitalocean_oauth.ts @@ -0,0 +1,114 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as digitalocean_api from '../cloud/digitalocean_api'; +import {SentryErrorReporter} from './error_reporter'; + +export interface TokenManager { + // Returns the Oauth token, or null if unavailable, and clears the URL. + extractTokenFromUrl(): string; + // Writes the token to storage. + writeTokenToStorage(token: string): void; + // Removes the token from storage. + removeTokenFromStorage(): void; +} + + +// TODO: this class combines URL manipulation with persistence logic. +// Consider moving the URL manipulation logic to a separate class, so we +// can pass in other implementations when the global "window" is not present. +export class DigitalOceanTokenManager implements TokenManager { + private readonly DIGITALOCEAN_TOKEN_STORAGE_KEY = 'LastDOToken'; + + // Searches the current URL (post-OAuth) and local storage for a DigitalOcean + // access token. The token is not checked for validity as this would require + // an extra roundtrip to DigitalOcean. + extractTokenFromUrl(): string { + const tokenFromUrl = this.getTokenFromUrl(); + if (tokenFromUrl) { + const msg = 'found access token in URL'; + console.log(msg); + SentryErrorReporter.logInfo(msg); + // Clear the access_token param it doesn't get sent along with error reports. + this.clearUrl(); + return tokenFromUrl; + } + + const tokenFromStorage = this.getTokenFromStorage(); + if (tokenFromStorage) { + const msg = 'found access token in local storage'; + console.log(msg); + SentryErrorReporter.logInfo(msg); + return tokenFromStorage; + } + + // Not an error as user may not yet have authenticated. + return null; + } + + writeTokenToStorage(token: string): void { + localStorage.setItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY, token); + } + + removeTokenFromStorage(): void { + localStorage.removeItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY); + } + + private getTokenFromUrl(): string { + const urlMatches = window.location.hash.match(/access_token=([^&]*)/); + if (urlMatches && urlMatches[1]) { + return urlMatches[1]; + } + return null; + } + + private clearUrl(): void { + window.location.hash = ''; + } + + private getTokenFromStorage(): string { + return localStorage.getItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY); + } +} + +export function getOauthUrl(currentUrl: string) { + let redirectUrl = currentUrl; + // Running on Electron + if (currentUrl.substr(0, 'outline:'.length) === 'outline:') { + redirectUrl = 'https://www.getoutline.org/digitalocean_oauth'; + } + // Remove trailing '#' + const hashIndex = redirectUrl.indexOf('#'); + if (hashIndex !== -1) { + redirectUrl = redirectUrl.substr(0, hashIndex); + } + const clientId = CLIENT_ID_BY_URL[redirectUrl]; + if (!clientId) { + const msg = 'could not find client ID for redirect url'; + SentryErrorReporter.logError(msg); + throw new Error(`${msg}: ${redirectUrl}`); + } + // Redirects back to the current URL. + return digitalocean_api.getOauthUrl(clientId, redirectUrl); +} + +// DigitalOcean client IDs can be found at +// https://cloud.digitalocean.com/settings/api/applications +// using the App creator's DigitalOcean account. Note each client ID +// only allows for 1 redirect URI. +const CLIENT_ID_BY_URL: {[key: string]: string} = { + // https://cloud.digitalocean.com/settings/api/applications/details/28204 + 'https://www.getoutline.org/digitalocean_oauth': + 'd1879633d5f426356345ae7d46be9b900b1bd58208a72edc8df9e9162be69d9a' +}; diff --git a/src/server_manager/web_app/digitalocean_server.ts b/src/server_manager/web_app/digitalocean_server.ts new file mode 100644 index 000000000..78e31561a --- /dev/null +++ b/src/server_manager/web_app/digitalocean_server.ts @@ -0,0 +1,431 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {EventEmitter} from 'eventemitter3'; + +import {DigitalOceanSession, DropletInfo} from '../cloud/digitalocean_api'; +import * as crypto from '../infrastructure/crypto'; +import * as errors from '../infrastructure/errors'; +import {asciiToHex, hexToString} from '../infrastructure/hex_encoding'; +import * as do_install_script from '../install_scripts/do_install_script'; +import * as server from '../model/server'; + +import {SentryErrorReporter} from './error_reporter'; +import {ShadowboxServer} from './shadowbox_server'; + +// WARNING: these strings must be lowercase due to a DigitalOcean case +// sensitivity bug. + +// Tag used to mark Shadowbox Droplets. +const SHADOWBOX_TAG = 'shadowbox'; +// Prefix used in key-value tags. +const KEY_VALUE_TAG = 'kv'; + +// The tag key for the manager API certificate fingerprint. +const CERTIFICATE_FINGERPRINT_TAG = 'certsha256'; +// The tag key for the manager API URL. +const API_URL_TAG = 'apiurl'; +// The tag which appears if there is an error during installation. +const INSTALL_ERROR_TAG = 'install-error'; + +// These are superceded by the API_URL_TAG +// The tag key for the manager API port. +const DEPRECATED_API_PORT_TAG = 'apiport'; +// The tag key for the manager API url prefix. +const DEPRECATED_API_PREFIX_TAG = 'apiprefix'; + +function makeKeyValueTagPrefix(key: string) { + return makeKeyValueTag(key, ''); +} + +function makeKeyValueTag(key: string, value: string) { + return [KEY_VALUE_TAG, key, asciiToHex(value)].join(':'); +} + +const cityEnglishNameById: {[key: string]: string} = { + ams: 'Amsterdam', + sgp: 'Singapore', + blr: 'Bangalore', + fra: 'Frankfurt', + lon: 'London', + sfo: 'San Francisco', + tor: 'Toronto', + nyc: 'New York' +}; + +// Returns a name for a server in the given region. +export function MakeEnglishNameForServer(regionId: server.RegionId) { + return `Outline Server ${cityEnglishNameById[getCityId(regionId)]}`; +} + +// Possible install states for DigitaloceanServer. +enum InstallState { + // Unknown state - server may still be installing. + UNKNOWN = 0, + // Server is running and has the API URL and certificate fingerprint set. + SUCCESS, + // Server is in an error state. + ERROR, + // Server has been deleted. + DELETED +} + +class DigitaloceanServer extends ShadowboxServer implements server.ManagedServer { + private eventQueue = new EventEmitter(); + private installState: InstallState = InstallState.UNKNOWN; + + constructor(private digitalOcean: DigitalOceanSession, private dropletInfo: DropletInfo) { + // Consider passing a RestEndpoint object to the parent constructor, + // to better encapsulate the management api address logic. + super(); + const msg = 'DigitalOceanServer created'; + console.info(`${msg}: %O`, dropletInfo); + SentryErrorReporter.logInfo(msg); + this.eventQueue.once('server-active', () => console.timeEnd('activeServer')); + this.waitOnInstall(true) + .then(() => { + this.setInstallCompleted(); + }) + .catch((e) => { + console.error('Error installing server', e); + }); + } + + waitOnInstall(resetTimeout: boolean): Promise { + if (resetTimeout) { + this.installState = InstallState.UNKNOWN; + this.refreshInstallState(); + } + + return new Promise((fulfill, reject) => { + // Poll this.installState for changes. This can poll quickly as it + // will not make any network requests. + const intervalId = setInterval(() => { + if (this.installState === InstallState.UNKNOWN) { + // installState not known, wait until next retry. + return; + } + + // State is now known, so we can stop checking. + clearInterval(intervalId); + if (this.installState === InstallState.SUCCESS) { + // Verify that the server is healthy (e.g. server config can be + // retrieved) before fulfilling. + this.isHealthy().then((isHealthy) => { + if (isHealthy) { + fulfill(); + } else { + // Server has been installed (Api Url and Certificate have been) + // set, but is not healthy. This could occur if the server + // is behind a firewall. + reject(new errors.UnreachableServerError()); + } + }); + } else if (this.installState === InstallState.ERROR) { + reject(new errors.ServerInstallFailedError()); + } else if (this.installState === InstallState.DELETED) { + reject(new errors.DeletedServerError()); + } + }, 100); + }); + } + + // Sets this.installState, will keep polling until this.installState can + // be set to something other than UNKNOWN. + private refreshInstallState(): void { + const TIMEOUT_MS = 5 * 60 * 1000; + const startTimestamp = Date.now(); + + // Synchronous function for updating the installState, which doesn't + // refresh droplet info. + const updateInstallState = (): void => { + if (this.installState !== InstallState.UNKNOWN) { + // State is already known, so it cannot be changed. + return; + } + if (this.getTagValue(INSTALL_ERROR_TAG) || Date.now() - startTimestamp >= TIMEOUT_MS) { + this.installState = InstallState.ERROR; + } else if (this.setApiUrlAndCertificate()) { + // API Url and Certificate have been set, so we have successfully + // installed the server and can now make API calls. + this.installState = InstallState.SUCCESS; + } + }; + + // Attempt to set the install state immediately, based on the initial + // droplet info, to possibly save on a refresh API call. + updateInstallState(); + if (this.installState !== InstallState.UNKNOWN) { + return; + } + + // Periodically refresh the droplet info then try to update the install + // state again. + const intervalId = setInterval(() => { + // Check if install state is already known, so we don't make an unnecessary + // request to fetch droplet info. + if (this.installState !== InstallState.UNKNOWN) { + clearInterval(intervalId); + return; + } + this.refreshDropletInfo().then(() => { + updateInstallState(); + // Immediately clear the interval if the installState is known to prevent + // race conditions due to setInterval firing async. + if (this.installState !== InstallState.UNKNOWN) { + clearInterval(intervalId); + return; + } + }); + // Note, if there is an error refreshing the droplet, we should just + // try again, as there may be an intermittent network issue. + }, 3000); + } + + // Returns true on success, else false. + private setApiUrlAndCertificate(): boolean { + try { + // Atempt to get certificate fingerprint and management api address, + // these methods throw exceptions if the fields are unavailable. + const certificateFingerprint = this.getCertificateFingerprint(); + const apiAddress = this.getManagementApiAddress(); + // Loaded both the cert and url without exceptions, they can be set. + this.whitelistCertificate(certificateFingerprint); + this.setManagementApiUrl(apiAddress); + return true; + } catch (e) { + // Install state not yet ready. + return false; + } + } + + // Refreshes the state from DigitalOcean API. + private refreshDropletInfo(): Promise { + return this.digitalOcean.getDroplet(this.dropletInfo.id).then((newDropletInfo: DropletInfo) => { + const oldDropletInfo = this.dropletInfo; + this.dropletInfo = newDropletInfo; + + if (newDropletInfo.status !== oldDropletInfo.status) { + if (newDropletInfo.status === 'active') { + this.eventQueue.emit('server-active'); + } + } + }); + } + + // Gets the value for the given key, stored in the DigitalOcean tags. + private getTagValue(key: string): string { + const tagPrefix = makeKeyValueTagPrefix(key); + for (const tag of this.dropletInfo.tags) { + if (!startsWithCaseInsensitive(tag, tagPrefix)) { + continue; + } + const encodedData = tag.slice(tagPrefix.length); + try { + return hexToString(encodedData); + } catch (e) { + const msg = 'error decoding hex string'; + console.error(msg, e); + SentryErrorReporter.logError(msg); + return null; + } + } + } + + // Returns the public ipv4 address of this server. + private ipv4Address() { + for (const network of this.dropletInfo.networks.v4) { + if (network.type === 'public') { + return network.ip_address; + } + } + return undefined; + } + + // Gets the address for the user management api, throws an error if unavailable. + private getManagementApiAddress(): string { + let apiAddress = this.getTagValue(API_URL_TAG); + // Check the old tags for backward-compatibility. + // TODO(fortuna): Delete this before we release v1.0 + if (!apiAddress) { + const portNumber = this.getTagValue(DEPRECATED_API_PORT_TAG); + if (!portNumber) { + throw new Error('Could not get API port number'); + } + if (!this.ipv4Address()) { + throw new Error('API hostname not set'); + } + apiAddress = `https://${this.ipv4Address()}:${portNumber}/`; + const apiPrefix = this.getTagValue(DEPRECATED_API_PREFIX_TAG); + if (apiPrefix) { + apiAddress += apiPrefix + '/'; + } + } + if (!apiAddress.endsWith('/')) { + apiAddress += '/'; + } + return apiAddress; + } + + // Gets the certificate fingerprint in base64 format, throws an error if + // unavailable. + private getCertificateFingerprint(): string { + const fingerprint = this.getTagValue(CERTIFICATE_FINGERPRINT_TAG); + if (fingerprint) { + return btoa(fingerprint); + } else { + throw new Error('certificate fingerprint unavailable'); + } + } + + getHost(): DigitalOceanHost { + // Construct a new DigitalOceanHost object, to be sure it has the latest + // session and droplet info. + return new DigitalOceanHost(this.digitalOcean, this.dropletInfo, this.onDelete.bind(this)); + } + + // Callback to be invoked once server is deleted. + private onDelete() { + this.installState = InstallState.DELETED; + } + + private getInstallCompletedStorageKey() { + return `droplet-${this.dropletInfo.id}-install-completed`; + } + + private setInstallCompleted() { + localStorage.setItem(this.getInstallCompletedStorageKey(), 'true'); + } + + public isInstallCompleted(): boolean { + return localStorage.getItem(this.getInstallCompletedStorageKey()) === 'true'; + } +} + +class DigitalOceanHost implements server.ManagedServerHost { + constructor( + private digitalOcean: DigitalOceanSession, private dropletInfo: DropletInfo, + private deleteCallback: Function) {} + + getMonthlyTransferLimit(): server.DataAmount { + return {terabytes: this.dropletInfo.size.transfer}; + } + + getMonthlyCost(): server.MonetaryCost { + return {usd: this.dropletInfo.size.price_monthly}; + } + + getRegionId(): server.RegionId { + return this.dropletInfo.region.slug; + } + + delete(): Promise { + return this.digitalOcean.deleteDroplet(this.dropletInfo.id).then(() => { + this.deleteCallback(); + }); + } +} + +function startsWithCaseInsensitive(text: string, prefix: string) { + return text.slice(0, prefix.length).toLowerCase() === prefix.toLowerCase(); +} + +function getCityId(slug: server.RegionId): string { + return slug.substr(0, 3).toLowerCase(); +} + +const MACHINE_SIZE = '512mb'; + +export class DigitaloceanServerRepository implements server.ManagedServerRepository { + constructor( + private digitalOcean: DigitalOceanSession, private image: string, private metricsUrl: string, + private sentryApiUrl: string, private debugMode: boolean) {} + + // Return a map of regions that are available and support our target machine size. + getRegionMap(): Promise> { + return this.digitalOcean.getRegionInfo().then((regions) => { + const ret: server.RegionMap = {}; + regions.forEach((region) => { + const cityId = getCityId(region.slug); + if (!(cityId in ret)) { + ret[cityId] = []; + } + if (region.available && region.sizes.indexOf(MACHINE_SIZE) !== -1) { + ret[cityId].push(region.slug); + } + }); + return ret; + }); + } + + // Creates a server and returning it when it becomes active. + createServer(region: server.RegionId): Promise { + const name = MakeEnglishNameForServer(region); + console.time('activeServer'); + console.time('servingServer'); + const keyPair = crypto.generateKeyPair(); + const installCommand = getInstallScript( + this.digitalOcean.accessToken, name, this.image, this.metricsUrl, this.sentryApiUrl); + + const dropletSpec = { + installCommand, + size: MACHINE_SIZE, + image: 'docker', + tags: [SHADOWBOX_TAG], + }; + return this.digitalOcean.createDroplet(name, region, keyPair.public, dropletSpec) + .then((response) => { + if (this.debugMode) { + // Strip carriage returns. They produce annoying blank lines when pasting + // into a terminal. + console.log( + `private key for SSH access to new droplet:\n${ + keyPair.private.replace(/\r/g, '')}\n\n` + + 'Use "ssh -i keyfile root@[ip_address]" to connect to the machine'); + } + return new DigitaloceanServer(this.digitalOcean, response.droplet); + }); + } + + listServers(): Promise { + return this.digitalOcean.getDropletsByTag(SHADOWBOX_TAG).then((droplets) => { + console.log('Found droplets: ', droplets); + return droplets.map((droplet) => { + return new DigitaloceanServer(this.digitalOcean, droplet); + }); + }); + } +} + +function sanitizeDigitaloceanToken(input: string): string { + const sanitizedInput = input.trim(); + const pattern = /^[A-Za-z0-9_\/-]+$/; + if (!pattern.test(sanitizedInput)) { + throw new Error('Invalid DigitalOcean Token'); + } + return sanitizedInput; +} + +// cloudFunctions needs to define cloud::public_ip and cloud::add_tag. +function getInstallScript( + accessToken: string, name: string, image?: string, metricsUrl?: string, + sentryApiUrl?: string): string { + const sanitizezedAccessToken = sanitizeDigitaloceanToken(accessToken); + return '#!/bin/bash -eu\n' + + `export DO_ACCESS_TOKEN=${sanitizezedAccessToken}\n` + + (image ? `export SB_IMAGE=${image}\n` : '') + + (sentryApiUrl ? `export SENTRY_API_URL="${sentryApiUrl}"\n` : '') + + (metricsUrl ? `export SB_METRICS_URL=${metricsUrl}\n` : '') + + `export SB_DEFAULT_SERVER_NAME="${name}"\n` + do_install_script.SCRIPT; +} diff --git a/src/server_manager/web_app/error_reporter.ts b/src/server_manager/web_app/error_reporter.ts new file mode 100644 index 000000000..eaffe8f05 --- /dev/null +++ b/src/server_manager/web_app/error_reporter.ts @@ -0,0 +1,80 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as Raven from 'raven-js'; + +import * as errors from '../infrastructure/errors'; + +// TODO(dborkan): This class contains a lot of duplication from the client but +// has the Cordova specific logic removed. Consider combining +// these into 1 shared library if possible. +// tslint:disable-next-line:no-namespace +export namespace SentryErrorReporter { + export class IllegalStateError extends errors.OutlineError { + constructor(message?: string) { + super(message); + } + } + + export function init(sentryDsn: string, appVersion: string): void { + if (Raven.isSetup()) { + throw new IllegalStateError('Error reporter already initialized.'); + } + // Breadcrumbs for console logging and XHR may include PII such as the server IP address, + // secret API prefix, or shadowsocks access credentials. Only enable DOM breadcrumbs to receive + // UI click data. + const autoBreadcrumbOptions = { + dom: true, + console: false, + location: false, + xhr: false, + }; + Raven.config(sentryDsn, {autoBreadcrumbs: autoBreadcrumbOptions, release: appVersion}) + .install(); + try { + // tslint:disable-next-line:no-any + window.addEventListener('unhandledrejection', (event: any) => { + Raven.captureException(event.reason); + }); + } catch (e) { + // window.addEventListener not available, i.e. not running in a browser + // environment. + // TODO: refactor this code so the try/catch isn't necessary and the + // unhandledrejection listener can be tested. + } + } + + export function report(userFeedback: string, feedbackCategory: string, userEmail?: string): void { + if (!Raven.isSetup()) { + throw new IllegalStateError('Error reporter not initialized.'); + } + Raven.setUserContext({email: userEmail || ''}); + Raven.captureMessage(userFeedback, {tags: {category: feedbackCategory}}); + Raven.setUserContext(); // Reset the user context, don't cache the email + } + + // Logs an info message to be sent to Sentry when `report` is called. + export function logInfo(message: string): void { + log({message, level: 'info'}); + } + + // Logs an error message to be sent to Sentry when `report` is called. + export function logError(message: string): void { + log({message, level: 'error'}); + } + + function log(breadcrumb: Raven.Breadcrumb) { + Raven.captureBreadcrumb(breadcrumb); + } +} diff --git a/src/server_manager/web_app/main.ts b/src/server_manager/web_app/main.ts new file mode 100644 index 000000000..7fbc9f334 --- /dev/null +++ b/src/server_manager/web_app/main.ts @@ -0,0 +1,61 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A little bit of Node.js that is available to us thanks to Browserify. +import * as url from 'url'; + +import * as digitalocean_api from '../cloud/digitalocean_api'; + +import {App} from './app'; +import {DigitalOceanTokenManager} from './digitalocean_oauth'; +import * as digitalocean_server from './digitalocean_server'; +import {SentryErrorReporter} from './error_reporter'; +import {ManualServerRepository} from './manual_server'; + +const DEFAULT_SENTRY_DSN = 'https://533e56d1b2d64314bd6092a574e6d0f1@sentry.io/215496'; + +document.addEventListener('WebComponentsReady', () => { + // Parse URL query params. + const queryParams = url.parse(document.URL, true).query; + const debugMode = queryParams.outlineDebugMode === 'true'; + const metricsUrl = queryParams.metricsUrl; + const shadowboxImage = queryParams.image; + const version = queryParams.version; + const sentryDsn = queryParams.sentryDsn || DEFAULT_SENTRY_DSN; + + // Initialize error reporting. + SentryErrorReporter.init(sentryDsn, version); + + // Set DigitalOcean server repository parameters. + const digitalOceanServerRepositoryFactory = (session: digitalocean_api.DigitalOceanSession) => { + return new digitalocean_server.DigitaloceanServerRepository( + session, shadowboxImage, metricsUrl, getSentryApiUrl(sentryDsn), debugMode); + }; + + // Create and start the app. + new App( + document.getElementById('appRoot'), document.URL, version, + digitalocean_api.createDigitalOceanSession, digitalOceanServerRepositoryFactory, + new ManualServerRepository('manualServers'), new DigitalOceanTokenManager()) + .start(); +}); + +// Returns Sentry URL for DSN string. +// e.g. for DSN "https://ee9db4eb185b471ca08c8eb5efbf61f1@sentry.io/214597" +// this will return +// "https://sentry.io/api/214597/store/?sentry_version=7&sentry_key=ee9db4eb185b471ca08c8eb5efbf61f1" +function getSentryApiUrl(sentryDsn: string): string { + const matches = sentryDsn.match(/https:\/\/(\S+)@sentry\.io\/(\d+)/); + return `https://sentry.io/api/${matches[2]}/store/?sentry_version=7&sentry_key=${matches[1]}`; +} diff --git a/src/server_manager/web_app/manual_server.ts b/src/server_manager/web_app/manual_server.ts new file mode 100644 index 000000000..76bb5b9a5 --- /dev/null +++ b/src/server_manager/web_app/manual_server.ts @@ -0,0 +1,77 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {hexToString} from '../infrastructure/hex_encoding'; +import * as server from '../model/server'; + +import {SentryErrorReporter} from './error_reporter'; +import {ShadowboxServer} from './shadowbox_server'; + +class ManualServer extends ShadowboxServer implements server.ManualServer { + constructor(config: server.ManualServerConfig, private forgetCallback: Function) { + super(); + this.setManagementApiUrl(config.apiUrl); + // config.certSha256 is expected to be in hex format (install script). + // Electron requires that this be decoded from hex (to unprintable binary), + // then encoded as base64. + try { + this.whitelistCertificate(btoa(hexToString(config.certSha256))); + } catch (e) { + // Error whitelisting certificate, may be due to bad user input. + const msg = 'Error whitelisting certificate'; + console.error(msg, e); + SentryErrorReporter.logError(msg); + } + } + + forget(): void { + this.forgetCallback(); + } +} + +export class ManualServerRepository implements server.ManualServerRepository { + constructor(private storageKey: string) {} + + addServer(config: server.ManualServerConfig): Promise { + const server = new ManualServer(config, this.forgetServer.bind(this)); + // Write to storage as an array, so we can easily extend this once we support + // multiple servers. + localStorage.setItem(this.storageKey, JSON.stringify([config])); + return Promise.resolve(server); + } + + listServers(): Promise { + const serversJson = localStorage.getItem(this.storageKey); + if (serversJson) { + try { + const serversData = JSON.parse(serversJson); + const manualServers = serversData.map((config: server.ManualServerConfig) => { + return new ManualServer(config, this.forgetServer.bind(this)); + }); + return Promise.resolve(manualServers); + } catch (e) { + const msg = 'Error creating manual servers from localStorage'; + console.error(msg, e); + SentryErrorReporter.logError(msg); + } + } + return Promise.resolve([]); + } + + private forgetServer(): void { + // TODO(dborkan): extend this code to find a specific server for deleting, + // once we support multiple servers. + localStorage.removeItem(this.storageKey); + } +} diff --git a/src/server_manager/web_app/shadowbox_server.ts b/src/server_manager/web_app/shadowbox_server.ts new file mode 100644 index 000000000..6e54a0894 --- /dev/null +++ b/src/server_manager/web_app/shadowbox_server.ts @@ -0,0 +1,177 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as server from '../model/server'; +import {SentryErrorReporter} from './error_reporter'; + +// Interfaces used by metrics REST APIs. +interface MetricsEnabled { + metricsEnabled: boolean; +} +export interface ServerName { name: string; } +export interface ServerConfig { + name: string; + metricsEnabled: boolean; + serverId: string; + createdTimestampMs: number; +} + +// This function is defined in electron_app/preload.ts. +declare function whitelistCertificate(fp: string): boolean; + +export class ShadowboxServer implements server.Server { + private managementApiAddress: string; + private serverConfig: ServerConfig; + + constructor() {} + + listAccessKeys(): Promise { + return this.apiRequest<{accessKeys: server.AccessKey[]}>('access-keys').then((response) => { + return response.accessKeys; + }); + } + + addAccessKey(): Promise { + return this.apiRequest('access-keys', {method: 'POST'}); + } + + renameAccessKey(accessKeyId: server.AccessKeyId, name: string): Promise { + const body = new FormData(); + body.append('name', name); + return this.apiRequest('access-keys/' + accessKeyId + '/name', {method: 'PUT', body}); + } + + removeAccessKey(accessKeyId: server.AccessKeyId): Promise { + return this.apiRequest('access-keys/' + accessKeyId, {method: 'DELETE'}); + } + + getDataUsage(): Promise { + return this.apiRequest('metrics/transfer'); + } + + getName(): string { + return this.serverConfig.name; + } + + setName(name: string): Promise { + const requestOptions: RequestInit = { + method: 'PUT', + headers: new Headers({'Content-Type': 'application/json'}), + body: JSON.stringify({name}) + }; + return this.apiRequest('name', requestOptions).then(() => { + this.serverConfig.name = name; + }); + } + + getMetricsEnabled(): boolean { + return this.serverConfig.metricsEnabled; + } + + setMetricsEnabled(metricsEnabled: boolean): Promise { + const requestOptions: RequestInit = { + method: 'PUT', + headers: new Headers({'Content-Type': 'application/json'}), + body: JSON.stringify({metricsEnabled}) + }; + return this.apiRequest('metrics/enabled', requestOptions).then(() => { + this.serverConfig.metricsEnabled = metricsEnabled; + }); + } + + getServerId(): string { + return this.serverConfig.serverId; + } + + isHealthy(timeoutMs = 30000): Promise { + return new Promise((fulfill, reject) => { + // Query the API and expect a successful response to validate that the + // service is up and running. + this.getServerConfig().then( + (serverConfig) => { + this.serverConfig = serverConfig; + fulfill(true); + }, + (e) => { + fulfill(false); + }); + // Return not healthy if API doesn't complete within timeoutMs. + setTimeout(() => { + fulfill(false); + }, timeoutMs); + }); + } + + getCreatedDate(): Date { + return new Date(this.serverConfig.createdTimestampMs); + } + + private getServerConfig(): Promise { + return this.apiRequest('server'); + } + + whitelistCertificate(base64Fingerprint: string): void { + // This function is defined in electron_app/preload.ts if we are running + // in the electron app, otherwise it will not be defined. + if (typeof whitelistCertificate === 'function') { + whitelistCertificate(base64Fingerprint); + } + } + + protected setManagementApiUrl(apiAddress: string): void { + this.managementApiAddress = apiAddress; + } + + // Makes a request to the management API. + private apiRequest(path: string, options?: RequestInit): Promise { + try { + let apiAddress = this.managementApiAddress; + if (!apiAddress) { + const msg = 'Management API address unavailable'; + SentryErrorReporter.logError(msg); + throw new Error(msg); + } + if (!apiAddress.endsWith('/')) { + apiAddress += '/'; + } + const url = apiAddress + path; + console.log(`Fetching url ${url}...`); + return fetch(url, options) + .then( + (response) => { + console.log('Fetch result:', url, response.ok); + if (!response.ok) { + const msg = 'Failed to fetch API request results'; + SentryErrorReporter.logError(msg); + throw new Error(msg); + } + return response.text(); + }, + (error: Error) => { + const msg = 'Failed to fetch url'; + console.error(msg, url, error); + SentryErrorReporter.logError(msg); + throw error; + }) + .then((body) => { + if (!body) { + return; + } + return JSON.parse(body); + }); + } catch (error) { + return Promise.reject(error); + } + } +} diff --git a/src/server_manager/web_app/test_action.sh b/src/server_manager/web_app/test_action.sh new file mode 100755 index 000000000..f2881a950 --- /dev/null +++ b/src/server_manager/web_app/test_action.sh @@ -0,0 +1,18 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +do_action server_manager/web_app/build +jasmine --config=$ROOT_DIR/jasmine.json diff --git a/src/server_manager/yarn.lock b/src/server_manager/yarn.lock new file mode 100644 index 000000000..54197fec1 --- /dev/null +++ b/src/server_manager/yarn.lock @@ -0,0 +1,3177 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"7zip-bin-linux@~1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/7zip-bin-linux/-/7zip-bin-linux-1.3.1.tgz#4856db1ab1bf5b6ee8444f93f5a8ad71446d00d5" + +"7zip-bin-mac@~1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/7zip-bin-mac/-/7zip-bin-mac-1.0.1.tgz#3e68778bbf0926adc68159427074505d47555c02" + +"7zip-bin-win@~2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/7zip-bin-win/-/7zip-bin-win-2.2.0.tgz#0b81c43e911100f3ece2ebac4f414ca95a572d5b" + +"7zip-bin@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-3.1.0.tgz#70814c6b6d44fef8b74be6fc64d3977a2eff59a5" + optionalDependencies: + "7zip-bin-linux" "~1.3.1" + "7zip-bin-mac" "~1.0.1" + "7zip-bin-win" "~2.2.0" + +"@types/bytes@^2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@types/bytes/-/bytes-2.5.1.tgz#392c42adb65e16d32328b82b58fbc1611b75da89" + +"@types/node-forge@^0.6.9": + version "0.6.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.6.11.tgz#bb7781db81caef361f8ea060bad73cb921843821" + +"@types/node@^7.0.18": + version "7.0.52" + resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.52.tgz#8990d3350375542b0c21a83cd0331e6a8fc86716" + +"@types/semver@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.4.0.tgz#f3658535af7f1f502acd6da7daf405ffeb1f7ee4" + +JSONStream@^1.0.3: + version "1.3.2" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea" + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.2.1: + version "5.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822" + +ajv-keywords@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" + +ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ajv@^6.1.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.2.1.tgz#28a6abc493a2abe0fb4c8507acaedb43fa550671" + dependencies: + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ansi-align@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" + dependencies: + string-width "^2.0.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + +app-builder-bin-linux@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/app-builder-bin-linux/-/app-builder-bin-linux-1.6.0.tgz#d7731d7988b8a740e74d591cbd565f168a266111" + +app-builder-bin-linux@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/app-builder-bin-linux/-/app-builder-bin-linux-1.7.2.tgz#a764c8e52ecf1b5b068f32c820c6daf1ffed6a8f" + +app-builder-bin-mac@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/app-builder-bin-mac/-/app-builder-bin-mac-1.6.0.tgz#c976da70796d67aeb7134a57899636f2581d1c67" + +app-builder-bin-mac@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/app-builder-bin-mac/-/app-builder-bin-mac-1.7.2.tgz#c4ee0d950666c97c12a45ac74ec6396be3357644" + +app-builder-bin-win@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/app-builder-bin-win/-/app-builder-bin-win-1.6.0.tgz#528ef96430d519c270b4de260bea0ddc70df1733" + +app-builder-bin-win@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/app-builder-bin-win/-/app-builder-bin-win-1.7.2.tgz#7acac890782f4118f09941b343ba06c56452a6f6" + +app-builder-bin@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-1.6.0.tgz#c0e88a488d4c23c2e7fe0bbfb70c1d61165be206" + optionalDependencies: + app-builder-bin-linux "1.6.0" + app-builder-bin-mac "1.6.0" + app-builder-bin-win "1.6.0" + +app-builder-bin@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-1.7.2.tgz#daf67060a6bad8f5f611a0d2876d9db897a83f06" + optionalDependencies: + app-builder-bin-linux "1.7.2" + app-builder-bin-mac "1.7.2" + app-builder-bin-win "1.7.2" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +args@^2.3.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/args/-/args-2.6.1.tgz#b2590ed4168cd31b62444199bdc5166bb1920c2f" + dependencies: + camelcase "4.1.0" + chalk "1.1.3" + minimist "1.2.0" + pkginfo "0.4.0" + string-similarity "1.1.0" + +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" + +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asn1.js@^4.0.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +astw@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/astw/-/astw-2.2.0.tgz#7bd41784d32493987aeb239b6b4e1c57a873b917" + dependencies: + acorn "^4.0.3" + +async-exit-hook@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" + +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +bignumber.js@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8" + +bluebird-lst@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.5.tgz#bebc83026b7e92a72871a3dc599e219cbfb002a9" + dependencies: + bluebird "^3.5.1" + +bluebird@^3.5.0, bluebird@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + +bmp-js@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.3.tgz#64113e9c7cf1202b376ed607bf30626ebe57b18a" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +bower@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.2.tgz#adf53529c8d4af02ef24fb8d5341c1419d33e2f7" + +boxen@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" + dependencies: + ansi-align "^2.0.0" + camelcase "^4.0.0" + chalk "^2.0.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^1.2.0" + widest-line "^2.0.0" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browser-pack@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.0.3.tgz#91ca96518583ef580ab063a309de62e407767a39" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.8.0" + defined "^1.0.0" + safe-buffer "^5.1.1" + through2 "^2.0.0" + umd "^3.0.0" + +browser-resolve@^1.11.0, browser-resolve@^1.7.0: + version "1.11.2" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" + dependencies: + resolve "1.1.7" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + dependencies: + pako "~1.0.5" + +browserify@^14.5.0: + version "14.5.0" + resolved "https://registry.yarnpkg.com/browserify/-/browserify-14.5.0.tgz#0bbbce521acd6e4d1d54d8e9365008efb85a9cc5" + dependencies: + JSONStream "^1.0.3" + assert "^1.4.0" + browser-pack "^6.0.1" + browser-resolve "^1.11.0" + browserify-zlib "~0.2.0" + buffer "^5.0.2" + cached-path-relative "^1.0.0" + concat-stream "~1.5.1" + console-browserify "^1.1.0" + constants-browserify "~1.0.0" + crypto-browserify "^3.0.0" + defined "^1.0.0" + deps-sort "^2.0.0" + domain-browser "~1.1.0" + duplexer2 "~0.1.2" + events "~1.1.0" + glob "^7.1.0" + has "^1.0.0" + htmlescape "^1.1.0" + https-browserify "^1.0.0" + inherits "~2.0.1" + insert-module-globals "^7.0.0" + labeled-stream-splicer "^2.0.0" + module-deps "^4.0.8" + os-browserify "~0.3.0" + parents "^1.0.1" + path-browserify "~0.0.0" + process "~0.11.0" + punycode "^1.3.2" + querystring-es3 "~0.2.0" + read-only-stream "^2.0.0" + readable-stream "^2.0.2" + resolve "^1.1.4" + shasum "^1.0.0" + shell-quote "^1.6.1" + stream-browserify "^2.0.0" + stream-http "^2.0.0" + string_decoder "~1.0.0" + subarg "^1.0.0" + syntax-error "^1.1.1" + through2 "^2.0.0" + timers-browserify "^1.0.1" + tty-browserify "~0.0.0" + url "~0.11.0" + util "~0.10.1" + vm-browserify "~0.0.1" + xtend "^4.0.0" + +buffer-equal@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^5.0.2: + version "5.0.8" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.0.8.tgz#84daa52e7cf2fa8ce4195bc5cf0f7809e0930b24" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +builder-util-runtime@4.0.5, builder-util-runtime@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-4.0.5.tgz#5340cf9886b9283ea6e5b20dc09b5e3e461aef62" + dependencies: + bluebird-lst "^1.0.5" + debug "^3.1.0" + fs-extra-p "^4.5.0" + sax "^1.2.4" + +builder-util@5.6.1: + version "5.6.1" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-5.6.1.tgz#ab90845cb8949ea4ac81da0ce87b8ec3424cdbf9" + dependencies: + "7zip-bin" "~3.1.0" + app-builder-bin "1.6.0" + bluebird-lst "^1.0.5" + builder-util-runtime "^4.0.5" + chalk "^2.3.2" + debug "^3.1.0" + fs-extra-p "^4.5.2" + is-ci "^1.1.0" + js-yaml "^3.10.0" + lazy-val "^1.0.3" + semver "^5.5.0" + source-map-support "^0.5.3" + stat-mode "^0.2.2" + temp-file "^3.1.1" + +builder-util@5.6.4: + version "5.6.4" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-5.6.4.tgz#d130dded98a58d8bee791408a7071f825f488c7a" + dependencies: + "7zip-bin" "~3.1.0" + app-builder-bin "1.7.2" + bluebird-lst "^1.0.5" + builder-util-runtime "^4.0.5" + chalk "^2.3.2" + debug "^3.1.0" + fs-extra-p "^4.5.2" + is-ci "^1.1.0" + js-yaml "^3.11.0" + lazy-val "^1.0.3" + semver "^5.5.0" + source-map-support "^0.5.3" + stat-mode "^0.2.2" + temp-file "^3.1.1" + +builder-util@^5.6.0: + version "5.6.5" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-5.6.5.tgz#f2d156541b8df9599456848e057566443dc04c82" + dependencies: + "7zip-bin" "~3.1.0" + app-builder-bin "1.7.2" + bluebird-lst "^1.0.5" + builder-util-runtime "^4.0.5" + chalk "^2.3.2" + debug "^3.1.0" + fs-extra-p "^4.5.2" + is-ci "^1.1.0" + js-yaml "^3.11.0" + lazy-val "^1.0.3" + semver "^5.5.0" + source-map-support "^0.5.3" + stat-mode "^0.2.2" + temp-file "^3.1.1" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + +cached-path-relative@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@4.1.0, camelcase@^4.0.0, camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +capture-stack-trace@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +caseless@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.6.0.tgz#8167c1ab8397fb5bb95f96d28e5a81c50f247ac4" + +chalk@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.1, chalk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chalk@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chromium-pickle-js@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" + +ci-info@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +cli-boxes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + +clipboard-polyfill@^2.4.6: + version "2.4.6" + resolved "https://registry.yarnpkg.com/clipboard-polyfill/-/clipboard-polyfill-2.4.6.tgz#9fcf2ad9e0a3a0e76c256182b159de0e679b2709" + dependencies: + es6-promise "4.1.1" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +cliui@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc" + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-convert@~0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +combine-source-map@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e" + dependencies: + convert-source-map "~1.1.0" + inline-source-map "~0.6.0" + lodash.memoize "~3.0.3" + source-map "~0.5.3" + +combine-source-map@~0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b" + dependencies: + convert-source-map "~1.1.0" + inline-source-map "~0.6.0" + lodash.memoize "~3.0.3" + source-map "~0.5.3" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +compare-version@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@~1.5.0, concat-stream@~1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + +configstore@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90" + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +constants-browserify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +convert-source-map@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-error-class@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + dependencies: + capture-stack-trace "^1.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + +crypto-browserify@^3.0.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.0.0, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +del@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +deps-sort@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5" + dependencies: + JSONStream "^1.0.3" + shasum "^1.0.0" + subarg "^1.0.0" + through2 "^2.0.0" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +detective@^4.0.0: + version "4.7.1" + resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" + dependencies: + acorn "^5.2.1" + defined "^1.0.0" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dmg-builder@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-4.1.1.tgz#a12214eb3eb3cba0addccfd129f1981c9805045c" + dependencies: + bluebird-lst "^1.0.5" + builder-util "^5.6.0" + electron-builder-lib "~20.2.0" + fs-extra-p "^4.5.2" + iconv-lite "^0.4.19" + js-yaml "^3.10.0" + parse-color "^1.0.0" + sanitize-filename "^1.6.1" + +dom-walk@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" + +domain-browser@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + dependencies: + is-obj "^1.0.0" + +dotenv-expand@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.0.1.tgz#68fddc1561814e0a10964111057ff138ced7d7a8" + +dotenv@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" + +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + dependencies: + readable-stream "^2.0.2" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ejs@^2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" + +electron-builder-lib@20.4.1: + version "20.4.1" + resolved "https://registry.yarnpkg.com/electron-builder-lib/-/electron-builder-lib-20.4.1.tgz#8560563e21ca0596046eac398cad204154665a5e" + dependencies: + "7zip-bin" "~3.1.0" + app-builder-bin "1.7.2" + async-exit-hook "^2.0.1" + bluebird-lst "^1.0.5" + builder-util "5.6.4" + builder-util-runtime "4.0.5" + chromium-pickle-js "^0.2.0" + debug "^3.1.0" + ejs "^2.5.7" + electron-osx-sign "0.4.10" + electron-publish "20.2.0" + fs-extra-p "^4.5.2" + hosted-git-info "^2.6.0" + is-ci "^1.1.0" + isbinaryfile "^3.0.2" + js-yaml "^3.11.0" + lazy-val "^1.0.3" + minimatch "^3.0.4" + normalize-package-data "^2.4.0" + plist "^2.1.0" + read-config-file "3.0.0" + sanitize-filename "^1.6.1" + semver "^5.5.0" + temp-file "^3.1.1" + +electron-builder-lib@~20.2.0: + version "20.2.1" + resolved "https://registry.yarnpkg.com/electron-builder-lib/-/electron-builder-lib-20.2.1.tgz#ff8dc6ac7f6f3c676fc370ddafb2aba464a17672" + dependencies: + "7zip-bin" "~3.1.0" + app-builder-bin "1.6.0" + async-exit-hook "^2.0.1" + bluebird-lst "^1.0.5" + builder-util "5.6.1" + builder-util-runtime "4.0.5" + chromium-pickle-js "^0.2.0" + debug "^3.1.0" + ejs "^2.5.7" + electron-osx-sign "0.4.8" + electron-publish "20.2.0" + fs-extra-p "^4.5.2" + hosted-git-info "^2.5.0" + is-ci "^1.1.0" + isbinaryfile "^3.0.2" + js-yaml "^3.10.0" + lazy-val "^1.0.3" + minimatch "^3.0.4" + normalize-package-data "^2.4.0" + plist "^2.1.0" + read-config-file "3.0.0" + sanitize-filename "^1.6.1" + semver "^5.5.0" + temp-file "^3.1.1" + +electron-builder@^20.4.1: + version "20.4.1" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-20.4.1.tgz#ec8b5ada929df8d7a4ab4b1685568be4e12bf8f3" + dependencies: + bluebird-lst "^1.0.5" + builder-util "5.6.4" + builder-util-runtime "4.0.5" + chalk "^2.3.2" + dmg-builder "4.1.1" + electron-builder-lib "20.4.1" + electron-download-tf "4.3.4" + fs-extra-p "^4.5.2" + is-ci "^1.1.0" + lazy-val "^1.0.3" + read-config-file "3.0.0" + sanitize-filename "^1.6.1" + update-notifier "^2.3.0" + yargs "^11.0.0" + +electron-download-tf@4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/electron-download-tf/-/electron-download-tf-4.3.4.tgz#b03740b2885aa2ad3f8784fae74df427f66d5165" + dependencies: + debug "^3.0.0" + env-paths "^1.0.0" + fs-extra "^4.0.1" + minimist "^1.2.0" + nugget "^2.0.1" + path-exists "^3.0.0" + rc "^1.2.1" + semver "^5.4.1" + sumchecker "^2.0.2" + +electron-download@^3.0.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-3.3.0.tgz#2cfd54d6966c019c4d49ad65fbe65cc9cdef68c8" + dependencies: + debug "^2.2.0" + fs-extra "^0.30.0" + home-path "^1.0.1" + minimist "^1.2.0" + nugget "^2.0.0" + path-exists "^2.1.0" + rc "^1.1.2" + semver "^5.3.0" + sumchecker "^1.2.0" + +electron-icon-maker@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/electron-icon-maker/-/electron-icon-maker-0.0.4.tgz#0766087c270a736d0857204bb72130d574d91c51" + dependencies: + args "^2.3.0" + icon-gen "1.0.7" + jimp "^0.2.27" + +electron-osx-sign@0.4.10: + version "0.4.10" + resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz#be4f3b89b2a75a1dc5f1e7249081ab2929ca3a26" + dependencies: + bluebird "^3.5.0" + compare-version "^0.1.2" + debug "^2.6.8" + isbinaryfile "^3.0.2" + minimist "^1.2.0" + plist "^2.1.0" + +electron-osx-sign@0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.8.tgz#f0b9fadded9e1e54ec35fa89877b5c6c34c7bc40" + dependencies: + bluebird "^3.5.0" + compare-version "^0.1.2" + debug "^2.6.8" + isbinaryfile "^3.0.2" + minimist "^1.2.0" + plist "^2.1.0" + +electron-publish@20.2.0: + version "20.2.0" + resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-20.2.0.tgz#1812738c4a4e14a8e156a9a083424a6e4e8e8264" + dependencies: + bluebird-lst "^1.0.5" + builder-util "^5.6.0" + builder-util-runtime "^4.0.5" + chalk "^2.3.0" + fs-extra-p "^4.5.2" + lazy-val "^1.0.3" + mime "^2.2.0" + +electron@^1.7.9: + version "1.7.10" + resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.10.tgz#3a3e83d965fd7fafe473be8ddf8f472561b6253d" + dependencies: + "@types/node" "^7.0.18" + electron-download "^3.0.1" + extract-zip "^1.0.3" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +env-paths@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +es6-promise@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" + +es6-promise@^3.0.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + +es6-promise@^4.0.3, es6-promise@^4.0.5: + version "4.2.2" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.2.tgz#f722d7769af88bd33bc13ec6605e1f92966b82d9" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +eventemitter3@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" + +events@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exif-parser@^0.1.9: + version "0.1.12" + resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" + +extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extract-zip@^1.0.3, extract-zip@^1.6.5: + version "1.6.6" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" + dependencies: + concat-stream "1.6.0" + debug "2.6.9" + mkdirp "0.5.0" + yauzl "2.4.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + +file-type@^3.1.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + +file-url@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/file-url/-/file-url-1.1.0.tgz#a0f9cf3eb6904c9b1d3a6790b83a976fc40217bb" + dependencies: + meow "^3.7.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +for-each@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" + dependencies: + is-function "~1.0.0" + +forever-agent@~0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.5.2.tgz#6d0e09c4921f94a27f63d3b49c5feff1ea4c5130" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +fs-extra-p@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/fs-extra-p/-/fs-extra-p-4.5.0.tgz#b79f3f3fcc0b5e57b7e7caeb06159f958ef15fe8" + dependencies: + bluebird-lst "^1.0.5" + fs-extra "^5.0.0" + +fs-extra-p@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/fs-extra-p/-/fs-extra-p-4.5.2.tgz#0a22aba489284d17f375d5dc5139aa777fe2df51" + dependencies: + bluebird-lst "^1.0.5" + fs-extra "^5.0.0" + +fs-extra@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + +fs-extra@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +function-bind@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob@^7.0.3, glob@^7.0.5, glob@^7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + dependencies: + ini "^1.3.4" + +global@~4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f" + dependencies: + min-document "^2.19.0" + process "~0.5.1" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +has@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hasha@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" + dependencies: + is-stream "^1.0.1" + pinkie-promise "^2.0.0" + +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + +home-path@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/home-path/-/home-path-1.0.5.tgz#788b29815b12d53bacf575648476e6f9041d133f" + +hosted-git-info@^2.1.4, hosted-git-info@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +hosted-git-info@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" + +htmlescape@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + +icon-gen@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/icon-gen/-/icon-gen-1.0.7.tgz#0c710adccbf96e10d05c4595d549df43e423a20a" + dependencies: + del "^2.2.2" + mkdirp "^0.5.1" + pngjs "^3.0.0" + svg2png "4.1.0" + uuid "^3.0.0" + +iconv-lite@^0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@^1.3.4, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +inline-source-map@~0.6.0: + version "0.6.2" + resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" + dependencies: + source-map "~0.5.3" + +insert-module-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.7.1" + concat-stream "~1.5.1" + is-buffer "^1.1.0" + lexical-scope "^1.2.0" + process "~0.11.0" + through2 "^2.0.0" + xtend "^4.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ip-regex@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-buffer@^1.1.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-ci@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5" + dependencies: + ci-info "^1.0.0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-function@^1.0.1, is-function@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" + +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + +is-retry-allowed@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" + +is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +isarray@0.0.1, isarray@~0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isbinaryfile@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jimp@^0.2.27: + version "0.2.28" + resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.2.28.tgz#dd529a937190f42957a7937d1acc3a7762996ea2" + dependencies: + bignumber.js "^2.1.0" + bmp-js "0.0.3" + es6-promise "^3.0.2" + exif-parser "^0.1.9" + file-type "^3.1.0" + jpeg-js "^0.2.0" + load-bmfont "^1.2.3" + mime "^1.3.4" + mkdirp "0.5.1" + pixelmatch "^4.0.0" + pngjs "^3.0.0" + read-chunk "^1.0.1" + request "^2.65.0" + stream-to-buffer "^0.1.0" + tinycolor2 "^1.1.2" + url-regex "^3.0.0" + +jpeg-js@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.2.0.tgz#53e448ec9d263e683266467e9442d2c5a2ef5482" + +js-yaml@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.0, json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kew@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +labeled-stream-splicer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59" + dependencies: + inherits "^2.0.1" + isarray "~0.0.1" + stream-splicer "^2.0.0" + +latest-version@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" + dependencies: + package-json "^4.0.0" + +lazy-val@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +lexical-scope@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4" + dependencies: + astw "^2.0.0" + +load-bmfont@^1.2.3: + version "1.3.0" + resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.3.0.tgz#bb7e7c710de6bcafcb13cb3b8c81e0c0131ecbc9" + dependencies: + buffer-equal "0.0.1" + mime "^1.3.4" + parse-bmfont-ascii "^1.0.3" + parse-bmfont-binary "^1.0.5" + parse-bmfont-xml "^1.1.0" + xhr "^2.0.1" + xtend "^4.0.0" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.assign@^4.1.0, lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.memoize@~3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" + +lodash@^4.13.1: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lowercase-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-dir@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" + dependencies: + pify "^3.0.0" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +meow@^3.1.0, meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + +mime-types@^2.1.12, mime-types@~2.1.17: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + +mime-types@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-1.0.2.tgz#995ae1392ab8affcbfcb2641dd054e943c0d5dce" + +mime@^1.3.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + +mime@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b" + +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +min-document@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + dependencies: + dom-walk "^0.1.0" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" + dependencies: + minimist "0.0.8" + +mkdirp@0.5.1, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +module-deps@^4.0.8: + version "4.1.1" + resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd" + dependencies: + JSONStream "^1.0.3" + browser-resolve "^1.7.0" + cached-path-relative "^1.0.0" + concat-stream "~1.5.0" + defined "^1.0.0" + detective "^4.0.0" + duplexer2 "^0.1.2" + inherits "^2.0.1" + parents "^1.0.0" + readable-stream "^2.0.2" + resolve "^1.1.3" + stream-combiner2 "^1.1.1" + subarg "^1.0.0" + through2 "^2.0.0" + xtend "^4.0.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +node-forge@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" + +node-uuid@~1.4.0: + version "1.4.8" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +nugget@^2.0.0, nugget@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0" + dependencies: + debug "^2.1.3" + minimist "^1.1.0" + pretty-bytes "^1.0.2" + progress-stream "^1.1.0" + request "^2.45.0" + single-line-log "^1.1.2" + throttleit "0.0.2" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-keys@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +os-browserify@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +package-json@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" + dependencies: + got "^6.7.1" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +pako@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" + +parents@^1.0.0, parents@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" + dependencies: + path-platform "~0.11.15" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-bmfont-ascii@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz#11ac3c3ff58f7c2020ab22769079108d4dfa0285" + +parse-bmfont-binary@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz#d038b476d3e9dd9db1e11a0b0e53a22792b69006" + +parse-bmfont-xml@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/parse-bmfont-xml/-/parse-bmfont-xml-1.1.3.tgz#d6b66a371afd39c5007d9f0eeb262a4f2cce7b7c" + dependencies: + xml-parse-from-string "^1.0.0" + xml2js "^0.4.5" + +parse-color@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619" + dependencies: + color-convert "~0.5.0" + +parse-headers@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.1.tgz#6ae83a7aa25a9d9b700acc28698cd1f1ed7e9536" + dependencies: + for-each "^0.3.2" + trim "0.0.1" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +path-browserify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-exists@^2.0.0, path-exists@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-platform@~0.11.15: + version "0.11.15" + resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.14" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +phantomjs-prebuilt@^2.1.10: + version "2.1.16" + resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" + dependencies: + es6-promise "^4.0.3" + extract-zip "^1.6.5" + fs-extra "^1.0.0" + hasha "^2.2.0" + kew "^0.7.0" + progress "^1.1.8" + request "^2.81.0" + request-progress "^2.0.1" + which "^1.2.10" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pixelmatch@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854" + dependencies: + pngjs "^3.0.0" + +pkginfo@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65" + +plist@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025" + dependencies: + base64-js "1.2.0" + xmlbuilder "8.2.2" + xmldom "0.1.x" + +pn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + +pngjs@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.1.tgz#8e14e6679ee7424b544334c3b2d21cea6d8c209a" + +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +pretty-bytes@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" + dependencies: + get-stdin "^4.0.1" + meow "^3.1.0" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +process@~0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +process@~0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" + +progress-stream@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" + dependencies: + speedometer "~0.1.2" + through2 "~0.2.3" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.3.2, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.2.tgz#19b57ff24dc2a99ce1f8bdf6afcda59f8ef61f88" + +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +querystring-es3@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +raven-js@^3.17.0: + version "3.22.1" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.1.tgz#1117f00dfefaa427ef6e1a7d50bbb1fb998a24da" + +rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.4.tgz#a0f606caae2a3b862bbd0ef85482c0125b315fa3" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-chunk@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-1.0.1.tgz#5f68cab307e663f19993527d9b589cace4661194" + +read-config-file@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-config-file/-/read-config-file-3.0.0.tgz#771def5184a7f76abaf6b2c82f20cb983775b8ea" + dependencies: + ajv "^6.1.1" + ajv-keywords "^3.1.0" + bluebird-lst "^1.0.5" + dotenv "^5.0.0" + dotenv-expand "^4.0.1" + fs-extra-p "^4.5.0" + js-yaml "^3.10.0" + json5 "^0.5.1" + lazy-val "^1.0.3" + +read-only-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" + dependencies: + readable-stream "^2.0.2" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +registry-auth-token@^3.0.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.1.tgz#fb0d3289ee0d9ada2cbb52af5dfe66cb070d3006" + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + dependencies: + rc "^1.0.1" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request-lite@^2.40.1: + version "2.40.1" + resolved "https://registry.yarnpkg.com/request-lite/-/request-lite-2.40.1.tgz#08a8a151eaa84117e01d3e14fe765e8529b3f3df" + dependencies: + caseless "~0.6.0" + forever-agent "~0.5.0" + json-stringify-safe "~5.0.0" + mime-types "~1.0.1" + node-uuid "~1.4.0" + qs "~1.2.0" + +request-progress@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" + dependencies: + throttleit "^1.0.0" + +request@^2.45.0, request@^2.65.0, request@^2.81.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +resolve@^1.1.3, resolve@^1.1.4: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + +rimraf@^2.2.8: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sanitize-filename@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a" + dependencies: + truncate-utf8-bytes "^1.0.0" + +sax@>=0.6.0, sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + dependencies: + semver "^5.0.3" + +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: + version "2.4.10" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shasum@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" + dependencies: + json-stable-stringify "~0.0.0" + sha.js "~2.4.4" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shell-quote@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +single-line-log@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364" + dependencies: + string-width "^1.0.1" + +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + +source-map-support@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.3.tgz#2b3d5fff298cfa4d1afd7d4352d569e9a0158e76" + dependencies: + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +source-map@~0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +speedometer@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +stat-mode@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.2.2.tgz#e6c80b623123d7d80cf132ce538f346289072502" + +stream-browserify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner2@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + +stream-http@^2.0.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.3" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-splicer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.2" + +stream-to-buffer@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stream-to-buffer/-/stream-to-buffer-0.1.0.tgz#26799d903ab2025c9bd550ac47171b00f8dd80a9" + dependencies: + stream-to "~0.2.0" + +stream-to@~0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stream-to/-/stream-to-0.2.2.tgz#84306098d85fdb990b9fa300b1b3ccf55e8ef01d" + +string-similarity@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-1.1.0.tgz#3c66498858a465ec7c40c7d81739bbd995904914" + dependencies: + lodash "^4.13.1" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.0.0, string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + dependencies: + minimist "^1.1.0" + +sumchecker@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-1.3.1.tgz#79bb3b4456dd04f18ebdbc0d703a1d1daec5105d" + dependencies: + debug "^2.2.0" + es6-promise "^4.0.5" + +sumchecker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e" + dependencies: + debug "^2.2.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +supports-color@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0" + dependencies: + has-flag "^3.0.0" + +svg2png@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/svg2png/-/svg2png-4.1.0.tgz#68e85fc9d0784dc041f97d2a28815405acd56217" + dependencies: + file-url "^1.1.0" + phantomjs-prebuilt "^2.1.10" + pn "^1.0.0" + yargs "^5.0.0" + +syntax-error@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.3.0.tgz#1ed9266c4d40be75dc55bf9bb1cb77062bb96ca1" + dependencies: + acorn "^4.0.3" + +temp-file@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.1.1.tgz#8823649aa4e8a6e419eb71b601a2e4d472b0f24f" + dependencies: + async-exit-hook "^2.0.1" + bluebird-lst "^1.0.5" + fs-extra-p "^4.5.0" + lazy-val "^1.0.3" + +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + dependencies: + execa "^0.7.0" + +throttleit@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +through2@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f" + dependencies: + readable-stream "~1.1.9" + xtend "~2.1.1" + +"through@>=2.2.7 <3": + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + +timers-browserify@^1.0.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" + dependencies: + process "~0.11.0" + +tinycolor2@^1.1.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +tough-cookie@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + dependencies: + punycode "^1.4.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + dependencies: + utf8-byte-length "^1.0.1" + +tty-browserify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typedarray@^0.0.6, typedarray@~0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +umd@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.1.tgz#8ae556e11011f63c2596708a8837259f01b3d60e" + +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + +update-notifier@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451" + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + dependencies: + prepend-http "^1.0.1" + +url-regex@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-3.2.0.tgz#dbad1e0c9e29e105dd0b1f09f6862f7fdb482724" + dependencies: + ip-regex "^1.0.1" + +url@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@~0.10.1: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +uuid@^3.0.0, uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@~0.0.1: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.10, which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +widest-line@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.0.tgz#0142a4e8a243f8882c0233aa0e0281aa76152273" + dependencies: + string-width "^2.1.1" + +window-size@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + +xhr@^2.0.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.4.1.tgz#ba982cced205ae5eec387169ac9dc77ca4853d38" + dependencies: + global "~4.3.0" + is-function "^1.0.1" + parse-headers "^2.0.0" + xtend "^4.0.0" + +xml-parse-from-string@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" + +xml2js@^0.4.5: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" + +xmlbuilder@~9.0.1: + version "9.0.4" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" + +xmldom@0.1.x: + version "0.1.27" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +xtend@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" + dependencies: + object-keys "~0.4.0" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-3.2.0.tgz#5081355d19d9d0c8c5d81ada908cb4e6d186664f" + dependencies: + camelcase "^3.0.0" + lodash.assign "^4.1.0" + +yargs-parser@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" + dependencies: + camelcase "^4.1.0" + +yargs@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b" + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^9.0.2" + +yargs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-5.0.0.tgz#3355144977d05757dbb86d6e38ec056123b3a66e" + dependencies: + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + lodash.assign "^4.2.0" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + window-size "^0.2.0" + y18n "^3.2.1" + yargs-parser "^3.2.0" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + dependencies: + fd-slicer "~1.0.1" diff --git a/src/shadowbox/README.md b/src/shadowbox/README.md new file mode 100644 index 000000000..7f6c2f7a1 --- /dev/null +++ b/src/shadowbox/README.md @@ -0,0 +1,173 @@ +# Introduction + +Shadowbox is a server set up that runs a user management API and starts Shadowsocks +instances on demand. + +It aims to make it as easy as possible to set up and share a Shadowsocks server. It's +used by the Outline server launcher. + +## Requirements + +1. [Node](https://nodejs.org/en/download/) +1. [Yarn](https://yarnpkg.com/en/docs/install) +1. [Docker 1.13+](https://docs.docker.com/engine/installation/) +1. [docker-compose 1.11+](https://docs.docker.com/compose/install/) + +Run `docker info` and make sure `Storage Driver` is `devicemapper`. If it is not, you can override it by +editting `/etc/default/docker` or by passing another storage driver in the daemon commandline: +``` +sudo dockerd --storage-driver=devicemapper +``` + +## Development + +Set up +``` +yarn shadowbox_install +``` + +Start the server +``` +yarn do shadowbox/server/run +``` + +If you just want to build the server: +``` +yarn do shadowbox/server/build +``` + +The output will be at `build/shadowbox/app`. + + +## Queries + +List users +``` +curl --insecure https://localhost:8081/TestApiPrefix/access-keys/ +``` + +Create a user +``` +curl --insecure -X POST https://localhost:8081/TestApiPrefix/access-keys +``` + +Remove a user +``` +curl --insecure -X DELETE https://localhost:8081/TestApiPrefix/access-keys/2 +``` + + +
+ +Example output + + +``` +$ curl --insecure https://localhost:8081/TestApiPrefix/access-keys +{"users":[]} + +$ curl --insecure -X POST https://localhost:8081/TestApiPrefix/access-keys +{"id":"0","password":"Nm9wtQkPeshs","port":34180} + +$ curl --insecure -X POST https://localhost:8081/TestApiPrefix/access-keys +{"id":"1","password":"32mW3jhuhBGv","port":55625} + +$ curl --insecure -X POST https://localhost:8081/TestApiPrefix/access-keys +{"id":"2","password":"jFOKrJcpbgIb","port":15884} + +$ curl --insecure https://localhost:8081/TestApiPrefix/access-keys +{"users":[{"id":"0","password":"Nm9wtQkPeshs","port":34180},{"id":"1","password":"32mW3jhuhBGv","port":55625},{"id":"2","password":"jFOKrJcpbgIb","port":15884}]} + +$ curl --insecure -X DELETE https://localhost:8081/TestApiPrefix/access-keys/0 -v +* Hostname was NOT found in DNS cache +* Trying ::1... +* Connected to localhost (::1) port 8081 (#0) +> DELETE /access-keys/0 HTTP/1.1 +> User-Agent: curl/7.35.0 +> Host: localhost:8081 +> Accept: */* +> +< HTTP/1.1 204 No Content +< Date: Fri, 03 Feb 2017 22:46:39 GMT +< Connection: keep-alive +< +* Connection #0 to host localhost left intact + +$ curl --insecure https://localhost:8081/TestApiPrefix/access-keys +{"users":[{"id":"1","password":"32mW3jhuhBGv","port":55625},{"id":"2","password":"jFOKrJcpbgIb","port":15884}]} +``` +
+ +## Docker Deployment + +**NOTE**: This does not currently work in Docker on Mac due to use of +`--host=net` and integrity checks failing. For now, please see the Manual +testing section below. + +### With docker command + +Build Docker image: +``` +yarn do shadowbox/docker/build +``` + +Run server: +``` +yarn do shadowbox/docker/run +``` + +Debug image: +``` +docker run --rm -it --entrypoint=sh quay.io/outline/shadowbox +``` + +or +``` +docker exec -it shadowbox sh +``` + + +Delete dangling images: +``` +docker rmi $(docker images -f dangling=true -q) +``` + +## Testing + +### Manual + +After building a docker image with some local changes, +upload it to your favorite registry +(e.g. Docker Hub, quay.io, etc.). + +Then set your `SB_IMAGE` environment variable to point to the image you just +uploaded (e.g. `export SB_IMAGE=yourdockerhubusername/shadowbox`) and +run `yarn do server_manager/electron_app/run` and your droplet should be created with your +modified image. + +### Automated + +To run the integration test: +``` +yarn do shadowbox/integration_test/run +``` + +This will set up three containers and two networks: +``` +client <-> shadowbox <-> target +``` + +`client` can only access `target` via shadowbox. We create a user on `shadowbox` then connect using the Shadowsocks client. + +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: +``` +yarn 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 SHADOWBOX_IMAGE=$USER/shadowbox yarn do shadowbox/integration_test/run +``` + +You can add tags if you need different versions in different clients. diff --git a/src/shadowbox/docker/Dockerfile b/src/shadowbox/docker/Dockerfile new file mode 100644 index 000000000..26b37cfb7 --- /dev/null +++ b/src/shadowbox/docker/Dockerfile @@ -0,0 +1,66 @@ +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See versions at https://hub.docker.com/_/node/ +FROM node:8.10.0-alpine + +# Versions can be found at https://github.com/shadowsocks/shadowsocks-libev/releases +ARG SS_VERSION=3.1.3 + +# Save metadata on the software versions we are using. +LABEL shadowbox.node_version=8.10.0 +LABEL shadowbox.shadowsocks_version="${SS_VERSION}" + +ARG GITHUB_RELEASE +LABEL shadowbox.github.release="${GITHUB_RELEASE}" + +# lsof for Shadowbox, curl for detecting our public IP. +RUN apk add --no-cache lsof curl + +COPY src/shadowbox/scripts scripts/ +RUN sh ./scripts/install_shadowsocks.sh $SS_VERSION + +WORKDIR /root/shadowbox + +# Install management service +COPY build/shadowbox/app app/ +COPY src/shadowbox/package.json . +COPY src/shadowbox/yarn.lock . +RUN yarn install --prod + +# Create default state directory. +RUN mkdir -p /root/shadowbox/persisted-state + +# The maximum number of files that can be opened by ss-server greatly +# influence on performance, as described here: +# https://shadowsocks.org/en/config/advanced.html +# +# The steps described in that page do *not* work for processes running +# under Docker, at least on modern Debian/Ubuntu-like systems whose init +# daemons allow per-service limits and ignore completely +# /etc/security/limits.conf. On those systems, the Shadowbox container +# will, by default, inherit the limits configured for the Docker service: +# https://docs.docker.com/engine/reference/commandline/run/#set-ulimits-in-container-ulimit +# +# Interestingly, we observed poor performance with large values such as 524288 +# and 1048576, the default values in recent releases of Ubuntu. Our +# non-exhaustive testing indicates a performance cliff for Outline after values +# around 270k; to stay well bekow of this cliff we've semi-handwaved-ly settled +# upon a limit of 32k files. +# +# By configuring this limit via the Docker image's CMD line we are able to +# propagate the value to current users and change it in the future via our +# normal Watchtower-based update mechanism (this is *not* the case with +# docker run --ulimit) without changing the host system's configuration. +CMD SB_PUBLIC_IP=${SB_PUBLIC_IP:-$(curl https://ipinfo.io/ip)} SB_METRICS_URL=${SB_METRICS_URL:-https://metrics-prod.uproxy.org} sh -c 'ulimit -n 32768; node app/server/main.js' diff --git a/src/shadowbox/docker/build_action.sh b/src/shadowbox/docker/build_action.sh new file mode 100755 index 000000000..6f7b3c6b6 --- /dev/null +++ b/src/shadowbox/docker/build_action.sh @@ -0,0 +1,20 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +do_action shadowbox/server/build + +export DOCKER_CONTENT_TRUST=${DOCKER_CONTENT_TRUST:-1} +docker build --force-rm --build-arg GITHUB_RELEASE="${TRAVIS_TAG:-none}" -t outline/shadowbox $ROOT_DIR -f src/shadowbox/docker/Dockerfile diff --git a/src/shadowbox/docker/run_action.sh b/src/shadowbox/docker/run_action.sh new file mode 100755 index 000000000..6431b607b --- /dev/null +++ b/src/shadowbox/docker/run_action.sh @@ -0,0 +1,32 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +touch /tmp/config.json +source $ROOT_DIR/src/shadowbox/scripts/make_certificate.sh + +# TODO: mount a folder rather than individual files. +declare -a docker_bindings=( + -v /tmp/config.json:/root/shadowbox/shadowbox_config.json + -v /tmp/stats.json:/root/shadowbox/shadowbox_stats.json + -v ${SB_CERTIFICATE_FILE}:${SB_CERTIFICATE_FILE} + -v ${SB_PRIVATE_KEY_FILE}:${SB_PRIVATE_KEY_FILE} + -e "LOG_LEVEL=${LOG_LEVEL:-debug}" + -e SB_API_PREFIX=TestApiPrefix + -e SB_CERTIFICATE_FILE + -e SB_PRIVATE_KEY_FILE +) +export DOCKER_CONTENT_TRUST=${DOCKER_CONTENT_TRUST:-1} +docker run --rm -it --network=host --name shadowbox "${docker_bindings[@]}" outline/shadowbox diff --git a/src/shadowbox/infrastructure/file_read.ts b/src/shadowbox/infrastructure/file_read.ts new file mode 100644 index 000000000..08aa657fc --- /dev/null +++ b/src/shadowbox/infrastructure/file_read.ts @@ -0,0 +1,30 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as fs from 'fs'; + +// Reads a text file if it exists, or null if the file is not found. +// Throws any other error except file not found. +export function readFileIfExists(filename: string): string { + try { + return fs.readFileSync(filename, {encoding: 'utf8'}) || null; + } catch (err) { + // err.code will be 'ENOENT' if the file is not found, this is expected. + if (err.code === 'ENOENT') { + return null; + } else { + throw err; + } + } +} diff --git a/src/shadowbox/infrastructure/filesystem_text_file.ts b/src/shadowbox/infrastructure/filesystem_text_file.ts new file mode 100644 index 000000000..73e08a111 --- /dev/null +++ b/src/shadowbox/infrastructure/filesystem_text_file.ts @@ -0,0 +1,30 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as fs from 'fs'; +import { TextFile } from '../model/text_file'; + +// Reads a text file if it exists, or null if the file is not found. +// Throws any other error except file not found. +export class FilesystemTextFile implements TextFile { + constructor(private readonly filename: string) {} + + readFileSync(): string { + return fs.readFileSync(this.filename, {encoding: 'utf8'}); + } + + writeFileSync(text: string): void { + fs.writeFileSync(this.filename, text, {encoding: 'utf8'}); + } +} diff --git a/src/shadowbox/infrastructure/follow_redirects.ts b/src/shadowbox/infrastructure/follow_redirects.ts new file mode 100644 index 000000000..730828779 --- /dev/null +++ b/src/shadowbox/infrastructure/follow_redirects.ts @@ -0,0 +1,54 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as request from 'request-lite'; + +interface Response { + statusCode: number; + headers: { location?: string }; +} + +interface Options { + url: string; + method?: string; + headers?: {}; + body?: string; + followRedirect?: boolean; + followAllRedirects?: boolean; +} + +// Makes an http(s) request, and follows any redirect with the same request +// without changing the request method or body. This is used because typical +// http(s) clients follow redirects for POST/PUT/DELETE requests by changing the +// method to GET and removing the request body. Function signature matches the +// request/request-lite function. +export function requestFollowRedirectsWithSameMethodAndBody( + options: Options, callback: (error: Error, response: Response, body: string) => void): void { + // Make a copy of options to modify parameters. + const modifiedOptions = Object.assign({}, options); + modifiedOptions.followAllRedirects = false; + modifiedOptions.followRedirect = false; + request(modifiedOptions, (error, response, body) => { + if (!error && + response.statusCode >= 300 && response.statusCode < 400 && + response.headers.location) { + // Request has been redirected, try again at the new location. + modifiedOptions.url = response.headers.location; + return requestFollowRedirectsWithSameMethodAndBody(modifiedOptions, callback); + } else { + // Request has not been redirected, invoke callback. + return callback(error, response, body); + } + }); +} diff --git a/src/shadowbox/infrastructure/get_port.spec.ts b/src/shadowbox/infrastructure/get_port.spec.ts new file mode 100644 index 000000000..2703f79c6 --- /dev/null +++ b/src/shadowbox/infrastructure/get_port.spec.ts @@ -0,0 +1,63 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as get_port from './get_port'; + +const USED_PORT_1 = 2001; +const USED_PORT_2 = 2002; +const USED_PORT_3 = 2003; +const UNUSED_PORT_1 = 3000; + +describe('getRandomUnusedPort', () => { + it('Tries until it finds an unused port', (done) => { + let generateCount = 0; + function generatePort(): number { + return [USED_PORT_1, USED_PORT_2, USED_PORT_3, UNUSED_PORT_1][generateCount++]; + } + // This test replaces the default lsof check for used ports, as lsof may + // return different used ports on each machine. To test with the real lsof + // code: + // 1. run "lsof -P | grep LISTEN" to see which ports are in use + // 2. change the USED_PORT_1..3 variables above to use those port numbers, + // and be sure that UNUSED_PORT_1 is in fact unused on your machine. + // 3. remove the "isPortUsed" parameter from this call to getRandomUnusedPort. + get_port.getRandomUnusedPort(new Set(), generatePort, isPortUsed).then((port) => { + expect(port).toEqual(UNUSED_PORT_1); + done(); + }); + }); + + it('Rejects if it cannot find an unused port', (done) => { + get_port + .getRandomUnusedPort( + new Set(), get_port.getRandomPortOver1023, + (port: number) => Promise.resolve(true)) // always return port in use + .catch(done); + }); + + it('Does not pick from reserved ports', (done) => { + const RESERVED_PORT = 123; + const MAX_RETRIES = 1; + get_port + .getRandomUnusedPort( + new Set([RESERVED_PORT]), () => RESERVED_PORT, (port: number) => Promise.resolve(false), + MAX_RETRIES) + .catch(done); + }); +}); + +function isPortUsed(port: number): Promise { + const isUsed = port === USED_PORT_1 || port === USED_PORT_2 || port === USED_PORT_3; + return Promise.resolve(isUsed); +} diff --git a/src/shadowbox/infrastructure/get_port.ts b/src/shadowbox/infrastructure/get_port.ts new file mode 100644 index 000000000..6541301de --- /dev/null +++ b/src/shadowbox/infrastructure/get_port.ts @@ -0,0 +1,57 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as child_process from 'child_process'; + +const MAX_PORT = 65535; +const MIN_PORT = 1024; + +export function getRandomUnusedPort( + reservedPorts: Set, generatePort: (() => number) = getRandomPortOver1023, + isPortUsed: ((port: number) => Promise) = isPortUsedLsof, + maxRetries = MAX_PORT): Promise { + // TODO: consider using a set of available ports, so we don't randomly + // try the same port multiple times. + const port = generatePort(); + return isPortUsed(port).then((isUsed) => { + if (!isUsed && !reservedPorts.has(port)) { + return Promise.resolve(port); + } else if (maxRetries === 0) { + return Promise.reject(new Error('Could not find available port')); + } + return getRandomUnusedPort(reservedPorts, generatePort, isPortUsed, maxRetries - 1); + }); +} + +export function getRandomPortOver1023() { + return Math.floor(Math.random() * (MAX_PORT + 1 - MIN_PORT) + MIN_PORT); +} + +function isPortUsedLsof(port: number): Promise { + return new Promise((fulfill, reject) => { + const cmd = `lsof -P -i:${port} | grep LISTEN`; + child_process.exec(cmd, (error: child_process.ExecError, stdout: string, stderr: string) => { + if (error && error.code === 1) { + // lsof will return error code 1 if nothing is found. + fulfill(false); + } else if (stdout.trim() || stderr.trim()) { + // Anything written to stdout or stderr indicates that this port + // is in use. + fulfill(true); + } else { + fulfill(false); + } + }); + }); +} diff --git a/src/shadowbox/integration_test/client/Dockerfile b/src/shadowbox/integration_test/client/Dockerfile new file mode 100644 index 000000000..da96a3d9d --- /dev/null +++ b/src/shadowbox/integration_test/client/Dockerfile @@ -0,0 +1,18 @@ +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This ensures that the client is up to date with the server. +FROM outline/shadowbox + +ENTRYPOINT [ "sh" ] diff --git a/src/shadowbox/integration_test/docker-compose.yml b/src/shadowbox/integration_test/docker-compose.yml new file mode 100644 index 000000000..c3805d619 --- /dev/null +++ b/src/shadowbox/integration_test/docker-compose.yml @@ -0,0 +1,60 @@ +version: "2.1" + +networks: + open: + censored: + +services: + target: + build: + context: ./target + ports: + - "10080:80" + networks: + - open + # The python SimpleHTTPServer doesn't quit with SIGTERM. + stop_signal: SIGKILL + + shadowbox: + image: ${SHADOWBOX_IMAGE:-outline/shadowbox} + environment: + - SB_PUBLIC_IP=shadowbox + - SB_API_PORT=443 + - SB_API_PREFIX=${SB_API_PREFIX} + - LOG_LEVEL=debug + - SB_CERTIFICATE_FILE=/root/shadowbox/test.crt + - SB_PRIVATE_KEY_FILE=/root/shadowbox/test.key + ports: + - "20443:443" + links: + - target + networks: + - open + - censored + volumes: + - ${SB_CERTIFICATE_FILE}:/root/shadowbox/test.crt + - ${SB_PRIVATE_KEY_FILE}:/root/shadowbox/test.key + # The user management service doesn't quit with SIGTERM + stop_signal: SIGKILL + + client: + build: + context: ./client + ports: + - "30555:555" + # Keep the container running + stdin_open: true + tty: true + links: + - shadowbox + networks: + - censored + + util: + build: + context: ./util + networks: + - open + # Keep the container running + stdin_open: true + tty: true diff --git a/src/shadowbox/integration_test/run_action.sh b/src/shadowbox/integration_test/run_action.sh new file mode 100755 index 000000000..64b0db21f --- /dev/null +++ b/src/shadowbox/integration_test/run_action.sh @@ -0,0 +1,20 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +do_action shadowbox/docker/build + +cd src/shadowbox/integration_test +./test.sh diff --git a/src/shadowbox/integration_test/target/Dockerfile b/src/shadowbox/integration_test/target/Dockerfile new file mode 100644 index 000000000..b72df9523 --- /dev/null +++ b/src/shadowbox/integration_test/target/Dockerfile @@ -0,0 +1,17 @@ +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM gcr.io/distroless/python3 +COPY index.html . +ENTRYPOINT ["python", "-m", "http.server", "80"] \ No newline at end of file diff --git a/src/shadowbox/integration_test/target/index.html b/src/shadowbox/integration_test/target/index.html new file mode 100644 index 000000000..03e16be50 --- /dev/null +++ b/src/shadowbox/integration_test/target/index.html @@ -0,0 +1 @@ +TARGET PAGE CONTENT diff --git a/src/shadowbox/integration_test/test.sh b/src/shadowbox/integration_test/test.sh new file mode 100755 index 000000000..ba8e60d00 --- /dev/null +++ b/src/shadowbox/integration_test/test.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Shadowbox Integration Test +# +# This test verifies that a client can access a target in a different network via a shadowbox node. +# +# Architecture: +# +# +--------+ +-----------+ +--------+ +# | Client | --> | Shadowbox | --> | Target | +# +--------+ +-----------+ +--------+ +# +# Each node runs on a different Docker container. + +export DOCKER_CONTENT_TRUST=${DOCKER_CONTENT_TRUST:-1} + +readonly OUTPUT_DIR=$(mktemp -d) +# TODO(fortuna): Make it possible to run multiple tests in parallel by adding a +# run id to the container names. +readonly TARGET_CONTAINER=integrationtest_target_1 +readonly SHADOWBOX_CONTAINER=integrationtest_shadowbox_1 +readonly CLIENT_CONTAINER=integrationtest_client_1 +readonly UTIL_CONTAINER=integrationtest_util_1 +echo Test output at $OUTPUT_DIR +# Set DEBUG=1 to not kill the stack when the test is finished so you can query +# the containers. +declare -ir DEBUG=${DEBUG:-0} + +# Waits for the input URL to return success. +function wait_for_resource() { + declare -r URL=$1 + until curl --insecure $URL; do sleep 1; done +} + +# Takes the JSON from a /access-keys POST request and returns the appropriate +# ss-local arguments to connect to that user/instance. +function ss_arguments_for_user() { + declare -r SS_INSTANCE_CIPHER=$(echo $1 | docker exec -i $UTIL_CONTAINER jq -r .method) + declare -r SS_INSTANCE_PASSWORD=$(echo $1 | docker exec -i $UTIL_CONTAINER jq -r .password) + declare -r SS_INSTANCE_PORT=$(echo $1 | docker exec -i $UTIL_CONTAINER jq .port) + echo "-p $SS_INSTANCE_PORT -k $SS_INSTANCE_PASSWORD -m $SS_INSTANCE_CIPHER -u" +} + +# Runs curl on the client container. +function client_curl() { + docker exec $CLIENT_CONTAINER curl "$@" +} + +# Start a subprocess for trap +( + set -eu + (($DEBUG != 0)) && set -x + + # Make the certificate + source ../scripts/make_certificate.sh + + # Ensure proper shut down on exit if not in debug mode + (($DEBUG != 0)) || trap "docker-compose down" EXIT + + # Sets everything up + export SB_API_PREFIX=TestApiPrefix + docker-compose up --build -d + + # Wait for target to come up. + wait_for_resource localhost:10080 + declare -r TARGET_IP=$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $TARGET_CONTAINER) + + # Verify that the client cannot access or even resolve the target + # Exit code 28 for "Connection timed out". + docker exec $CLIENT_CONTAINER curl --connect-timeout 1 $TARGET_IP && exit 1 || (($? == 28)) + + # Exit code 6 for "Could not resolve host". + docker exec $CLIENT_CONTAINER curl --connect-timeout 1 http://target && exit 1 || (($? == 6)) + + # Wait for shadowbox to come up. + wait_for_resource https://localhost:20443/access-keys + # Verify that the shadowbox can access the target + docker exec $SHADOWBOX_CONTAINER wget --spider http://target + + # Create new shadowbox user. + # TODO(bemasc): Verify that the server is using the right certificate + declare -r NEW_USER_JSON=$(client_curl --insecure -X POST https://shadowbox/${SB_API_PREFIX}/access-keys) + [[ ${NEW_USER_JSON} == '{"id":'* ]] || exit 1 + declare -r SS_USER_ARGUMENTS=$(ss_arguments_for_user $NEW_USER_JSON) + + # Start Shadowsocks client and wait for it to be ready + declare -r LOCAL_SOCKS_PORT=5555 + docker exec -d $CLIENT_CONTAINER \ + ss-local -l $LOCAL_SOCKS_PORT -s shadowbox $SS_USER_ARGUMENTS -v + while ! docker exec $CLIENT_CONTAINER nc -z localhost $LOCAL_SOCKS_PORT; do + sleep 0.1 + done + + # Verify we can retrieve the target by IP. + client_curl -x socks5h://localhost:$LOCAL_SOCKS_PORT $TARGET_IP > $OUTPUT_DIR/actual.html + diff $OUTPUT_DIR/actual.html target/index.html + + # Verify we can retrieve the target using the system nameservers. + client_curl -x socks5h://localhost:$LOCAL_SOCKS_PORT http://target > $OUTPUT_DIR/actual.html + diff $OUTPUT_DIR/actual.html target/index.html + + # TODO(fortuna): Verify UDP requests. +) diff --git a/src/shadowbox/integration_test/util/Dockerfile b/src/shadowbox/integration_test/util/Dockerfile new file mode 100644 index 000000000..29ca1758b --- /dev/null +++ b/src/shadowbox/integration_test/util/Dockerfile @@ -0,0 +1,17 @@ +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM alpine:3.5 +RUN apk add --no-cache jq +ENTRYPOINT [ "sh" ] diff --git a/src/shadowbox/model/access_key.ts b/src/shadowbox/model/access_key.ts new file mode 100644 index 000000000..d56e13733 --- /dev/null +++ b/src/shadowbox/model/access_key.ts @@ -0,0 +1,39 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {ShadowsocksInstance} from './shadowsocks_server'; + +export type AccessKeyId = string; + +export interface AccessKey { + // The unique identifier for this access key. + id: AccessKeyId; + // Admin-controlled, editable name for this access key. + name: string; + rename(name: string): void; + // The Shadowsocks instance being used by this access key. + shadowsocksInstance: ShadowsocksInstance; +} + +export interface AccessKeyRepository { + // Creates a new access key. Parameters are chosen automatically. + createNewAccessKey(): Promise; + // Removes the access key given its id. Returns true if successful. + removeAccessKey(id: AccessKeyId): boolean; + // Lists all existing access keys + listAccessKeys(): IterableIterator; + // Apply the specified update to the specified access key. + // Returns true if successful. + renameAccessKey(id: AccessKeyId, name: string): boolean; +} diff --git a/src/shadowbox/model/metrics.ts b/src/shadowbox/model/metrics.ts new file mode 100644 index 000000000..bf99c7db6 --- /dev/null +++ b/src/shadowbox/model/metrics.ts @@ -0,0 +1,52 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {AccessKeyId} from './access_key'; + +export type LastHourMetricsReadyCallback = (startDatetime: Date, endDatetime: Date, lastHourUserStats: Map) => void; + +// TODO: replace "user" with "access key" in metrics. This may also require changing +// - the metrics server +// - the metrics bigquery tables +// - the persisted metrics format (JSON file). +export interface Stats { + // Record the number of bytes transferred for a user, and include known + // client IP addresses that are connected for that user. If there are >1 + // IP addresses, numBytes is the sum of bytes transferred across all of those + // clients - we do not know the breakdown of how many bytes were transferred + // per IP address, due to limitations of the ss-server. ipAddresses are only + // used for recording which countries clients are connecting from. + recordBytesTransferred(userId: AccessKeyId, metricsUserId: AccessKeyId, numBytes: number, ipAddresses: string[]); + // Get 30 day data usage, broken down by userId. + get30DayByteTransfer(): DataUsageByUser; + // Register callback for hourly metrics report. + onLastHourMetricsReady(callback: LastHourMetricsReadyCallback): void; +} + +export interface PerUserStats { + bytesTransferred: number; + anonymizedIpAddresses: Set; +} + +// Byte transfer stats for the past 30 days, including both inbound and outbound. +// TODO: this is copied at src/model/server.ts. Both copies should +// be kept in sync, until we can find a way to share code between the web_app +// and shadowbox. +export interface DataUsageByUser { + // The userId key should be of type AccessKeyId, however that results in the tsc + // error TS1023: An index signature parameter type must be 'string' or 'number'. + // See https://github.com/Microsoft/TypeScript/issues/2491 + // TODO: rename this to AccessKeyId in a backwards compatible way. + bytesTransferredByUserId: {[userId: string]: number}; +} diff --git a/src/shadowbox/model/shadowsocks_server.ts b/src/shadowbox/model/shadowsocks_server.ts new file mode 100644 index 000000000..6f4a734a4 --- /dev/null +++ b/src/shadowbox/model/shadowsocks_server.ts @@ -0,0 +1,33 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as dgram from 'dgram'; + +export interface ShadowsocksServer { + startInstance( + portNumber: number, password: string, statsSocket: dgram.Socket, + encryptionMethod?: string): Promise; +} + +export interface ShadowsocksInstance { + portNumber: number; + password: string; + encryptionMethod: string; + accessUrl: string; + // Registers a callback to be invoked when the ShadowsocksInstance has + // transferred data (inbound and outbond). bytes is the number of + // bytes transferred since the last callback. + onBytesTransferred(callback: (bytes: number, ipAddresses: string[]) => void); + stop(); +} diff --git a/src/shadowbox/model/text_file.ts b/src/shadowbox/model/text_file.ts new file mode 100644 index 000000000..67b3a85f8 --- /dev/null +++ b/src/shadowbox/model/text_file.ts @@ -0,0 +1,18 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export interface TextFile { + readFileSync(): string; + writeFileSync(text: string): void; +} diff --git a/src/shadowbox/package.json b/src/shadowbox/package.json new file mode 100644 index 000000000..87e1607ed --- /dev/null +++ b/src/shadowbox/package.json @@ -0,0 +1,25 @@ +{ + "name": "outline-server", + "private": true, + "version": "0.1.0", + "description": "Outline server", + "main": "build/server/main.js", + "author": "Outline", + "license": "Apache", + "__COMMENTS__": [ + "Using https:// for ShadowsocksConfig to avoid adding git in the Docker image" + ], + "dependencies": { + "ShadowsocksConfig": "https://github.com/Jigsaw-Code/outline-shadowsocksconfig/archive/v0.0.6.tar.gz", + "ipaddr.js": "^1.4.0", + "randomstring": "^1.1.5", + "request-lite": "^2.40.1", + "restify": "^4.3.0", + "uuid": "^3.1.0" + }, + "devDependencies": { + "@types/node": "^7.0.16", + "@types/randomstring": "^1.1.6", + "@types/restify": "^2.0.41" + } +} diff --git a/src/shadowbox/scripts/install_shadowsocks.sh b/src/shadowbox/scripts/install_shadowsocks.sh new file mode 100755 index 000000000..7fc93af82 --- /dev/null +++ b/src/shadowbox/scripts/install_shadowsocks.sh @@ -0,0 +1,69 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +VERSION=$1 +DOWNLOAD_URL=https://github.com/shadowsocks/shadowsocks-libev/releases/download/v${VERSION}/shadowsocks-libev-${VERSION}.tar.gz +BUILD_DIR=/src/shadowsocks-libev + +set -ex + +# Install runtime dependencies +apk add --no-cache libev c-ares libsodium mbedtls pcre + +# Install build dependencies +apk add --no-cache --virtual BUILD_DEPS \ + autoconf automake build-base gettext-dev libev-dev libsodium-dev libtool \ + linux-headers mbedtls-dev openssl-dev pcre-dev tar c-ares-dev + +# Build. +mkdir -p $BUILD_DIR +cd $BUILD_DIR +curl -sSL $DOWNLOAD_URL | tar xz --strip 1 + +./configure --disable-documentation +make install + +# Other licenses and/or source. +# Alpine does not always include LICENSE files and has no equivalent of +# Debian's "apt source" command. So, we have to manually roll something. +# We'll place licenses in the root folder of the image, named LICENSE.xxx, +# and sources under /src. + +# libev (BSD or GPL2): +# http://software.schmorp.de/pkg/libev.html +curl -sS http://cvs.schmorp.de/libev/LICENSE > /LICENSE.libev + +# c-ares (MIT): +# https://c-ares.haxx.se/ +curl -sS https://c-ares.haxx.se/license.html > /LICENSE.c-ares.html + +# libsodium (ISC): +# https://libsodium.org/ +curl -sS https://raw.githubusercontent.com/jedisct1/libsodium/master/LICENSE > /LICENSE.libsodium + +# mbedtls (Apache): +# https://tls.mbed.org/ +curl -sS https://raw.githubusercontent.com/ARMmbed/mbedtls/development/apache-2.0.txt > /LICENSE.mbedtls + +# pcre (BSD): +# http://www.pcre.org/ +curl -sS http://www.pcre.org/licence.txt > /LICENSE.pcre + +# Clean shadowsocks-libev's folder, leaving the source in the image. +make clean + +# Remove build dependencies. +apk del BUILD_DEPS diff --git a/src/shadowbox/scripts/make_certificate.sh b/src/shadowbox/scripts/make_certificate.sh new file mode 100755 index 000000000..7f86a11c2 --- /dev/null +++ b/src/shadowbox/scripts/make_certificate.sh @@ -0,0 +1,31 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Make a certificate for development purposes, and populate the +# corresponding environment variables. +CERTIFICATE_NAME='/tmp/shadowbox-selfsigned-dev' +export SB_CERTIFICATE_FILE="${CERTIFICATE_NAME}.crt" +export SB_PRIVATE_KEY_FILE="${CERTIFICATE_NAME}.key" +declare -a openssl_req_flags=( + -x509 + -nodes + -days 36500 + -newkey rsa:2048 + -subj '/CN=localhost' + -keyout "${SB_PRIVATE_KEY_FILE}" + -out "${SB_CERTIFICATE_FILE}" +) +openssl req "${openssl_req_flags[@]}" diff --git a/src/shadowbox/server/build_action.sh b/src/shadowbox/server/build_action.sh new file mode 100755 index 000000000..c42291cd4 --- /dev/null +++ b/src/shadowbox/server/build_action.sh @@ -0,0 +1,31 @@ +#!/bin/bash -eux +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +readonly OUT_DIR=$BUILD_DIR/shadowbox +rm -rf $OUT_DIR + +mkdir -p $OUT_DIR/js + +# Compile Typescript +tsc -p src/shadowbox --outDir $OUT_DIR/js + +# Assemble the node app +readonly APP_DIR=$OUT_DIR/app +mkdir -p $APP_DIR +# Copy built code, without test files. +rsync --exclude='**/*.spec.js' --exclude='mocks' -r $OUT_DIR/js/* $APP_DIR/ +# Copy static resources +cp -r $ROOT_DIR/src/shadowbox/package.json $APP_DIR diff --git a/src/shadowbox/server/ip_util.ts b/src/shadowbox/server/ip_util.ts new file mode 100644 index 000000000..70c02b88d --- /dev/null +++ b/src/shadowbox/server/ip_util.ts @@ -0,0 +1,67 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as ipaddr from 'ipaddr.js'; +import * as https from 'https'; + +// Returns anonymized IP address, by setting the last octet to 0 for ipv4, +// or setting the last 80 bits to 0 for ipv6. +// Throws an exception when passed an invalid IP address. +export function anonymizeIp(ip: string): string { + const addr = ipaddr.parse(ip); + if (addr.kind() === 'ipv4') { + // Replace last octet of ipv4 address with a 0. + addr.octets[3] = 0; + return addr.toString(); + } else { + // Replace last 80 bits (5 groups of 4 hex characters) with 0s. + for (let i = 3; i < 8; ++i) { + addr.parts[i] = 0; + } + return addr.toNormalizedString(); + } +} + +// Cache country lookups per IP address. +const countryCache = new Map>(); + +export function lookupCountry(ipAddress: string) : Promise { + if (countryCache.has(ipAddress)) { + // Return cached promise to prevent duplicate lookups. + return countryCache.get(ipAddress); + } + + const promise = new Promise((fulfill, reject) => { + const options = {host: 'freegeoip.io', path: '/json/' + ipAddress}; + https.get(options, (response) => { + let body = ''; + response.on('data', (data) => { + body += data; + }); + response.on('end', () => { + try { + fulfill(JSON.parse(body).country_code); + } catch (err) { + console.error('Error loading country: ', err); + reject(err); + } + }); + }); + }); + + // Prevent multiple lookups of the same country. + countryCache.set(ipAddress, promise); + + return promise; +} diff --git a/src/shadowbox/server/libev_shadowsocks_server.ts b/src/shadowbox/server/libev_shadowsocks_server.ts new file mode 100644 index 000000000..a5826f5cb --- /dev/null +++ b/src/shadowbox/server/libev_shadowsocks_server.ts @@ -0,0 +1,175 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as child_process from 'child_process'; +import * as dgram from 'dgram'; +import * as dns from 'dns'; +import * as events from 'events'; +import {SIP002_URI, makeConfig} from 'ShadowsocksConfig/shadowsocks_config'; +import {ShadowsocksInstance, ShadowsocksServer} from '../model/shadowsocks_server'; + +// Runs shadowsocks-libev server instances. +export class LibevShadowsocksServer implements ShadowsocksServer { + // Old shadowsocks instances had been started with the aes-128-cfb encryption + // method, while new instances specify which method to use. + private DEFAULT_METHOD = 'aes-128-cfb'; + + constructor(private publicAddress: string, private verbose: boolean) {} + + public startInstance( + portNumber: number, password: string, statsSocket: dgram.Socket, + encryptionMethod = this.DEFAULT_METHOD): Promise { + console.info(`Starting server on port ${portNumber}`); + + const statsAddress = statsSocket.address(); + const commandArguments = [ + '-m', encryptionMethod, // Encryption method + '-u', // Allow UDP + '--fast-open', // Allow TCP fast open + '-p', portNumber.toString(), '-k', password, '--manager-address', + `${statsAddress.address}:${statsAddress.port}` + ]; + console.info('starting ss-server with args: ' + commandArguments.join(' ')); + // Add the system DNS servers. + // TODO(fortuna): Add dns.getServers to @types/node. + for (const dnsServer of dns.getServers()) { + commandArguments.push('-d'); + commandArguments.push(dnsServer); + } + if (this.verbose) { + // Make the Shadowsocks output verbose in debug mode. + commandArguments.push('-v'); + } + const childProcess = child_process.spawn('ss-server', commandArguments); + + childProcess.on('error', (error) => { + console.error(`Error spawning server on port ${portNumber}: ${error}`); + }); + // TODO(fortuna): Add restart logic. + childProcess.on('exit', (code, signal) => { + console.info(`Server on port ${portNumber} has exited. Code: ${code}, Signal: ${signal}`); + }); + // TODO(fortuna): Disable this for production. + // TODO(fortuna): Consider saving the output and expose it through the manager service. + childProcess.stdout.pipe(process.stdout); + childProcess.stderr.pipe(process.stderr); + + // Generate a SIP002 access url. + const accessUrl = SIP002_URI.stringify(makeConfig({ + host: this.publicAddress, + port: portNumber, + method: encryptionMethod, + password, + outline: 1, + })); + + return Promise.resolve(new LibevShadowsocksServerInstance( + childProcess, portNumber, password, encryptionMethod, accessUrl, statsSocket)); + } +} + +class LibevShadowsocksServerInstance implements ShadowsocksInstance { + private eventEmitter = new events.EventEmitter(); + private BYTES_TRANSFERRED_EVENT = 'bytesTransferred'; + + constructor( + private childProcess: child_process.ChildProcess, + public portNumber: number, public password, public encryptionMethod: string, + public accessUrl: string, private statsSocket: dgram.Socket) {} + + public stop() { + console.info(`Stopping server on port ${this.portNumber}`); + this.childProcess.kill(); + } + + public onBytesTransferred(callback: (bytes: number, ipAddresses: string[]) => void) { + if (this.eventEmitter.listenerCount(this.BYTES_TRANSFERRED_EVENT) === 0) { + this.createStatsListener(); + } + this.eventEmitter.on(this.BYTES_TRANSFERRED_EVENT, callback); + } + + private createStatsListener() { + let lastBytesTransferred = 0; + this.statsSocket.on('message', (buf: Buffer) => { + let statsMessage; + try { + statsMessage = parseStatsMessage(buf); + } catch (err) { + console.error('error parsing stats: ' + buf + ', ' + err); + return; + } + if (statsMessage.portNumber !== this.portNumber) { + // Ignore stats for other ss-servers, which post to the same statsSocket. + return; + } + const delta = statsMessage.totalBytesTransferred - lastBytesTransferred; + if (delta > 0) { + this.getConnectedClientIPAddresses() + .then((ipAddresses: string[]) => { + lastBytesTransferred = statsMessage.totalBytesTransferred; + this.eventEmitter.emit(this.BYTES_TRANSFERRED_EVENT, delta, ipAddresses); + }) + .catch((err) => { + console.error('Unable to get client IP addresses ', err); + }); + } + }); + } + + private getConnectedClientIPAddresses() :Promise { + const lsofCommand = `lsof -i tcp:${this.portNumber} -n -P -Fn ` + + " | grep '\\->'" + // only look at connection lines (e.g. skips "p8855" and "f60") + " | sed 's/:\\d*$//g'" + // remove p + " | sed 's/n\\S*->//g'" + // remove first part of address + " | sed 's/\\[//g'" + // remove [] (used by ipv6) + " | sed 's/\\]//g'" + // remove ] (used by ipv6) + " | sort | uniq"; // remove duplicates + return this.execCmd(lsofCommand).then((output: string) => { + return output.split('\n'); + }); + } + + private execCmd(cmd: string): Promise { + return new Promise((fulfill, reject) => { + child_process.exec(cmd, (error: child_process.ExecError, stdout: string, stderr: string) => { + if (error) { + reject(error); + } else { + fulfill(stdout.trim()); + } + }); + }); + } +} + +interface StatsMessage { + portNumber: number; + totalBytesTransferred: number; +} + +function parseStatsMessage(buf): StatsMessage { + const jsonString = buf.toString() + .substr('stat: '.length) // remove leading "stat: " + .replace(/\0/g, ''); // remove trailing null terminator + // statObj is in the form {"port#": totalBytesTransferred}, where + // there is always only 1 port# per JSON object. If there are multiple + // ss-servers communicating to the same manager, we will get multiple + // message events. + const statObj = JSON.parse(jsonString); + // Object.keys is used here because node doesn't support Object.values. + const portNumber = parseInt(Object.keys(statObj)[0], 10); + const totalBytesTransferred = statObj[portNumber]; + return {portNumber, totalBytesTransferred}; +} diff --git a/src/shadowbox/server/main.ts b/src/shadowbox/server/main.ts new file mode 100644 index 000000000..f68fe19f9 --- /dev/null +++ b/src/shadowbox/server/main.ts @@ -0,0 +1,161 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as fs from 'fs'; +import * as path from 'path'; +import * as process from 'process'; +import * as restify from 'restify'; + +import * as server_config from './server_config'; +import { createManagedAccessKeyRepository } from './managed_user'; +import { ShadowsocksManagerService } from './manager_service'; +import * as metrics from './metrics'; +import { LibevShadowsocksServer } from './libev_shadowsocks_server'; +import { FilesystemTextFile } from '../infrastructure/filesystem_text_file'; + +const DEFAULT_STATE_DIR = '/root/shadowbox/persisted-state'; + +function main() { + const verbose = process.env.LOG_LEVEL === 'debug'; + + const publicAddress = process.env.SB_PUBLIC_IP; + if (!publicAddress) { + console.error('Need to specify SB_PUBLIC_IP for invite links'); + process.exit(1); + } + console.info('Public address is %s', publicAddress); + + const DEFAULT_PORT = 8081; + const portNumber = Number(process.env.SB_API_PORT || DEFAULT_PORT); + if (isNaN(portNumber)) { + console.error(`Invalid SB_API_PORT: ${process.env.SB_API_PORT}`); + process.exit(1); + } + + const serverConfigFilename = getPersistentFilename('shadowbox_server_config.json'); + const serverConfig = new server_config.ServerConfig(serverConfigFilename, process.env.SB_DEFAULT_SERVER_NAME); + + const shadowsocksServer = new LibevShadowsocksServer(publicAddress, verbose); + + const statsFilename = getPersistentFilename('shadowbox_stats.json'); + const stats = new metrics.PersistentStats(statsFilename); + stats.onLastHourMetricsReady((startDatetime, endDatetime, lastHourUserStats) => { + if (serverConfig.getMetricsEnabled()) { + metrics.getHourlyServerMetricsReport(serverConfig.serverId, startDatetime, endDatetime, lastHourUserStats) + .then((report) => { + if (report) { + metrics.postHourlyServerMetricsReports(report, process.env.SB_METRICS_URL); + } + }); + } + }); + + console.info('Starting...'); + const userConfigFilename = getPersistentFilename('shadowbox_config.json'); + createManagedAccessKeyRepository( + new FilesystemTextFile(userConfigFilename), + shadowsocksServer, + stats).then((managedAccessKeyRepository) => { + const managerService = new ShadowsocksManagerService(managedAccessKeyRepository); + const certificateFilename = process.env.SB_CERTIFICATE_FILE; + const privateKeyFilename = process.env.SB_PRIVATE_KEY_FILE; + + // TODO(bemasc): Remove casts once https://github.com/DefinitelyTyped/DefinitelyTyped/pull/15229 lands + const apiServer = restify.createServer({ + certificate: fs.readFileSync(certificateFilename), + key: fs.readFileSync(privateKeyFilename) + }); + + // Pre-routing handlers + apiServer.pre(restify.CORS()); + + // All routes handlers + const apiPrefix = process.env.SB_API_PREFIX ? `/${process.env.SB_API_PREFIX}` : ''; + apiServer.pre(restify.pre.sanitizePath()); + apiServer.use(restify.jsonp()); + apiServer.use(restify.bodyParser()); + setApiHandlers(apiServer, apiPrefix, managerService, stats, serverConfig); + + // TODO(fortuna): Bind to localhost or unix socket to avoid external access. + apiServer.listen(portNumber, () => { + console.info(`Manager listening at ${apiServer.url}${apiPrefix}`); + }); + }); +} + +function setApiHandlers( + apiServer: restify.Server, + apiPrefix: string, + managerService: ShadowsocksManagerService, + stats: metrics.PersistentStats, + serverConfig: server_config.ServerConfig) { + // Access key service handlers + apiServer.post(`${apiPrefix}/access-keys`, managerService.createNewAccessKey.bind(managerService)); + apiServer.get(`${apiPrefix}/access-keys`, managerService.listAccessKeys.bind(managerService)); + apiServer.del(`${apiPrefix}/access-keys/:id`, managerService.removeAccessKey.bind(managerService)); + apiServer.put(`${apiPrefix}/access-keys/:id/name`, managerService.renameAccessKey.bind(managerService)); + + // Metrics handlers. + apiServer.get(`${apiPrefix}/metrics/transfer`, (req, res, next) => { + res.send(stats.get30DayByteTransfer()); + next(); + }); + apiServer.get(`${apiPrefix}/metrics/enabled`, (req, res, next) => { + res.send({metricsEnabled: serverConfig.getMetricsEnabled()}); + next(); + }); + apiServer.put(`${apiPrefix}/metrics/enabled`, (req, res, next) => { + if (typeof req.params.metricsEnabled === 'boolean') { + serverConfig.setMetricsEnabled(req.params.metricsEnabled); + res.send(204); + } else { + res.send(400); + } + next(); + }); + + // Rename handler. + apiServer.put(`${apiPrefix}/name`, (req, res, next) => { + const name = req.params.name; + if (typeof name !== 'string' || name.length > 100) { + res.send(400); + next(); + return; + } + serverConfig.setName(name); + res.send(204); + next(); + }); + + apiServer.get(`${apiPrefix}/server`, (req, res, next) => { + res.send({ + name: serverConfig.getName(), + serverId: serverConfig.serverId, + metricsEnabled: serverConfig.getMetricsEnabled(), + createdTimestampMs: serverConfig.getCreatedTimestampMs() + }); + next(); + }); +} + +function getPersistentFilename(file: string): string { + const stateDir = process.env.SB_STATE_DIR || DEFAULT_STATE_DIR; + return path.join(stateDir, file); +} + +process.on('unhandledRejection', (error) => { + console.error('unhandledRejection', error); +}); + +main(); diff --git a/src/shadowbox/server/managed_user.spec.ts b/src/shadowbox/server/managed_user.spec.ts new file mode 100644 index 000000000..d57143fa1 --- /dev/null +++ b/src/shadowbox/server/managed_user.spec.ts @@ -0,0 +1,147 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { createManagedAccessKeyRepository } from './managed_user'; +import { MockShadowsocksServer, MockStats, InMemoryFile } from './mocks/mocks'; +import { AccessKeyRepository } from '../model/access_key'; + +describe('ManagedAccessKeyRepository', () => { + it('Repos with non-existent files are created with no access keys', (done) => { + createRepo(new InMemoryFile(false)).then((repo) => { + expect(countAccessKeys(repo)).toEqual(0); + done(); + }); + }); + + it('Can create new access keys', (done) => { + createRepo(new InMemoryFile(false)).then((repo) => { + repo.createNewAccessKey().then((accessKey) => { + expect(accessKey).toBeDefined(); + done(); + }); + }); + }); + + it('Can remove access keys', (done) => { + createRepo(new InMemoryFile(false)).then((repo) => { + repo.createNewAccessKey().then((accessKey) => { + expect(countAccessKeys(repo)).toEqual(1); + const removeResult = repo.removeAccessKey(accessKey.id); + expect(removeResult).toEqual(true); + expect(countAccessKeys(repo)).toEqual(0); + done(); + }); + }); + }); + + it('removeAccessKey returns false for missing keys', (done) => { + createRepo(new InMemoryFile(false)).then((repo) => { + repo.createNewAccessKey().then((accessKey) => { + expect(countAccessKeys(repo)).toEqual(1); + const removeResult = repo.removeAccessKey('badId'); + expect(removeResult).toEqual(false); + expect(countAccessKeys(repo)).toEqual(1); + done(); + }); + }); + }); + + it('Can rename access keys', (done) => { + createRepo(new InMemoryFile(false)).then((repo) => { + repo.createNewAccessKey().then((accessKey) => { + const NEW_NAME = 'newName'; + const renameResult = repo.renameAccessKey(accessKey.id, NEW_NAME); + expect(renameResult).toEqual(true); + // List keys again and expect to see the NEW_NAME; + const accessKeys = iterToArray(repo.listAccessKeys()); + expect(accessKeys[0].name).toEqual(NEW_NAME); + done(); + }); + }); + }); + + it('renameAccessKey returns false for missing keys', (done) => { + createRepo(new InMemoryFile(false)).then((repo) => { + repo.createNewAccessKey().then((accessKey) => { + const NEW_NAME = 'newName'; + const renameResult = repo.renameAccessKey('badId', NEW_NAME); + expect(renameResult).toEqual(false); + // List keys again and expect to NOT see the NEW_NAME; + const accessKeys = iterToArray(repo.listAccessKeys()); + expect(accessKeys[0].name).not.toEqual(NEW_NAME); + done(); + }); + }); + }); + + it('Repos created with an existing file restore access keys', (done) => { + const accessKeyConfigFile = new InMemoryFile(false); + createRepo(accessKeyConfigFile).then((repo1) => { + // Create 2 new access keys + Promise.all([repo1.createNewAccessKey(), repo1.createNewAccessKey()]).then(() => { + // Create a 2nd repo from the same config file. This simulates what + // might happen after the shadowbox server is restarted. + createRepo(accessKeyConfigFile).then((repo2) => { + // Check that repo1 and repo2 have the same access keys + const repo1Keys = iterToArray(repo1.listAccessKeys()); + const repo2Keys = iterToArray(repo2.listAccessKeys()); + expect(repo1Keys.length).toEqual(2); + expect(repo2Keys.length).toEqual(2); + expect(repo1Keys[0]).toEqual(repo2Keys[0]); + expect(repo1Keys[1]).toEqual(repo2Keys[1]); + done(); + }); + }); + }); + }); + + it('Does not re-use ids when using the same config file', (done) => { + const accessKeyConfigFile = new InMemoryFile(false); + // Create a repo with 1 access key, then delete that access key. + createRepo(accessKeyConfigFile).then((repo1) => { + repo1.createNewAccessKey().then((accessKey1) => { + repo1.removeAccessKey(accessKey1.id); + + // Create a 2nd repo with one access key, and verify that + // it hasn't reused the first access key's ID. + createRepo(accessKeyConfigFile).then((repo2) => { + repo2.createNewAccessKey().then((accessKey2) => { + expect(accessKey1.id).not.toEqual(accessKey2.id); + done(); + }); + }); + }); + }); + }); +}); + +// Convert from an IterableIterator to an Array +function iterToArray(iter: IterableIterator): T[] { + const returnArray = []; + for (const el of iter) { + returnArray.push(el); + } + return returnArray; +} + +function countAccessKeys(repo: AccessKeyRepository) { + return iterToArray(repo.listAccessKeys()).length; +} + +function createRepo(inMemoryFile: InMemoryFile) { + return createManagedAccessKeyRepository( + inMemoryFile, + new MockShadowsocksServer(), + new MockStats()); +} diff --git a/src/shadowbox/server/managed_user.ts b/src/shadowbox/server/managed_user.ts new file mode 100644 index 000000000..6e11f1df5 --- /dev/null +++ b/src/shadowbox/server/managed_user.ts @@ -0,0 +1,244 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as child_process from 'child_process'; +import * as dgram from 'dgram'; +import * as randomstring from 'randomstring'; +import * as uuidv4 from 'uuid/v4'; + +import {getRandomUnusedPort} from '../infrastructure/get_port'; +import {AccessKey, AccessKeyId, AccessKeyRepository} from '../model/access_key'; +import {Stats} from '../model/metrics'; +import {ShadowsocksInstance, ShadowsocksServer} from '../model/shadowsocks_server'; +import {TextFile} from '../model/text_file'; + +// The format as json of access keys in the config file. +interface AccessKeyConfig { + id: AccessKeyId; + metricsId: AccessKeyId; + name: string; + password: string; + port: number; + encryptionMethod?: string; +} + +// The configuration file format as json. +interface ConfigJson { + accessKeys: AccessKeyConfig[]; + // Next AccessKeyId to use. + nextId: number; +} + +// AccessKey implementation that starts and stops a Shadowsocks server. +class ManagedAccessKey implements AccessKey { + constructor(public id: AccessKeyId, public metricsId: AccessKeyId, public name: string, public shadowsocksInstance: ShadowsocksInstance) {} + + public rename(name: string): void { + this.name = name; + } +} + +// Generates a random password for Shadowsocks access keys. +function generatePassword(): string { + return randomstring.generate(12); +} + +function readConfig(configFile: TextFile): ConfigJson { + const EMPTY_CONFIG = {accessKeys: [], nextId: 0} as ConfigJson; + + // Try to read the file from disk. + let configText: string; + try { + configText = configFile.readFileSync(); + } catch (err) { + if (err.code === 'ENOENT') { + // File not found (e.g. this is a new server), return an empty config. + return EMPTY_CONFIG; + } + throw err; + } + + // Ignore if the config file is empty. + if (!configText) { + return EMPTY_CONFIG; + } + + return JSON.parse(configText) as ConfigJson; +} + +export function createManagedAccessKeyRepository( + configFile: TextFile, + shadowsocksServer: ShadowsocksServer, + stats: Stats): Promise { + const repo = new ManagedAccessKeyRepository(configFile, shadowsocksServer, stats); + return repo.init().then(() => { + return repo; + }); +} + +// AccessKeyRepository that keeps its state in a config file and uses ManagedAccessKey +// to start and stop per-access-key Shadowsocks instances. +class ManagedAccessKeyRepository implements AccessKeyRepository { + private accessKeys = new Map(); + // This is the max id + 1 among all access keys. Used to generate unique ids for new access keys. + private nextId = 0; + private NEW_USER_ENCRYPTION_METHOD = 'chacha20-ietf-poly1305'; + private statsSocket: dgram.Socket; + private reservedPorts: Set = new Set(); + + constructor( + private configFile: TextFile, private shadowsocksServer: ShadowsocksServer, + private stats: Stats) { + } + + // Initialize the repository from the config file. + public init(): Promise { + const configJson = readConfig(this.configFile); + const accessKeys = configJson.accessKeys; + this.nextId = configJson.nextId; + + this.reservedPorts = getReservedPorts(accessKeys); + + // Create and save the stats socket. + return createBoundUdpSocket(this.reservedPorts).then((statsSocket) => { + this.statsSocket = statsSocket; + this.reservedPorts.add(statsSocket.address().port); + + // Start an instance for each access key. + const startInstancePromises = []; + for (const accessKeyJson of accessKeys) { + startInstancePromises.push( + this.shadowsocksServer + .startInstance( + accessKeyJson.port, accessKeyJson.password, statsSocket, + accessKeyJson.encryptionMethod) + .then((ssInstance) => { + ssInstance.onBytesTransferred(this.handleBytesTransferred.bind( + this, accessKeyJson.id, accessKeyJson.metricsId)); + const accessKey = new ManagedAccessKey( + accessKeyJson.id, accessKeyJson.metricsId, accessKeyJson.name, ssInstance); + this.accessKeys.set(accessKey.id, accessKey); + const idAsNumber = parseInt(accessKey.id, 10); + })); + } + return Promise.all(startInstancePromises).then(() => { + return Promise.resolve(); + }); + }); + } + + public createNewAccessKey(): Promise { + return getRandomUnusedPort(this.reservedPorts).then((port) => { + return this.shadowsocksServer + .startInstance( + port, generatePassword(), this.statsSocket, this.NEW_USER_ENCRYPTION_METHOD) + .then((ssInstance) => { + this.reservedPorts.add(port); + const id = this.allocateId(); + const metricsId = uuidv4(); + ssInstance.onBytesTransferred(this.handleBytesTransferred.bind(this, id, metricsId)); + const accessKey = new ManagedAccessKey(id, metricsId, '', ssInstance); + this.accessKeys.set(accessKey.id, accessKey); + this.persistState(); + return accessKey; + }); + }); + } + + public removeAccessKey(id: AccessKeyId): boolean { + const accessKey = this.accessKeys.get(id); + if (!accessKey) { + return false; + } + accessKey.shadowsocksInstance.stop(); + this.accessKeys.delete(accessKey.id); + this.persistState(); + return true; + } + + public listAccessKeys(): IterableIterator { + return this.accessKeys.values(); + } + + public renameAccessKey(id: AccessKeyId, name: string): boolean { + const accessKey = this.accessKeys.get(id); + if (!accessKey) { + return false; + } + accessKey.rename(name); + this.persistState(); + return true; + } + + private handleBytesTransferred(accessKeyId: AccessKeyId, metricsId: AccessKeyId, bytesTransferred: number, ipAddresses: string[]) { + this.stats.recordBytesTransferred(accessKeyId, metricsId, bytesTransferred, ipAddresses); + } + + private allocateId(): AccessKeyId { + const allocatedId = this.nextId; + this.nextId += 1; + return allocatedId.toString(); + } + + private serializeState() { + return JSON.stringify({ + accessKeys: Array.from(this.accessKeys.values()).map(managedAccessKeytoJson), + nextId: this.nextId + }); + } + + // Save the repository to the local disk. + // TODO(fortuna): Fix race condition. This can break if there are two modifications in parallel. + // TODO: this method should return an error if it fails to write to disk, + // then this error can be propagated back to the manager via the REST + // API, so users know there was an error and access keys may not be + // persisted. + private persistState() { + const state = this.serializeState(); + console.log('Persisting:', state); + this.configFile.writeFileSync(state); + } +} + +function managedAccessKeytoJson(accessKey: ManagedAccessKey) { + return { + id: accessKey.id, + metricsId: accessKey.metricsId, + name: accessKey.name, + password: accessKey.shadowsocksInstance.password, + port: accessKey.shadowsocksInstance.portNumber, + encryptionMethod: accessKey.shadowsocksInstance.encryptionMethod + }; +} + +// Gets the set of port numbers reserved by the accessKeys. +function getReservedPorts(accessKeys: AccessKeyConfig[]): Set { + const reservedPorts = new Set(); + for (const accessKeyJson of accessKeys) { + reservedPorts.add(accessKeyJson.port); + } + return reservedPorts; +} + +// Creates a bound UDP socket on a random unused port. +function createBoundUdpSocket(reservedPorts: Set): Promise { + const socket = dgram.createSocket('udp4'); + return new Promise((fulfill, reject) => { + getRandomUnusedPort(reservedPorts).then((portNumber) => { + socket.bind(portNumber, 'localhost', () => { + return fulfill(socket); + }); + }); + }); +} diff --git a/src/shadowbox/server/manager_service.spec.ts b/src/shadowbox/server/manager_service.spec.ts new file mode 100644 index 000000000..197f7ccae --- /dev/null +++ b/src/shadowbox/server/manager_service.spec.ts @@ -0,0 +1,160 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ShadowsocksManagerService } from './manager_service'; +import { MockAccessKeyRepository } from './mocks/mocks'; +import { AccessKey, AccessKeyRepository } from '../model/access_key'; + +describe('ShadowsocksManagerService', () => { + // After processing the response callback, we should set + // responseProcessed=true. This is so we can detect that first the response + // callback is invoked, followed by the next (done) callback. + let responseProcessed = false; + beforeEach(() => { + responseProcessed = false; + }); + afterEach(() => { + expect(responseProcessed).toEqual(true); + }); + + it('lists access keys in order', (done) => { + const repo = new MockAccessKeyRepository(); + const service = new ShadowsocksManagerService(repo); + + // Create 2 access keys with names. + Promise.all([ + createNewAccessKeyWithName(repo, 'keyName1'), + createNewAccessKeyWithName(repo, 'keyName2') + ]).then((keys) => { + // Verify that response returns keys in correct order with correct names. + const res = {send: (httpCode, data) => { + expect(httpCode).toEqual(200); + expect(data.accessKeys.length).toEqual(2); + expect(data.accessKeys[0].name).toEqual(keys[0].name); + expect(data.accessKeys[0].id).toEqual(keys[0].id); + expect(data.accessKeys[1].name).toEqual(keys[1].name); + expect(data.accessKeys[1].id).toEqual(keys[1].id); + responseProcessed = true; // required for afterEach to pass. + }}; + service.listAccessKeys({params: {}}, res, done); + }); + }); + + it('creates keys', (done) => { + const repo = new MockAccessKeyRepository(); + const service = new ShadowsocksManagerService(repo); + + // Verify that response returns a key with the expected properties. + const res = {send: (httpCode, data) => { + expect(httpCode).toEqual(201); + const expectedProperties = ['id', 'name', 'password', 'port', 'method', 'accessUrl']; + expect(Object.keys(data).sort()).toEqual(expectedProperties.sort()); + responseProcessed = true; // required for afterEach to pass. + }}; + service.createNewAccessKey({params: {}}, res, done); + }); + + it('removes keys', (done) => { + const repo = new MockAccessKeyRepository(); + const service = new ShadowsocksManagerService(repo); + + // Create 2 access keys with names. + Promise.all([ + createNewAccessKeyWithName(repo, 'keyName1'), + createNewAccessKeyWithName(repo, 'keyName2') + ]).then((keys) => { + const res = {send: (httpCode, data) => { + expect(httpCode).toEqual(204); + // expect that the only remaining key is the 2nd key we created. + expect(getFirstAccessKey(repo).id === keys[1].id); + responseProcessed = true; // required for afterEach to pass. + }}; + // remove the 1st key. + service.removeAccessKey({params: {id: keys[0].id}}, res, done); + }); + }); + + it('renames keys', (done) => { + const repo = new MockAccessKeyRepository(); + const service = new ShadowsocksManagerService(repo); + const OLD_NAME = 'oldName'; + const NEW_NAME = 'newName'; + + createNewAccessKeyWithName(repo, OLD_NAME).then((key) => { + expect(getFirstAccessKey(repo).name === OLD_NAME); + const res = {send: (httpCode, data) => { + expect(httpCode).toEqual(204); + expect(getFirstAccessKey(repo).name === NEW_NAME); + responseProcessed = true; // required for afterEach to pass. + }}; + service.renameAccessKey({params: {id: key.id, name: NEW_NAME}}, res, done); + }); + }); + + it('Rename returns a 500 when the repository throws an exception', (done) => { + const repo = new MockAccessKeyRepository(); + spyOn(repo, 'renameAccessKey').and.throwError('cannot write to disk'); + const service = new ShadowsocksManagerService(repo); + + createNewAccessKeyWithName(repo, 'oldName').then((key) => { + 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. + done(); + }); + }); + }); + + it('Create returns a 500 when the repository throws an exception', (done) => { + const repo = new MockAccessKeyRepository(); + spyOn(repo, 'createNewAccessKey').and.throwError('cannot write to disk'); + const service = new ShadowsocksManagerService(repo); + + const res = {send: (httpCode, data) => {}}; + service.createNewAccessKey({params: {}}, res, (error) => { + expect(error.statusCode).toEqual(500); + responseProcessed = true; // required for afterEach to pass. + done(); + }); + }); + + it('Remove returns a 500 when the repository throws an exception', (done) => { + const repo = new MockAccessKeyRepository(); + spyOn(repo, 'removeAccessKey').and.throwError('cannot write to disk'); + const service = new ShadowsocksManagerService(repo); + + // Create 2 access keys with names. + createNewAccessKeyWithName(repo, 'keyName1').then((key) => { + 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. + done(); + }); + }); + }); +}); + +function getFirstAccessKey(repo: AccessKeyRepository) { + return repo.listAccessKeys().next().value; +} + +function createNewAccessKeyWithName( + repo: AccessKeyRepository, name: string): Promise { + return repo.createNewAccessKey().then((key) => { + key.rename(name); + return key; + }); +} \ No newline at end of file diff --git a/src/shadowbox/server/manager_service.ts b/src/shadowbox/server/manager_service.ts new file mode 100644 index 000000000..cf19e2677 --- /dev/null +++ b/src/shadowbox/server/manager_service.ts @@ -0,0 +1,110 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as restify from 'restify'; + +import { AccessKey, AccessKeyRepository } from '../model/access_key'; + +// Creates a AccessKey response. +function accessKeyToJson(accessKey: AccessKey) { + return { + // The unique identifier of this access key. + id: accessKey.id, + // Admin-controlled, editable name for this access key. + name: accessKey.name, + // Shadowsocks-specific details and credentials. + password: accessKey.shadowsocksInstance.password, + port: accessKey.shadowsocksInstance.portNumber, + method: accessKey.shadowsocksInstance.encryptionMethod, + accessUrl: accessKey.shadowsocksInstance.accessUrl, + }; +} + +// Simplified request and response type interfaces containing only the +// properties we actually use, to make testing easier. +interface RequestParams { + id?: string; + name?: string; +} +interface RequestType { + params: RequestParams; +} +interface ResponseType { + send(code: number, data?: {}): void; +} + +// The ShadowsocksManagerService manages the access keys that can use the server +// as a proxy using Shadowsocks. It runs an instance of the Shadowsocks server +// for each existing access key, with the port and password assigned for that access key. +export class ShadowsocksManagerService { + constructor(private accessKeys: AccessKeyRepository) {} + + // Lists all access keys + public listAccessKeys(req: RequestType, res: ResponseType, next: restify.Next): void { + console.log('listAccessKeys request', req.params); + const response = {accessKeys: [], users: []}; + for (const accessKey of this.accessKeys.listAccessKeys()) { + response.accessKeys.push(accessKeyToJson(accessKey)); + } + console.log('listAccessKeys response', response); + res.send(200, response); + return next(); + } + + // Creates a new access key + public createNewAccessKey(req: RequestType, res: ResponseType, next: restify.Next): void { + try { + console.log('createNewAccessKey request', req.params); + this.accessKeys.createNewAccessKey().then((accessKey) => { + const accessKeyJson = accessKeyToJson(accessKey); + res.send(201, accessKeyJson); + return next(); + }); + } catch (error) { + console.error(error); + return next(new restify.InternalServerError()); + } + } + + // Removes an existing access key + public removeAccessKey(req: RequestType, res: ResponseType, next: restify.Next): void { + try { + console.log('removeAccessKey request', req.params); + const accessKeyId = req.params.id; + if (!this.accessKeys.removeAccessKey(accessKeyId)) { + return next(new restify.NotFoundError(`No access key found with id ${accessKeyId}`)); + } + res.send(204); + return next(); + } catch (error) { + console.error(error); + return next(new restify.InternalServerError()); + } + } + + public renameAccessKey(req: RequestType, res: ResponseType, next: restify.Next): void { + try { + console.log('renameAccessKey request', req.params); + const accessKeyId = req.params.id; + if (!this.accessKeys.renameAccessKey(accessKeyId, req.params.name)) { + return next(new restify.NotFoundError(`No access key found with id ${accessKeyId}`)); + } + res.send(204); + return next(); + } catch (error) { + console.error(error); + return next(new restify.InternalServerError()); + } + } +} diff --git a/src/shadowbox/server/metrics.spec.ts b/src/shadowbox/server/metrics.spec.ts new file mode 100644 index 000000000..1b6247f32 --- /dev/null +++ b/src/shadowbox/server/metrics.spec.ts @@ -0,0 +1,158 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as metrics from './metrics'; +import { PerUserStats } from '../model/metrics'; + +const SERVER_ID = 'serverId'; +const USER_ID_1 = 'userId1'; +const USER_ID_2 = 'userId2'; +const START_DATETIME = new Date(Date.now() - (3600 * 1000)); // 1 hour ago +const END_DATETIME = new Date(Date.now()); +const IP_ADDRESS_IN_US_1 = '45.55.19.0'; +const IP_ADDRESS_IN_US_2 = '192.81.216.0'; +const IP_ADDRESS_IN_GB = '185.86.151.11'; +const IP_ADDRESS_IN_NORTH_KOREA = '175.45.176.0'; +const IP_ADDRESS_IN_CUBA = '152.206.0.0'; + +describe('getHourlyServerMetricsReport', () => { + it('Converts IP addresses to country codes', (done) => { + const lastHourUserStats = new Map(); + lastHourUserStats.set(USER_ID_1, getPerUserStats([IP_ADDRESS_IN_US_1])); + + metrics.getHourlyServerMetricsReport( + SERVER_ID, START_DATETIME, END_DATETIME, lastHourUserStats, stubIpLookup) + .then((report) => { + expect(report.userReports.length).toEqual(1); + expect(report.userReports[0].countries.length).toEqual(1); + expect(report.userReports[0].countries[0]).toEqual('US'); + done(); + }); + }); + it('Supports multiple countries per user report', (done) => { + const lastHourUserStats = new Map(); + lastHourUserStats.set(USER_ID_1, getPerUserStats([IP_ADDRESS_IN_US_1, IP_ADDRESS_IN_GB])); + lastHourUserStats.set(USER_ID_2, getPerUserStats([IP_ADDRESS_IN_US_1])); + + metrics.getHourlyServerMetricsReport( + SERVER_ID, START_DATETIME, END_DATETIME, lastHourUserStats, stubIpLookup) + .then((report) => { + expect(report.userReports.length).toEqual(2); + expect(report.userReports[0].countries.length).toEqual(2); + expect(report.userReports[0].countries[0]).toEqual('US'); + expect(report.userReports[0].countries[1]).toEqual('GB'); + expect(report.userReports[1].countries.length).toEqual(1); + expect(report.userReports[1].countries[0]).toEqual('US'); + done(); + }); + }); + it('Does not include duplicate countries', (done) => { + const lastHourUserStats = new Map(); + lastHourUserStats.set(USER_ID_1, + getPerUserStats([IP_ADDRESS_IN_US_1, IP_ADDRESS_IN_US_2])); + + metrics.getHourlyServerMetricsReport( + SERVER_ID, START_DATETIME, END_DATETIME, lastHourUserStats, stubIpLookup) + .then((report) => { + expect(report.userReports.length).toEqual(1); + expect(report.userReports[0].countries.length).toEqual(1); + expect(report.userReports[0].countries[0]).toEqual('US'); + done(); + }); + }); + it('userReports matches input size for unsanctioned countries', (done) => { + const lastHourUserStats = new Map(); + lastHourUserStats.set(USER_ID_1, + getPerUserStats([IP_ADDRESS_IN_US_1])); + lastHourUserStats.set(USER_ID_2, getPerUserStats([IP_ADDRESS_IN_US_2])); + + metrics.getHourlyServerMetricsReport( + SERVER_ID, START_DATETIME, END_DATETIME, lastHourUserStats, stubIpLookup) + .then((report) => { + expect(report.userReports.length).toEqual(2); + expect(report.userReports[0].countries.length).toEqual(1); + expect(report.userReports[0].countries[0]).toEqual('US'); + expect(report.userReports[1].countries.length).toEqual(1); + expect(report.userReports[1].countries[0]).toEqual('US'); + done(); + }); + }); + it('Filters sanctioned countries from userReports', (done) => { + const lastHourUserStats = new Map(); + lastHourUserStats.set(USER_ID_1, + getPerUserStats([IP_ADDRESS_IN_NORTH_KOREA, IP_ADDRESS_IN_US_1])); + lastHourUserStats.set(USER_ID_2, getPerUserStats([IP_ADDRESS_IN_US_1])); + + metrics.getHourlyServerMetricsReport( + SERVER_ID, START_DATETIME, END_DATETIME, lastHourUserStats, stubIpLookup) + .then((report) => { + expect(report.userReports.length).toEqual(2); + expect(report.userReports[0].countries.length).toEqual(1); + expect(report.userReports[0].countries[0]).toEqual('US'); + expect(report.userReports[1].countries.length).toEqual(1); + expect(report.userReports[1].countries[0]).toEqual('US'); + done(); + }); + }); + it('Removes userReports that contain only sanctioned countries', (done) => { + const lastHourUserStats = new Map(); + lastHourUserStats.set(USER_ID_1, + getPerUserStats([IP_ADDRESS_IN_NORTH_KOREA, IP_ADDRESS_IN_CUBA])); + lastHourUserStats.set(USER_ID_2, getPerUserStats([IP_ADDRESS_IN_US_1])); + + metrics.getHourlyServerMetricsReport( + SERVER_ID, START_DATETIME, END_DATETIME, lastHourUserStats, stubIpLookup) + .then((report) => { + expect(report.userReports.length).toEqual(1); + expect(report.userReports[0].countries.length).toEqual(1); + expect(report.userReports[0].countries[0]).toEqual('US'); + done(); + }); + }); + it('Does not generate any report if all users in sanctioned countries', (done) => { + const lastHourUserStats = new Map(); + lastHourUserStats.set(USER_ID_1, + getPerUserStats([IP_ADDRESS_IN_NORTH_KOREA, IP_ADDRESS_IN_CUBA])); + lastHourUserStats.set(USER_ID_2, + getPerUserStats([IP_ADDRESS_IN_NORTH_KOREA])); + + metrics.getHourlyServerMetricsReport( + SERVER_ID, START_DATETIME, END_DATETIME, lastHourUserStats, stubIpLookup) + .then((report) => { + expect(report).toBeNull(); + done(); + }); + }); +}); + +function getPerUserStats(ipAddresses: string[]): PerUserStats { + return { + bytesTransferred: 123, + anonymizedIpAddresses: new Set(ipAddresses) + }; +} + +function stubIpLookup(ipAddress: string) { + if (ipAddress === IP_ADDRESS_IN_US_1 || + ipAddress === IP_ADDRESS_IN_US_2) { + return Promise.resolve('US'); + } else if (ipAddress === IP_ADDRESS_IN_NORTH_KOREA) { + return Promise.resolve('KP'); + } else if (ipAddress === IP_ADDRESS_IN_CUBA) { + return Promise.resolve('CU'); + } else if (ipAddress === IP_ADDRESS_IN_GB) { + return Promise.resolve('GB'); + } + return Promise.reject(new Error('IP address not found: ' + ipAddress)); +} diff --git a/src/shadowbox/server/metrics.ts b/src/shadowbox/server/metrics.ts new file mode 100644 index 000000000..66977fa78 --- /dev/null +++ b/src/shadowbox/server/metrics.ts @@ -0,0 +1,404 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as ip_util from './ip_util'; +import * as file_read from '../infrastructure/file_read'; +import { AccessKeyId } from '../model/access_key'; +import { Stats, DataUsageByUser, PerUserStats, LastHourMetricsReadyCallback } from '../model/metrics'; +import * as events from 'events'; +import * as fs from 'fs'; +import * as follow_redirects from '../infrastructure/follow_redirects'; +import * as url from 'url'; + +const MS_PER_HOUR = 60 * 60 * 1000; + +interface PersistentStatsStoredData { + // Serialized TransferStats object. + transferStats: string; + // Serialized ConnectionStats object. + hourlyMetrics: string; +} + +// Stats implementation which reads and writes state to a JSON file containing +// a PersistentStatsStoredData object. +export class PersistentStats implements Stats { + private static readonly MAX_STATS_FILE_AGE_MS = 5000; + private transferStats: TransferStats; + private connectionStats: ConnectionStats; + private dirty = false; + private eventEmitter = new events.EventEmitter(); + private static readonly LAST_HOUR_METRICS_READY_EVENT = 'lastHourMetricsReady'; + + constructor(private filename) { + // Initialize stats from saved file, if available. + const persistedStateObj = this.readStateFile(); + if (persistedStateObj) { + this.transferStats = new TransferStats(persistedStateObj.transferStats); + this.connectionStats = new ConnectionStats(persistedStateObj.hourlyMetrics); + } else { + this.transferStats = new TransferStats(); + this.connectionStats = new ConnectionStats(); + } + + // Set write interval. + setInterval(this.writeStatsToFile.bind(this), PersistentStats.MAX_STATS_FILE_AGE_MS); + + // Set hourly metrics report interval + setHourlyInterval(this.generateHourlyReport.bind(this)); + } + + public recordBytesTransferred(userId: AccessKeyId, metricsUserId: AccessKeyId, numBytes: number, ipAddresses: string[]) { + // Pass the userId (sequence number) to transferStats as this data is returned to the Outline + // manager which relies on the userId sequence number. + this.transferStats.recordBytesTransferred(userId, numBytes); + // Pass metricsUserId (uuid, rather than sequence number) to connectionStats + // as these values may be reported to the Outline metrics server. + this.connectionStats.recordBytesTransferred(metricsUserId, numBytes, ipAddresses); + this.dirty = true; + } + + public get30DayByteTransfer(): DataUsageByUser { + return this.transferStats.get30DayByteTransfer(); + } + + public onLastHourMetricsReady(callback: LastHourMetricsReadyCallback) { + this.eventEmitter.on(PersistentStats.LAST_HOUR_METRICS_READY_EVENT, callback); + + // Check if an hourly metrics report is already due (e.g. if server was shutdown over an + // hour ago and just restarted). + if (getHoursSinceDatetime(this.connectionStats.startDatetime) >= 1) { + this.generateHourlyReport(); + } + } + + private writeStatsToFile() { + if (!this.dirty) { + return; + } + + const statsSerialized = JSON.stringify({ + transferStats: this.transferStats.serialize(), + hourlyMetrics: this.connectionStats.serialize() + }); + + // Write to temporary file, then move that temporary file to the + // persistent location, to avoid accidentally breaking the stats file. + // Use *Sync calls for atomic operations, to guard against corrupting + // these files. + const tempFilename = `${this.filename}.${Date.now()}`; + try { + fs.writeFileSync(tempFilename, statsSerialized, {encoding: 'utf8'}); + fs.renameSync(tempFilename, this.filename); + this.dirty = false; + } catch (err) { + console.error('error writing stats file ', err); + } + } + + private generateHourlyReport(): void { + if (this.connectionStats.lastHourUserStats.size === 0) { + // No connection stats to report. + return; + } + + this.eventEmitter.emit( + PersistentStats.LAST_HOUR_METRICS_READY_EVENT, + this.connectionStats.startDatetime, + new Date(), // endDatetime is the current date and time. + this.connectionStats.lastHourUserStats); + + // Reset connection stats to begin recording the next hour. + this.connectionStats.reset(); + + // Update hasChange so we know to persist stats. + this.dirty = true; + } + + private readStateFile(): PersistentStatsStoredData { + const text = file_read.readFileIfExists(this.filename); + if (!text) { + return null; + } + try { + return JSON.parse(text); + } catch (e) { + return null; + } + } +} + +// TransferStats keeps track of the number of bytes transferred per user, per day. +class TransferStats { + // Key is a string in the form "userId-dateInYYYYMMDD", e.g. "3-20170726". + private dailyUserBytesTransferred: Map; + // Set of all User IDs for whom we have transfer stats. + private userIdSet: Set; + + constructor(serializedObject?: {}) { + if (serializedObject) { + this.deserialize(serializedObject); + } else { + this.dailyUserBytesTransferred = new Map(); + this.userIdSet = new Set(); + } + } + + public recordBytesTransferred(userId: AccessKeyId, numBytes: number) { + this.userIdSet.add(userId); + + const d = new Date(); + const oldTotal = this.getBytes(userId, d); + const newTotal = oldTotal + numBytes; + this.dailyUserBytesTransferred.set(this.getKey(userId, d), newTotal); + } + + public get30DayByteTransfer(): DataUsageByUser { + const bytesTransferredByUserId = {}; + for (let i = 0; i < 30; ++i) { + // Get Date from i days ago. + const d = new Date(); + d.setDate(d.getDate() - i); + + // Get transfer per userId and total + for (const userId of this.userIdSet) { + if (!bytesTransferredByUserId[userId]) { + bytesTransferredByUserId[userId] = 0; + } + const numBytes = this.getBytes(userId, d); + bytesTransferredByUserId[userId] += numBytes; + } + } + return {bytesTransferredByUserId}; + } + + // Returns the state of this object, e.g. + // {"dailyUserBytesTransferred":[["0-20170816",100],["1-20170816",100]],"userIdSet":["0","1"]} + public serialize(): {} { + return { + // Use [...] operator to serialize Map and Set objects to JSON. + dailyUserBytesTransferred: [...this.dailyUserBytesTransferred], + userIdSet: [...this.userIdSet] + }; + } + + private deserialize(serializedObject: {}) { + this.dailyUserBytesTransferred = new Map(serializedObject['dailyUserBytesTransferred']); + this.userIdSet = new Set(serializedObject['userIdSet']); + } + + private getBytes(userId: AccessKeyId, d: Date) { + const key = this.getKey(userId, d); + return this.dailyUserBytesTransferred.get(key) || 0; + } + + private getKey(userId: AccessKeyId, d: Date) { + const yyyymmdd = d.toISOString().substr(0, 'YYYY-MM-DD'.length).replace(/-/g, ''); + return `${userId}-${yyyymmdd}`; + } +} + +// Keeps track of the connection stats per user, sine the startDatetime. +class ConnectionStats { + // Date+time at which we started recording connection stats, e.g. + // in case this object is constructed from data written to disk. + public startDatetime: Date; + + // Map from the metrics AccessKeyId to stats (bytes transferred, IP addresses). + public lastHourUserStats: Map; + + constructor(serializedObject?: {}) { + if (serializedObject) { + this.deserialize(serializedObject); + } else { + this.startDatetime = new Date(); + this.lastHourUserStats = new Map(); + } + } + + // CONSIDER: accepting hashedIpAddresses, which can be persisted to disk + // and reported to the metrics server (to approximate number of devices per userId). + public recordBytesTransferred(userId: AccessKeyId, numBytes: number, ipAddresses: string[]) { + const perUserStats = this.lastHourUserStats.get(userId) || + {bytesTransferred: 0, anonymizedIpAddresses: new Set()}; + perUserStats.bytesTransferred += numBytes; + const anonymizedIpAddresses = getAnonymizedAndDedupedIpAddresses(ipAddresses); + for (const ip of anonymizedIpAddresses) { + perUserStats.anonymizedIpAddresses.add(ip); + } + this.lastHourUserStats.set(userId, perUserStats); + } + + public reset(): void { + this.lastHourUserStats = new Map(); + this.startDatetime = new Date(); + } + + // Returns the state of this object, e.g. + // {"startTimestamp":1502896650353,"lastHourUserStatsObj":{"0":{"bytesTransferred":100,"anonymizedIpAddresses":["2620:0:1003:0:0:0:0:0","5.2.79.0"]}}} + public serialize(): {} { + // lastHourUserStats is a Map containing Set structures. Convert to an object + // with array values. + const lastHourUserStatsObj = {}; + this.lastHourUserStats.forEach((perUserStats, userId) => { + lastHourUserStatsObj[userId] = { + bytesTransferred: perUserStats.bytesTransferred, + anonymizedIpAddresses: [...perUserStats.anonymizedIpAddresses] + }; + }); + return { + startTimestamp: this.startDatetime.getTime(), + lastHourUserStatsObj + }; + } + + private deserialize(serializedObject: {}) { + // Convert type of lastHourUserStatsObj from Object containing Arrays to + // Map containing Sets. + const lastHourUserStatsMap = new Map(); + Object.keys(serializedObject['lastHourUserStatsObj']).map((userId) => { + const perUserStatsObj = serializedObject['lastHourUserStatsObj'][userId]; + lastHourUserStatsMap.set(userId, { + bytesTransferred: perUserStatsObj.bytesTransferred, + anonymizedIpAddresses: new Set(perUserStatsObj.anonymizedIpAddresses) + }); + }); + + this.startDatetime = new Date(serializedObject['startTimestamp']); + this.lastHourUserStats = lastHourUserStatsMap; + } +} + +export function getHourlyServerMetricsReport( + serverId: string, + startDatetime: Date, + endDatetime: Date, + lastHourUserStats: Map, + lookupCountry: ((ip: string) => Promise) = ip_util.lookupCountry): Promise { + if (lastHourUserStats.size === 0) { + // Stats are empty, no need to post a report + return Promise.resolve(null); + } + // convert lastHourUserStats to an array HourlyUserMetricsReport + const userReportPromises = []; + lastHourUserStats.forEach((perUserStats, userId) => { + userReportPromises.push(getHourlyUserMetricsReport(userId, perUserStats, lookupCountry)); + }); + return Promise.all(userReportPromises).then((userReports: HourlyUserMetricsReport[]) => { + // Remove any userReports containing sanctioned countries, and return + // null if no reports remain with un-sanctioned countries. + userReports = getWithoutSanctionedReports(userReports); + if (userReports.length === 0) { + return null; + } + return { + serverId, + startUtcMs: startDatetime.getTime(), + endUtcMs: endDatetime.getTime(), + userReports + }; + }); +} + +export function postHourlyServerMetricsReports(report: HourlyServerMetricsReport, + metricsUrl: string) { + const options = { + url: metricsUrl, + headers: {'Content-Type': 'application/json'}, + method: 'POST', + body: JSON.stringify(report) + }; + console.info('Posting metrics: ' + JSON.stringify(options)); + return follow_redirects.requestFollowRedirectsWithSameMethodAndBody(options, (error, response, body) => { + if (error) { + console.error('Error posting metrics: ', error); + return; + } + console.info('Metrics server responded with status ' + response.statusCode); + }); +} + +function setHourlyInterval(callback: Function) { + const msUntilNextHour = MS_PER_HOUR - (Date.now() % MS_PER_HOUR); + setTimeout(() => { + setInterval(callback, MS_PER_HOUR); + callback(); + }, msUntilNextHour); +} + +// Returns the floating-point number of hours passed since the specified date. +function getHoursSinceDatetime(d: Date): number { + const deltaMs = Date.now() - d.getTime(); + return deltaMs / (MS_PER_HOUR); +} + +interface HourlyServerMetricsReport { + serverId: string; + startUtcMs: number; + endUtcMs: number; + userReports: HourlyUserMetricsReport[]; +} + +interface HourlyUserMetricsReport { + userId: string; + countries: string[]; + bytesTransferred: number; +} + +function getHourlyUserMetricsReport( + userId: AccessKeyId, + perUserStats: PerUserStats, + lookupCountry: ((ip: string) => Promise) = ip_util.lookupCountry): Promise { + const countryPromises = []; + for (const ip of perUserStats.anonymizedIpAddresses) { + countryPromises.push(lookupCountry(ip)); + } + return Promise.all(countryPromises).then((countries: string[]) => { + return { + userId, + bytesTransferred: perUserStats.bytesTransferred, + countries: getWithoutDuplicates(countries) + }; + }); +} + +function getAnonymizedAndDedupedIpAddresses(ipAddresses: string[]): Set { + const s = new Set(); + for (const ip of ipAddresses) { + try { + s.add(ip_util.anonymizeIp(ip)); + } catch (err) { + console.error('error anonymizing IP address: ' + ip + ', ' + err); + } + } + return s; +} + +// Return an array with the duplicate elements removed. +function getWithoutDuplicates(a: T[]): T[] { + return [...new Set(a)]; +} + +function getWithoutSanctionedReports(userReports: HourlyUserMetricsReport[]): HourlyUserMetricsReport[] { + const sanctionedCountries = ['CU', 'IR', 'KP', 'SY']; + const filteredReports = []; + for (const userReport of userReports) { + userReport.countries = userReport.countries.filter((country) => { + return sanctionedCountries.indexOf(country) === -1; + }); + if (userReport.countries.length > 0) { + filteredReports.push(userReport); + } + } + return filteredReports; +} diff --git a/src/shadowbox/server/mocks/mocks.ts b/src/shadowbox/server/mocks/mocks.ts new file mode 100644 index 000000000..49bcd7199 --- /dev/null +++ b/src/shadowbox/server/mocks/mocks.ts @@ -0,0 +1,113 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as dgram from 'dgram'; + +import {AccessKey, AccessKeyId, AccessKeyRepository} from '../../model/access_key'; +import {Stats} from '../../model/metrics'; +import {ShadowsocksInstance} from '../../model/shadowsocks_server'; +import {TextFile} from '../../model/text_file'; + +export class MockAccessKeyRepository implements AccessKeyRepository { + private accessKeys: AccessKey[] = []; + createNewAccessKey(): Promise { + const id = this.accessKeys.length.toString(); + const key = new MockAccessKey( + id, 'metricsId', 'name', new MockShadowsocksInstance()); + this.accessKeys.push(key); + return Promise.resolve(key); + } + removeAccessKey(id: AccessKeyId): boolean { + for (let i = 0; i < this.accessKeys.length; ++i) { + if (this.accessKeys[i].id === id) { + this.accessKeys.splice(i, 1); + return true; + } + } + return false; + } + listAccessKeys(): IterableIterator { + return this.accessKeys[Symbol.iterator](); + } + renameAccessKey(id: AccessKeyId, name: string): boolean { + for (let i = 0; i < this.accessKeys.length; ++i) { + if (this.accessKeys[i].id === id) { + this.accessKeys[i].name = name; + return true; + } + } + return false; + } +} + +class MockAccessKey implements AccessKey { + constructor( + public id: AccessKeyId, + public metricsId: AccessKeyId, + public name: string, + public shadowsocksInstance: ShadowsocksInstance) {} + public rename(name: string): void { + this.name = name; + } +} + +class MockShadowsocksInstance implements ShadowsocksInstance { + constructor( + public portNumber = 12345, + public password = 'password', + public encryptionMethod = 'encryption', + public accessUrl = 'ss://somethingsomething') {} + onBytesTransferred(callback: (bytes: number, ipAddresses: string[]) => void) {} + stop() {} +} + +export class MockShadowsocksServer { + startInstance( + portNumber: number, password: string, statsSocket: dgram.Socket, + encryptionMethod?: string): Promise { + const mock = new MockShadowsocksInstance(portNumber, password, encryptionMethod); + return Promise.resolve(mock); + } +} + +export class MockStats { + recordBytesTransferred( + userId: AccessKeyId, + metricsUserId: AccessKeyId, + numBytes: number, + ipAddresses: string[]) {} + onLastHourMetricsReady(callback) {} + get30DayByteTransfer() { + return {bytesTransferredByUserId: {}}; + } +} + +export class InMemoryFile implements TextFile { + private savedText: string; + constructor(private exists: boolean) {} + readFileSync() { + if (this.exists) { + return this.savedText; + } else { + const err = new Error('no such file or directory'); + // tslint:disable-next-line:no-any + (err as any).code = 'ENOENT'; + throw err; + } + } + writeFileSync(text: string) { + this.savedText = text; + this.exists = true; + } +} diff --git a/src/shadowbox/server/run_action.sh b/src/shadowbox/server/run_action.sh new file mode 100755 index 000000000..91a16b245 --- /dev/null +++ b/src/shadowbox/server/run_action.sh @@ -0,0 +1,27 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +do_action shadowbox/server/build + +export LOG_LEVEL="${LOG_LEVEL:-debug}" +export SB_PUBLIC_IP="${SB_PUBLIC_IP:-$(curl https://ipinfo.io/ip)}" +export SB_API_PREFIX=TestApiPrefix +export SB_METRICS_URL=https://metrics-test.uproxy.org +export SB_STATE_DIR=/tmp + +source $ROOT_DIR/src/shadowbox/scripts/make_certificate.sh + +node $BUILD_DIR/shadowbox/app/server/main diff --git a/src/shadowbox/server/server_config.ts b/src/shadowbox/server/server_config.ts new file mode 100644 index 000000000..54f0d36d7 --- /dev/null +++ b/src/shadowbox/server/server_config.ts @@ -0,0 +1,102 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as file_read from '../infrastructure/file_read'; +import * as fs from 'fs'; +import * as uuidv4 from 'uuid/v4'; + +export class ServerConfig { + public serverId: string; + private metricsEnabled = false; + private name: string; + private createdTimestampMs: number; // Created timestamp in UTC milliseconds. + + constructor(private filename: string, defaultName?: string) { + // Initialize from filename if possible. + const configText = file_read.readFileIfExists(filename); + if (configText) { + try { + const savedState = JSON.parse(configText); + if (savedState.serverId) { + this.serverId = savedState.serverId; + } + if (savedState.metricsEnabled) { + this.metricsEnabled = savedState.metricsEnabled; + } + if (savedState.name) { + this.name = savedState.name; + } + if (savedState.createdTimestampMs) { + this.createdTimestampMs = savedState.createdTimestampMs; + } + } catch (err) { + console.error('error parsing config', err); + } + } + + // Initialize to default values if file missing or not valid. + let dirty = false; + if (!this.serverId) { + this.serverId = uuidv4(); + dirty = true; + } + if (!this.name && defaultName) { + this.name = defaultName; + dirty = true; + } + if (!this.createdTimestampMs) { + this.createdTimestampMs = Date.now(); + dirty = true; + } + if (dirty) { + this.writeFile(); + } + } + + private writeFile(): void { + const state = JSON.stringify({ + serverId: this.serverId, + metricsEnabled: this.metricsEnabled, + name: this.name, + createdTimestampMs: this.createdTimestampMs + }); + fs.writeFileSync(this.filename, state, {encoding: 'utf8'}); + } + + public getMetricsEnabled(): boolean { + return this.metricsEnabled; + } + + public setMetricsEnabled(newValue: boolean): void { + if (newValue !== this.metricsEnabled) { + this.metricsEnabled = newValue; + this.writeFile(); + } + } + + public getName(): string { + return this.name || 'Outline Server'; + } + + public setName(newValue: string): void { + if (newValue !== this.name) { + this.name = newValue; + this.writeFile(); + } + } + + public getCreatedTimestampMs(): number { + return this.createdTimestampMs; + } +} diff --git a/src/shadowbox/shadowbox_config.json b/src/shadowbox/shadowbox_config.json new file mode 100644 index 000000000..40b81023b --- /dev/null +++ b/src/shadowbox/shadowbox_config.json @@ -0,0 +1 @@ +{"users":[]} \ No newline at end of file diff --git a/src/shadowbox/test_action.sh b/src/shadowbox/test_action.sh new file mode 100755 index 000000000..387ade00b --- /dev/null +++ b/src/shadowbox/test_action.sh @@ -0,0 +1,18 @@ +#!/bin/bash -eu +# +# Copyright 2018 The Outline Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +do_action shadowbox/server/build +jasmine --config=$ROOT_DIR/jasmine.json diff --git a/src/shadowbox/tsconfig.json b/src/shadowbox/tsconfig.json new file mode 100644 index 000000000..827709c73 --- /dev/null +++ b/src/shadowbox/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es6", + "removeComments": false, + "noImplicitAny": false, + "noImplicitThis": true, + "module": "commonjs", + "rootDir": "." + }, + "include": [ + "server/main.ts", + "**/*spec.ts", + "types/**/*.d.ts" + ], + "exclude": [ + "build", + "node_modules" + ] +} \ No newline at end of file diff --git a/src/shadowbox/types/node.d.ts b/src/shadowbox/types/node.d.ts new file mode 100644 index 000000000..f09fda88d --- /dev/null +++ b/src/shadowbox/types/node.d.ts @@ -0,0 +1,31 @@ +// Copyright 2018 The Outline Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Definitions missing from @types/node. + +// Reference: https://nodejs.org/api/dns.html +declare module 'dns' { + export function getServers(): string[]; +} + +// 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 function exec( + command: string, + callback?: (error: ExecError|undefined, stdout: string, stderr: string) => + void): ChildProcess; +} diff --git a/src/shadowbox/yarn.lock b/src/shadowbox/yarn.lock new file mode 100644 index 000000000..cfa8dbd5f --- /dev/null +++ b/src/shadowbox/yarn.lock @@ -0,0 +1,479 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/bunyan@*": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.0.tgz#913bf718a2f4dd1efa063e808cab76609289c986" + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@^7.0.16": + version "7.0.32" + resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.32.tgz#6afe6c66520a4c316623a14aef123908d01b4bba" + +"@types/randomstring@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@types/randomstring/-/randomstring-1.1.6.tgz#45cdc060a6f043d610bcd46503a6887db2a209c3" + +"@types/restify@^2.0.41": + version "2.0.42" + resolved "https://registry.yarnpkg.com/@types/restify/-/restify-2.0.42.tgz#6592376dc230c45afcffd4f9ee7bccd2776b1e1a" + dependencies: + "@types/bunyan" "*" + "@types/node" "*" + +"ShadowsocksConfig@https://github.com/Jigsaw-Code/outline-shadowsocksconfig/archive/v0.0.6.tar.gz": + version "0.0.6" + resolved "https://github.com/Jigsaw-Code/outline-shadowsocksconfig/archive/v0.0.6.tar.gz#620e56485c66277a2240b6f2e9e8091e103c547f" + dependencies: + base-64 "^0.1.0" + punycode "^1.4.1" + +array-uniq@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.2.tgz#5fcc373920775723cfd64d65c64bef53bf9eba6d" + +asn1@0.1.11: + version "0.1.11" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.1.11.tgz#559be18376d08a4ec4dbe80877d27818639b2df7" + +assert-plus@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.1.5.tgz#ee74009413002d84cec7219c6ac811812e723160" + +assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +backoff@^2.4.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f" + dependencies: + precond "0.2" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bunyan@^1.4.0: + version "1.8.10" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.10.tgz#201fedd26c7080b632f416072f53a90b9a52981c" + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.10.6" + mv "~2" + safe-json-stringify "~1" + +caseless@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.6.0.tgz#8167c1ab8397fb5bb95f96d28e5a81c50f247ac4" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +csv-generate@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/csv-generate/-/csv-generate-0.0.6.tgz#97e4e63ae46b21912cd9475bc31469d26f5ade66" + +csv-parse@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-1.2.0.tgz#047b73868ab9a85746e885f637f9ed0fb645a425" + +csv-stringify@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-0.0.8.tgz#52cc3b3dfc197758c55ad325a95be85071f9e51b" + +csv@^0.4.0: + version "0.4.6" + resolved "https://registry.yarnpkg.com/csv/-/csv-0.4.6.tgz#8dbae7ddfdbaae62c1ea987c3e0f8a9ac737b73d" + dependencies: + csv-generate "^0.0.6" + csv-parse "^1.0.0" + csv-stringify "^0.0.8" + stream-transform "^0.1.0" + +ctype@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/ctype/-/ctype-0.5.3.tgz#82c18c2461f74114ef16c135224ad0b9144ca12f" + +debug@^2.6.8: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +detect-node@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" + +dtrace-provider@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.6.0.tgz#0b078d5517937d873101452d9146737557b75e51" + dependencies: + nan "^2.0.8" + +dtrace-provider@~0.8: + version "0.8.3" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.3.tgz#ba1bfc6493285ccfcfc6ab69cd5c61d74c2a43bf" + dependencies: + nan "^2.3.3" + +escape-regexp-component@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/escape-regexp-component/-/escape-regexp-component-1.0.2.tgz#9c63b6d0b25ff2a88c3adbd18c5b61acc3b9faa2" + +extsprintf@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.2.0.tgz#5ad946c22f5b32ba7f8cd7426711c6e8a3fc2529" + +extsprintf@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +forever-agent@~0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.5.2.tgz#6d0e09c4921f94a27f63d3b49c5feff1ea4c5130" + +formidable@^1.0.14: + version "1.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" + +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +handle-thing@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + +http-signature@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.11.0.tgz#1796cf67a001ad5cd6849dca0991485f09089fe6" + dependencies: + asn1 "0.1.11" + assert-plus "^0.1.5" + ctype "0.5.3" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ipaddr.js@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +json-stringify-safe@~5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +keep-alive-agent@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/keep-alive-agent/-/keep-alive-agent-0.0.1.tgz#44847ca394ce8d6b521ae85816bd64509942b385" + +lru-cache@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +mime-types@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-1.0.2.tgz#995ae1392ab8affcbfcb2641dd054e943c0d5dce" + +mime@^1.2.11: + version "1.3.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +"minimatch@2 || 3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +moment@^2.10.6: + version "2.18.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + +nan@^2.0.8, nan@^2.3.3: + version "2.6.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" + +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + +negotiator@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-uuid@^1.4.1, node-uuid@~1.4.0: + version "1.4.8" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" + +obuf@^1.0.0, obuf@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +precond@0.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@^6.2.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +qs@~1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.2.tgz#19b57ff24dc2a99ce1f8bdf6afcda59f8ef61f88" + +randomstring@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/randomstring/-/randomstring-1.1.5.tgz#6df0628f75cbd5932930d9fe3ab4e956a18518c3" + dependencies: + array-uniq "1.0.2" + +readable-stream@^2.0.1, readable-stream@^2.2.9: + version "2.3.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.2.tgz#5a04df05e4f57fe3f0dc68fdd11dc5c97c7e6f4d" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.1.0" + string_decoder "~1.0.0" + util-deprecate "~1.0.1" + +request-lite@^2.40.1: + version "2.40.1" + resolved "https://registry.yarnpkg.com/request-lite/-/request-lite-2.40.1.tgz#08a8a151eaa84117e01d3e14fe765e8529b3f3df" + dependencies: + caseless "~0.6.0" + forever-agent "~0.5.0" + json-stringify-safe "~5.0.0" + mime-types "~1.0.1" + node-uuid "~1.4.0" + qs "~1.2.0" + +restify@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/restify/-/restify-4.3.0.tgz#03b67960d1d42a6dafcde3bd82fb882173a27678" + dependencies: + assert-plus "^0.1.5" + backoff "^2.4.0" + bunyan "^1.4.0" + csv "^0.4.0" + escape-regexp-component "^1.0.2" + formidable "^1.0.14" + http-signature "^0.11.0" + keep-alive-agent "^0.0.1" + lru-cache "^4.0.1" + mime "^1.2.11" + negotiator "^0.6.1" + node-uuid "^1.4.1" + once "^1.3.0" + qs "^6.2.1" + semver "^4.3.3" + spdy "^3.3.3" + tunnel-agent "^0.4.0" + vasync "1.6.3" + verror "^1.4.0" + optionalDependencies: + dtrace-provider "^0.6.0" + +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + dependencies: + glob "^6.0.1" + +safe-buffer@^5.0.1, safe-buffer@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +safe-json-stringify@~1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz#81a098f447e4bbc3ff3312a243521bc060ef5911" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + +semver@^4.3.3: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + +spdy-transport@^2.0.18: + version "2.0.20" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.20.tgz#735e72054c486b2354fe89e702256004a39ace4d" + dependencies: + debug "^2.6.8" + detect-node "^2.0.3" + hpack.js "^2.1.6" + obuf "^1.1.1" + readable-stream "^2.2.9" + safe-buffer "^5.0.1" + wbuf "^1.7.2" + +spdy@^3.3.3: + version "3.4.7" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" + dependencies: + debug "^2.6.8" + handle-thing "^1.2.5" + http-deceiver "^1.2.7" + safe-buffer "^5.0.1" + select-hose "^2.0.0" + spdy-transport "^2.0.18" + +stream-transform@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/stream-transform/-/stream-transform-0.1.2.tgz#7d8e6b4e03ac4781778f8c79517501bfb0762a9f" + +string_decoder@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +tunnel-agent@^0.4.0: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +vasync@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/vasync/-/vasync-1.6.3.tgz#4a69d7052a47f4ce85503d7641df1cbf40432a94" + dependencies: + verror "1.6.0" + +verror@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.6.0.tgz#7d13b27b1facc2e2da90405eb5ea6e5bdd252ea5" + dependencies: + extsprintf "1.2.0" + +verror@^1.4.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +wbuf@^1.1.0, wbuf@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" + dependencies: + minimalistic-assert "^1.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..1687e7e56 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "removeComments": false, + "noImplicitAny": true, + "noImplicitThis": true, + "module": "commonjs", + "rootDir": "src", + "outDir": "build/server_manager/web_app/js", + "sourceMap": true, + "lib": [ + "dom", + "es5", + "es2015.promise", + "es2015.iterable", + "es2016" + ] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/server_manager/electron_app/**/*.ts", + "src/metrics_server/**/*.ts", + "src/shadowbox/**/*.ts" + ], + "compileOnSave": true +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 000000000..c46f07246 --- /dev/null +++ b/tslint.json @@ -0,0 +1,47 @@ +// Copied from google.tslint.json, with internal rules removed +{ + "rules": { + "array-type": [true, "array-simple"], + "arrow-return-shorthand": true, + "ban-types": [true, + ["Object", "Use {} instead."], + ["String", "Use 'string' instead."], + ["Number", "Use 'number' instead."], + ["Boolean", "Use 'boolean' instead."] + ], + "class-name": true, + "forin": true, + "interface-name": [true, "never-prefix"], + "jsdoc-format": true, + "label-position": true, + "new-parens": true, + "no-angle-bracket-type-assertion": true, + "no-any": true, + "no-construct": true, + "no-debugger": true, + "no-default-export": true, + "no-inferrable-types": true, + "no-namespace": [true, "allow-declarations"], + "no-reference": true, + "no-require-imports": true, + "no-unused-expression": true, + "no-use-before-declare": false, + "no-var-keyword": true, + "object-literal-shorthand": true, + "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], + "prefer-const": true, + "radix": true, + "semicolon": [true, "always", "ignore-bound-class-methods"], + "no-string-throw": true, + "switch-default": true, + "triple-equals": [true, "allow-null-check"], + "use-isnan": true, + "variable-name": [ + true, + "check-format", + "ban-keywords", + "allow-leading-underscore", + "allow-trailing-underscore" + ] + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..5ad11c7c1 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,294 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/jasmine@^2.5.53": + version "2.5.53" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.5.53.tgz#4e0cefad09df5ec48c8dd40433512f84b1568d61" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +babel-code-frame@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" + dependencies: + chalk "^1.1.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +brace-expansion@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +chalk@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +ci-info@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a" + +clang-format@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.2.2.tgz#a7277a03fce9aa4e387ddaa83b60d99dab115737" + dependencies: + async "^1.5.2" + glob "^7.0.0" + resolve "^1.1.6" + +color-convert@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +commander@^2.12.1: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +diff@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +glob@^7.0.0, glob@^7.0.6, glob@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +husky@^0.14.3: + version "0.14.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" + dependencies: + is-ci "^1.0.10" + normalize-path "^1.0.0" + strip-indent "^2.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-ci@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + dependencies: + ci-info "^1.0.0" + +jasmine-core@~2.6.0: + version "2.6.4" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.6.4.tgz#dec926cd0a9fa287fb6db5c755fa487e74cecac5" + +jasmine@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.6.0.tgz#6b22e70883e8e589d456346153b4d206ddbe217f" + dependencies: + exit "^0.1.2" + glob "^7.0.6" + jasmine-core "~2.6.0" + +js-tokens@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" + +js-yaml@^3.7.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +normalize-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +resolve@^1.1.6: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + +resolve@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" + dependencies: + path-parse "^1.0.5" + +semver@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-indent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +tslib@^1.8.0, tslib@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.1.tgz#6946af2d1d651a7b1863b531d6e5afa41aa44eac" + +tslint@^5.9.1: + version "5.9.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.9.1.tgz#1255f87a3ff57eb0b0e1f0e610a8b4748046c9ae" + dependencies: + babel-code-frame "^6.22.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^3.2.0" + glob "^7.1.1" + js-yaml "^3.7.0" + minimatch "^3.0.4" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.12.1" + +tsutils@^2.12.1: + version "2.16.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.16.0.tgz#ad8e83f47bef4f7d24d173cc6cd180990c831105" + dependencies: + tslib "^1.8.1" + +typescript@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"