diff --git a/.env.production b/.env.production index 8cabc5a..e729e7c 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,6 @@ CREDENTIALS_ENABLED = 1 +CREDENTIALS_PATH = /home/ubuntu/.certbot/config/live/algorithm-visualizer.org +CREDENTIALS_CA = fullchain.pem +CREDENTIALS_KEY = privkey.pem +CREDENTIALS_CERT = cert.pem WEBHOOK_ENABLED = 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dbd56dc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Contributing + +> #### Table of Contents +> - [Running Locally](#running-locally) +> - [Directory Structure](#directory-structure) + +Are you a first-timer in contributing to open source? [These guidelines](https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution) from GitHub might help! + +## Running Locally + +1. Fork this repository. + +2. Clone your forked repo to your machine. + + ```bash + git clone https://github.com//server.git + ``` + +3. Install [Docker](https://docs.docker.com/install/), if not done already. + +4. Create `.env.local` in the project root: + ```bash + # By putting dummy values, GitHub sign in will not work locally + GITHUB_CLIENT_ID = dummy + GITHUB_CLIENT_SECRET = dummy + + # By putting dummy values, extracting visualizing commands will not work locally (except for JavaScript). + AWS_ACCESS_KEY_ID = dummy + AWS_SECRET_ACCESS_KEY = dummy + ``` + +5. Install dependencies, and run the server. + + ```bash + cd server + + npm install + + npm run watch + ``` + +6. Open [`http://localhost:8080/`](http://localhost:8080/) in a web browser. + +## Directory Structure + +- [**src/**](src) contains source code. + - [**config/**](src/config) contains configuration files. + - [**controllers/**](src/controllers) routes and processes incoming requests. + - [**middlewares/**](src/middlewares) contains Express middlewares. + - [**models/**](src/models) manages algorithm visualizations and their hierarchy. + - [**tracers/**](src/tracers) build visualization libraries and compiles/runs code. + - [**utils/**](src/utils) contains utility files. + +**NOTE** that for JavaScript, it builds a web worker rather than a docker image. Once a browser fetches the web worker, it will submit users' code to the web worker locally, instead of submitting to the remote server, to extract visualizing commands. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a52e6b --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Server + +> This repository is part of the project [Algorithm Visualizer](https://github.com/algorithm-visualizer). + +`server` serves [`algorithm-visualizer`](https://github.com/algorithm-visualizer/algorithm-visualizer) and provides APIs that the web app needs on the fly. (e.g., GitHub sign in, compiling/running code, etc.) + +## Contributing + +Check out the [contributing guidelines](https://github.com/algorithm-visualizer/server/blob/master/CONTRIBUTING.md). diff --git a/certbot.ini b/certbot.ini new file mode 100644 index 0000000..dab450e --- /dev/null +++ b/certbot.ini @@ -0,0 +1,7 @@ +config-dir = /home/ubuntu/.certbot/config +work-dir = /home/ubuntu/.certbot/work +logs-dir = /home/ubuntu/.certbot/logs +email = parkjs814@gmail.com +authenticator = webroot +webroot-path = /home/ubuntu/server/public/frontend-built +domains = algorithm-visualizer.org diff --git a/package-lock.json b/package-lock.json index ea58289..80b8362 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,6 +112,12 @@ "integrity": "sha512-1YKeT4JitGgE4SOzyB9eMwO0nGVNkNEsm9qlIt1Lqm/tG2QEiSMTD4kS3aO6L+w5SClLVxALmIBESK6Mk5wX0A==", "dev": true }, + "@types/node-cron": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.1.tgz", + "integrity": "sha512-BkMHHonDT8NJUE/pQ3kr5v2GLDKm5or9btLBoBx4F2MB2cuqYC748LYMDC55VlrLI5qZZv+Qgc3m4P3dBPcmeg==", + "dev": true + }, "@types/range-parser": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", @@ -198,13 +204,28 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "aws-sdk": { + "version": "2.814.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.814.0.tgz", + "integrity": "sha512-empd1m/J/MAkL6d9OeRpmg9thobULu0wk4v8W3JToaxGi2TD7PIdvE6yliZKyOVAdJINhBWEBhxR4OUIHhcGbQ==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + } + }, "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz", + "integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==", "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" + "follow-redirects": "^1.14.0" } }, "balanced-match": { @@ -213,6 +234,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -258,6 +284,16 @@ "concat-map": "0.0.1" } }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -418,14 +454,6 @@ "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==", "dev": true }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -516,6 +544,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -615,12 +648,9 @@ } }, "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", + "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==" }, "forwarded": { "version": "0.1.2", @@ -686,9 +716,9 @@ "dev": true }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "http-errors": { @@ -711,6 +741,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", @@ -746,11 +781,6 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" - }, "is-finite": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", @@ -772,12 +802,22 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -919,6 +959,19 @@ } } }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, + "moment-timezone": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", + "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", + "requires": { + "moment": ">= 2.9.0" + } + }, "morgan": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", @@ -951,6 +1004,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "node-cron": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz", + "integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==", + "requires": { + "moment-timezone": "^0.5.31" + } + }, "node-notifier": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", @@ -1040,9 +1101,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-to-regexp": { @@ -1091,11 +1152,21 @@ "ipaddr.js": "1.9.0" } }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1185,6 +1256,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", @@ -1357,9 +1433,9 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "tree-kill": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", - "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, "trim-newlines": { @@ -1494,6 +1570,15 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1534,6 +1619,20 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index 22904c9..d33045a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "name": "@algorithm-visualizer/server", "version": "2.0.0", - "description": "Server for Algorithm Visualizer", + "title": "Algorithm Visualizer", + "description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.", "scripts": { "watch": "NODE_ENV=development NODE_PATH=src ts-node-dev --respawn --ignore-watch node_modules --no-notify src", "start": "NODE_ENV=production NODE_PATH=src ts-node --transpile-only src", @@ -19,13 +20,15 @@ "@types/fs-extra": "^7.0.0", "@types/morgan": "^1.7.35", "@types/node": "^12.0.0", + "@types/node-cron": "^3.0.1", "@types/remove-markdown": "^0.1.1", "@types/uuid": "^3.4.4", "ts-node-dev": "^1.0.0-pre.39", "tslint": "^5.16.0" }, "dependencies": { - "axios": "^0.19.0", + "aws-sdk": "^2.814.0", + "axios": "^0.21.2", "body-parser": "^1.18.2", "compression": "^1.7.3", "dotenv": "^8.0.0", @@ -34,6 +37,7 @@ "express-github-webhook": "^1.0.6", "fs-extra": "^6.0.1", "morgan": "^1.9.1", + "node-cron": "^3.0.0", "remove-markdown": "^0.3.0", "ts-httpexceptions": "^4.1.0", "ts-node": "^8.1.0", diff --git a/pm2.json b/pm2.json new file mode 100644 index 0000000..9450d37 --- /dev/null +++ b/pm2.json @@ -0,0 +1,8 @@ +{ + "apps": [ + { + "name": "algorithm-visualizer", + "script": "npm start" + } + ] +} diff --git a/src/Server.ts b/src/Server.ts index ad9d1d0..637b2df 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -7,18 +7,19 @@ import compression from 'compression'; import { __PROD__, credentials, httpPort, httpsPort, webhookOptions } from 'config/environments'; import http from 'http'; import https from 'https'; +import cron from 'node-cron'; import { Hierarchy } from 'models'; import * as Tracers from 'tracers'; import { errorHandlerMiddleware, frontendMiddleware, redirectMiddleware } from 'middlewares'; -import { execute, pull } from 'utils/misc'; +import { execute, issueHttpsCertificate, pull } from 'utils/misc'; import { frontendBuildDir, frontendBuiltDir, frontendDir, rootDir } from 'config/paths'; const Webhook = require('express-github-webhook'); export default class Server { - private readonly app = express(); readonly hierarchy = new Hierarchy(); readonly tracers = Object.values(Tracers).map(Tracer => new Tracer()); + private readonly app = express(); private readonly webhook = webhookOptions && Webhook(webhookOptions); constructor() { @@ -27,17 +28,17 @@ export default class Server { .use(morgan(__PROD__ ? 'tiny' : 'dev')) .use(redirectMiddleware()) .use(bodyParser.json()) - .use(bodyParser.urlencoded({extended: true})) + .use(bodyParser.urlencoded({ extended: true })) .use('/api', this.getApiRouter()) .use(frontendMiddleware(this)); if (this.webhook) { - this.app.use('/webhook', this.webhook); + this.app.use(this.webhook); } this.app.use(errorHandlerMiddleware()); if (this.webhook) { this.webhook.on('push', async (repo: string, data: any) => { - const {ref, head_commit} = data; + const { ref, head_commit } = data; if (ref !== 'refs/heads/master') return; if (!head_commit) throw new Error('The `head_commit` is empty.'); @@ -62,6 +63,12 @@ export default class Server { await tracer.update(data.release); }); } + + if (credentials) { + cron.schedule('0 0 1 * *', () => { + issueHttpsCertificate(); + }); + } } getApiRouter() { @@ -75,7 +82,11 @@ export default class Server { async update(commit?: string) { await pull(rootDir, 'server', commit); - await execute('npm install', {cwd: rootDir}); + await execute('npm install', { + cwd: rootDir, + stdout: process.stdout, + stderr: process.stderr, + }); process.exit(0); }; @@ -86,7 +97,11 @@ export default class Server { 'npm run build', `rm -rf ${frontendBuiltDir}`, `mv ${frontendBuildDir} ${frontendBuiltDir}`, - ].join(' && '), {cwd: frontendDir}); + ].join(' && '), { + cwd: frontendDir, + stdout: process.stdout, + stderr: process.stderr, + }); } start() { diff --git a/src/config/environments.ts b/src/config/environments.ts index 0a8ae31..912d9c8 100644 --- a/src/config/environments.ts +++ b/src/config/environments.ts @@ -1,15 +1,10 @@ import fs from 'fs'; import { ServerOptions } from 'https'; import path from 'path'; +import { issueHttpsCertificate } from '../utils/misc'; require('dotenv-flow').config(); -declare var process: { - env: { - [key: string]: string, - } -}; - const { NODE_ENV, @@ -27,7 +22,12 @@ const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, -} = process.env; + + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, +} = process.env as { + [key: string]: string, +}; const isEnabled = (v: string) => v === '1'; @@ -48,6 +48,8 @@ const missingVars = [ ] : []), 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', ].filter(variable => process.env[variable] === undefined); if (missingVars.length) throw new Error(`The following environment variables are missing: ${missingVars.join(', ')}`); @@ -62,12 +64,22 @@ export const webhookOptions = isEnabled(WEBHOOK_ENABLED) ? { secret: WEBHOOK_SECRET, } : undefined; -const readCredentials = (file: string) => fs.readFileSync(path.resolve(CREDENTIALS_PATH, file)); -export const credentials: ServerOptions | undefined = isEnabled(CREDENTIALS_ENABLED) ? { - ca: readCredentials(CREDENTIALS_CA), - key: readCredentials(CREDENTIALS_KEY), - cert: readCredentials(CREDENTIALS_CERT), -} : undefined; +export let credentials: ServerOptions | undefined; +if (isEnabled(CREDENTIALS_ENABLED)) { + if (fs.existsSync(CREDENTIALS_PATH)) { + const readCredentials = (file: string) => fs.readFileSync(path.resolve(CREDENTIALS_PATH, file)); + credentials = { + ca: readCredentials(CREDENTIALS_CA), + key: readCredentials(CREDENTIALS_KEY), + cert: readCredentials(CREDENTIALS_CERT), + }; + } else { + issueHttpsCertificate(); + } +} export const githubClientId = GITHUB_CLIENT_ID; export const githubClientSecret = GITHUB_CLIENT_SECRET; + +export const awsAccessKeyId = AWS_ACCESS_KEY_ID; +export const awsSecretAccessKey = AWS_SECRET_ACCESS_KEY; diff --git a/src/middlewares/errorHandlerMiddleware.ts b/src/middlewares/errorHandlerMiddleware.ts index 8a1936a..8eeb830 100644 --- a/src/middlewares/errorHandlerMiddleware.ts +++ b/src/middlewares/errorHandlerMiddleware.ts @@ -8,7 +8,7 @@ export function errorHandlerMiddleware() { err = new InternalServerError(err.message, err); } - const {name, message, status} = err; - res.status(status).json({name, message, status}); + const {message, status} = err; + res.status(status).send(message); }; } diff --git a/src/tracers/DockerTracer.ts b/src/tracers/DockerTracer.ts index b3d5e27..3672e2f 100644 --- a/src/tracers/DockerTracer.ts +++ b/src/tracers/DockerTracer.ts @@ -15,13 +15,15 @@ export class DockerTracer extends Tracer { super(lang); this.directory = path.resolve(__dirname, lang); this.imageName = `tracer-${this.lang}`; - - this.build = this.build.bind(this); } build(release: Release) { const {tag_name} = release; - return execute(`docker build -t ${this.imageName} . --build-arg tag_name=${tag_name}`, {cwd: this.directory}); + return execute(`docker build -t ${this.imageName} . --build-arg tag_name=${tag_name}`, { + cwd: this.directory, + stdout: process.stdout, + stderr: process.stderr, + }); } route(router: express.Router) { diff --git a/src/tracers/LambdaTracer.ts b/src/tracers/LambdaTracer.ts new file mode 100644 index 0000000..b608a45 --- /dev/null +++ b/src/tracers/LambdaTracer.ts @@ -0,0 +1,33 @@ +import AWS from 'aws-sdk'; +import express from 'express'; +import { Release, Tracer } from 'tracers/Tracer'; +import { awsAccessKeyId, awsSecretAccessKey } from 'config/environments'; +import { BadRequest } from 'ts-httpexceptions'; + +export class LambdaTracer extends Tracer { + static lambda = new AWS.Lambda({ + region: 'us-east-2', + accessKeyId: awsAccessKeyId, + secretAccessKey: awsSecretAccessKey, + }); + + async build(release: Release) { + } + + route(router: express.Router) { + router.post(`/${this.lang}`, (req, res, next) => { + const {code} = req.body; + LambdaTracer.lambda.invoke({ + FunctionName: `extractor-${this.lang}`, + InvocationType: 'RequestResponse', + Payload: JSON.stringify(code), + }, function (err, data) { + if (err) return next(err); + if (typeof data.Payload !== 'string') return next(new Error('Unexpected Payload Type')); + const payload = JSON.parse(data.Payload); + if (!payload.success) return next(new BadRequest(payload.errorMessage)); + res.send(payload.commands); + }); + }); + } +} diff --git a/src/tracers/java/Dockerfile b/src/tracers/java/Dockerfile deleted file mode 100644 index 8aabc07..0000000 --- a/src/tracers/java/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM openjdk:8 - -ARG tag_name - -RUN curl --create-dirs -o /usr/local/lib/algorithm-visualizer.jar -L "https://github.com/algorithm-visualizer/tracers.java/releases/download/${tag_name}/algorithm-visualizer.jar" - -CMD javac -cp /usr/local/lib/algorithm-visualizer.jar Main.java \ - && java -cp /usr/local/lib/algorithm-visualizer.jar:. Main diff --git a/src/tracers/java/JavaTracer.ts b/src/tracers/java/JavaTracer.ts index 043c3c6..e4e5f2b 100644 --- a/src/tracers/java/JavaTracer.ts +++ b/src/tracers/java/JavaTracer.ts @@ -1,6 +1,6 @@ -import { DockerTracer } from 'tracers/DockerTracer'; +import { LambdaTracer } from 'tracers/LambdaTracer'; -export class JavaTracer extends DockerTracer { +export class JavaTracer extends LambdaTracer { constructor() { super('java'); } diff --git a/src/tracers/js/JsTracer.ts b/src/tracers/js/JsTracer.ts index b253a92..a798816 100644 --- a/src/tracers/js/JsTracer.ts +++ b/src/tracers/js/JsTracer.ts @@ -1,26 +1,27 @@ import path from 'path'; -import { download } from 'utils/misc'; import { Release, Tracer } from 'tracers/Tracer'; import express from 'express'; -import { publicDir } from 'config/paths'; export class JsTracer extends Tracer { - readonly tracerPath: string; readonly workerPath: string; + tagName?: string; constructor() { super('js'); - this.tracerPath = path.resolve(publicDir, 'algorithm-visualizer.js'); this.workerPath = path.resolve(__dirname, 'worker.js'); } - build(release: Release) { + async build(release: Release) { const {tag_name} = release; - return download(`https://github.com/algorithm-visualizer/tracers.js/releases/download/${tag_name}/algorithm-visualizer.js`, this.tracerPath); + this.tagName = tag_name; } route(router: express.Router) { - router.get(`/${this.lang}`, (req, res) => res.sendFile(this.tracerPath)); + router.get(`/${this.lang}`, (req, res) => { + if (!this.tagName) throw new Error('JsTracer has not been built yet.'); + const version = this.tagName.slice(1); + res.redirect(`https://unpkg.com/algorithm-visualizer@${version}/dist/algorithm-visualizer.umd.js`); + }); router.get(`/${this.lang}/worker`, (req, res) => res.sendFile(this.workerPath)); } } diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 6074ad2..1b5fe67 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -3,10 +3,12 @@ import fs from 'fs-extra'; import { File } from 'models'; import removeMarkdown from 'remove-markdown'; import * as child_process from 'child_process'; -import { ExecOptions } from 'child_process'; +import { ExecOptions, spawn } from 'child_process'; +import { rootDir } from '../config/paths'; +import path from 'path'; export function download(url: string, localPath: string) { - return axios({url, method: 'GET', responseType: 'stream'}) + return axios({ url, method: 'GET', responseType: 'stream' }) .then(response => new Promise((resolve, reject) => { const writer = fs.createWriteStream(localPath); writer.on('finish', resolve); @@ -17,11 +19,22 @@ export function download(url: string, localPath: string) { export async function pull(dir: string, repo: string, commit = 'origin/master') { if (fs.pathExistsSync(dir)) { - await execute(`git fetch`, {cwd: dir}); + await execute(`git fetch`, { + cwd: dir, + stdout: process.stdout, + stderr: process.stderr, + }); } else { - await execute(`git clone https://github.com/algorithm-visualizer/${repo}.git ${dir}`); + await execute(`git clone https://github.com/algorithm-visualizer/${repo}.git ${dir}`, { + stdout: process.stdout, + stderr: process.stderr, + }); } - await execute(`git reset --hard ${commit}`, {cwd: dir}); + await execute(`git reset --hard ${commit}`, { + cwd: dir, + stdout: process.stdout, + stderr: process.stderr, + }); } export function getDescription(files: File[]) { @@ -30,7 +43,7 @@ export function getDescription(files: File[]) { const lines = readmeFile.content.split('\n'); lines.shift(); while (lines.length && !lines[0].trim()) lines.shift(); - let descriptionLines = []; + const descriptionLines = []; while (lines.length && lines[0].trim()) descriptionLines.push(lines.shift()); return removeMarkdown(descriptionLines.join(' ')); } @@ -38,9 +51,9 @@ export function getDescription(files: File[]) { type ExecuteOptions = ExecOptions & { stdout?: NodeJS.WriteStream; stderr?: NodeJS.WriteStream; -} +}; -export function execute(command: string, {stdout, stderr, ...options}: ExecuteOptions = {}): Promise { +export function execute(command: string, { stdout, stderr, ...options }: ExecuteOptions = {}): Promise { return new Promise((resolve, reject) => { const child = child_process.exec(command, options, (error, stdout, stderr) => { if (error) return reject(error.code ? new Error(stderr) : error); @@ -50,3 +63,18 @@ export function execute(command: string, {stdout, stderr, ...options}: ExecuteOp if (child.stderr && stderr) child.stderr.pipe(stderr); }); } + +export function issueHttpsCertificate() { + const certbotIniPath = path.resolve(rootDir, 'certbot.ini'); + const childProcess = spawn('certbot', ['certonly', '--non-interactive', '--agree-tos', '--config', certbotIniPath]); + childProcess.stdout.pipe(process.stdout); + childProcess.stderr.pipe(process.stderr); + childProcess.on('error', console.error); + childProcess.on('exit', code => { + if (code === 0) { + process.exit(0); + } else { + console.error(new Error(`certbot failed with exit code ${code}.`)); + } + }); +}