From 18d35b5172f31f7a23a898b471a32257b8379b81 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Sat, 10 Apr 2021 20:19:23 +0200 Subject: [PATCH 1/5] Eliminate /token path from server.js, to match Dragos' env. --- bin/server.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) mode change 100644 => 100755 bin/server.js diff --git a/bin/server.js b/bin/server.js old mode 100644 new mode 100755 index 79c2015..13a9396 --- a/bin/server.js +++ b/bin/server.js @@ -21,6 +21,8 @@ let SIGNER = {}; // from cloudapi to our client caller. Effectively this function is a proxy // that solely signs the request as it passes through. function proxy(req, res, cb) { + console.log('### proxy', req.url) + // return data from cloudapi to the client caller function proxyReturn(err, _, res2, data) { if (err && !res2) { @@ -78,9 +80,11 @@ function proxy(req, res, cb) { // secure token. Once the user successfully logs in, the token is returned // through an SSO redirect to token() below. function login(req, res, cb) { + console.log('### login'); + const query = { permissions: '{"cloudapi":["/my/*"]}', - returnto: CONFIG.urls.local + '/token', + returnto: CONFIG.urls.local, now: new Date().toUTCString(), keyid: '/' + CONFIG.key.user + '/keys/' + CONFIG.key.id, nonce: mod_crypto.randomBytes(15).toString('base64') @@ -98,19 +102,7 @@ function login(req, res, cb) { const signature = signer.sign(PRIVATE_KEY, 'base64'); url += '&sig=' + encodeURIComponent(signature); - res.redirect(url, cb); -} - - -// Once a user successfully logs in, they are redirected to here. We convert -// the token that was returned to use as query arg into an X-Auth-Token header -// that is returned to the client caller. This header must be provided by the -// client from now on in order to communicate with Cloudapi. -function token(req, res, cb) { - const token = decodeURIComponent(req.query().split('=')[1]); - res.header('X-Auth-Token', token); - res.send(204); - return cb(); + res.json({ url }); } @@ -169,7 +161,6 @@ function main() { // route HTTP requests to proper functions server.get('/login', login); - server.get('/token', token); server.get(/^/, proxy); server.put(/^/, proxy); From 867243935852cea47e4f65848eee4b2f55e4d126 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Sat, 10 Apr 2021 21:14:09 +0200 Subject: [PATCH 2/5] Changed how paths are handled by server.js, to more closely match the Angular dev environment. New paths: /api/login: redirects to SSO /api: all calls (other than above) are sent to cloudapi /: static content served from static/ All API calls to cloudapi now pass through server.js's HTTP /api, not /. The static/static path is now gone, since it was causing too much trouble. static/ is now a symlink directly to app/dist, which is where a fresh Angular build appears when app/ is built. --- README.md | 35 ++++++++++++++--------------------- bin/server.js | 38 ++++++++++++++++++++++++-------------- static | 1 + static/index.html | 5 ----- static/static | 1 - 5 files changed, 39 insertions(+), 41 deletions(-) create mode 120000 static delete mode 100644 static/index.html delete mode 120000 static/static diff --git a/README.md b/README.md index dbf0dad..39eb91c 100644 --- a/README.md +++ b/README.md @@ -25,46 +25,39 @@ The SSH key used must be the correct format, e.g. generated with: # Endpoints -## GET /static/* +## GET /* This is where all the front-end code goes. All files will be served as-is as -found in that directory. The default is static/index.html. There is no -authentication; all files are public. +found in that directory (by default a symlink to app/dist). The default is +static/index.html. There is no authentication; all files are public. -## GET /login +## GET /api/login Call this endpoint to begin the login cycle. It will redirect you to the SSO login page: an HTTP 302, with a Location header. -## GET /token +## GET/POST/PUT/DELETE/HEAD /api/* -Upon successful login, the SSO login page will redirect to this endpoint. This -endpoint will return a 204, along with a X-Auth-Token header that must be saved -by the front-end code. All subsequent calls should provide this X-Auth-Token -header. - -## Other - -All other calls will be passed through to cloudapi. For these calls to succeed, -they MUST provide the X-Auth-Token header that the /token endpoint returns. +All calls will be passed through to cloudapi. For these calls to succeed, +they MUST provide an X-Auth-Token header, containing the token returned from +SSO. # Interaction cycle -client --- GET /login --------> this server +client --- GET /api/login --------> this server <-- 302 Location #1 ---- client --- GET --> SSO server - <-- 302 Location #2 ---- - -client --- GET --> this server - <-- 204 X-Auth-Token ---- + <-- 302 with token query arg From now on call this server as if it were a cloudapi server (using [cloudapi paths](https://github.com/joyent/sdc-cloudapi/blob/master/docs/index.md#api-introduction)), -always providing the X-Auth-Token. For example, to retrieve a list of packages: +except prefixing any path with "/api". Also always provide the X-Auth-Token. -client --- GET /my/packages --> this server +For example, to retrieve a list of packages: + +client --- GET /api/my/packages --> this server <-- 200 JSON body ------ The most useful cloudapi endpoints to begin with will be ListPackages, diff --git a/bin/server.js b/bin/server.js index 13a9396..ce5b150 100755 --- a/bin/server.js +++ b/bin/server.js @@ -15,6 +15,10 @@ let CLOUDAPI = {}; let CLOUDAPI_HOST = ''; let SIGNER = {}; +const LOGIN_PATH = '/api/login'; +const API_PATH = '/api'; // all calls here go to cloudapi +const API_RE = new RegExp('^' + API_PATH + '/'); +const STATIC_RE = new RegExp('^/'); // Take any HTTP request that has a token, sign that request with an // HTTP-Signature header, and pass it along to cloudapi. Return any response @@ -40,16 +44,19 @@ function proxy(req, res, cb) { // check the X-Auth-Token is present if (req.header('X-Auth-Token') == undefined) { - res.send({"Error": "X-Auth-Token header missing"}); + res.send({'Error': 'X-Auth-Token header missing'}); res.send(401); return cb(); } + // strip off /api from path before forwarding to cloudapi + let url = req.url.substr(API_PATH.length); + // sign the request before forwarding to cloudapi let headers = req.headers; var rs = mod_sdcauth.requestSigner({ sign: SIGNER }); headers.date = rs.writeDateHeader(); - rs.writeTarget(req.method, req.url); + rs.writeTarget(req.method, url); rs.sign(function signedCb(err, authz) { if (err) { @@ -60,10 +67,12 @@ function proxy(req, res, cb) { headers.authorization = authz; const opts = { - path: req.url, + path: url, headers: headers }; +console.dir(opts); + // make the call to cloudapi switch (req.method) { case 'GET': CLOUDAPI.get(opts, proxyReturn); break; @@ -153,20 +162,21 @@ function main() { server.use(mod_restify.authorizationParser()); server.use(mod_restify.bodyReader()); - // where to server static content from - server.get(/^\/static.*/, mod_restify.plugins.serveStatic({ + // login path is /api/login + server.get(LOGIN_PATH, login); + + // all cloudapi calls are proxied through /api + server.get(API_RE, proxy); + server.put(API_RE, proxy); + server.del(API_RE, proxy); + server.post(API_RE, proxy); + server.head(API_RE, proxy); + + // where to serve static content from + server.get(STATIC_RE, mod_restify.plugins.serveStatic({ directory: 'static', default: 'index.html' })); - - // route HTTP requests to proper functions - server.get('/login', login); - - server.get(/^/, proxy); - server.put(/^/, proxy); - server.del(/^/, proxy); - server.post(/^/, proxy); - server.head(/^/, proxy); // enable HTTP server server.listen(CONFIG.server.port, function listening() { diff --git a/static b/static new file mode 120000 index 0000000..ddac10c --- /dev/null +++ b/static @@ -0,0 +1 @@ +app/dist \ No newline at end of file diff --git a/static/index.html b/static/index.html deleted file mode 100644 index e45adde..0000000 --- a/static/index.html +++ /dev/null @@ -1,5 +0,0 @@ - - - Hi! - - diff --git a/static/static b/static/static deleted file mode 120000 index 945c9b4..0000000 --- a/static/static +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file From 3c38a932ed0ed19dc59e0737da43c190df6367e2 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Sat, 10 Apr 2021 23:06:24 +0200 Subject: [PATCH 3/5] Log all requests, using the bunyan format (JSON with certain fields). This is most easily consumed with the bunyan formatter; see README.md for more details. --- README.md | 6 ++++++ bin/server.js | 13 +++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 39eb91c..72737d9 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ The SSH key used must be the correct format, e.g. generated with: node bin/server.js config/prod.json +The server generates a lot of JSON data about every request. This is easier +for a human to handle if they have bunyan installed ("npm install -g bunyan"), +and instead: + + node bin/server.js config/prod.json | bunyan + # Endpoints ## GET /* diff --git a/bin/server.js b/bin/server.js index ce5b150..1dbdd37 100755 --- a/bin/server.js +++ b/bin/server.js @@ -6,6 +6,7 @@ const mod_cueball = require('cueball'); const mod_crypto = require('crypto'); const mod_fs = require('fs'); const mod_sdcauth = require('smartdc-auth'); +const mod_bunyan = require('bunyan'); // Globals that are assigned to by main(). They are used by proxy() and login(). @@ -25,8 +26,6 @@ const STATIC_RE = new RegExp('^/'); // from cloudapi to our client caller. Effectively this function is a proxy // that solely signs the request as it passes through. function proxy(req, res, cb) { - console.log('### proxy', req.url) - // return data from cloudapi to the client caller function proxyReturn(err, _, res2, data) { if (err && !res2) { @@ -71,8 +70,6 @@ function proxy(req, res, cb) { headers: headers }; -console.dir(opts); - // make the call to cloudapi switch (req.method) { case 'GET': CLOUDAPI.get(opts, proxyReturn); break; @@ -89,8 +86,6 @@ console.dir(opts); // secure token. Once the user successfully logs in, the token is returned // through an SSO redirect to token() below. function login(req, res, cb) { - console.log('### login'); - const query = { permissions: '{"cloudapi":["/my/*"]}', returnto: CONFIG.urls.local, @@ -159,9 +154,15 @@ function main() { }; const server = mod_restify.createServer(options); + server.use(mod_restify.requestLogger()); server.use(mod_restify.authorizationParser()); server.use(mod_restify.bodyReader()); + // log requests + server.on('after', mod_restify.auditLogger({ + log: mod_bunyan.createLogger({ name: 'proxy' }) + })); + // login path is /api/login server.get(LOGIN_PATH, login); From db8758cc3079e2fbd0d6622327a91420de1c36eb Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Sun, 11 Apr 2021 00:07:01 +0200 Subject: [PATCH 4/5] Attempt to fix 408 errors from the cueball connection pool. The pool now regularly pings cloudapi's /. Also (temporarily) silenced cueball noise. --- bin/server.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/bin/server.js b/bin/server.js index 1dbdd37..75bd31b 100755 --- a/bin/server.js +++ b/bin/server.js @@ -110,6 +110,20 @@ function login(req, res, cb) { } +// Logging function useful to silence bunyan-based logging systems +function silentLogger() { + let stub = function () {}; + + return { + child: silentLogger, + warn: stub, + trace: stub, + info: stub, + debug: stub + } +} + + // Start up HTTP server and pool of cloudapi clients. // // Read from config file, establish crypto singer needed for requests to @@ -132,8 +146,11 @@ function main() { CLOUDAPI = mod_restify.createStringClient({ url: CONFIG.urls.cloudapi, agent: new mod_cueball.HttpsAgent({ - spares: 0, - maximum: 4, + log: silentLogger(), // temporary + spares: 2, + maximum: 5, + ping: '/', + pingInterval: 5000, // in ms recovery: { default: { timeout: 2000, From 51028156c915873c09af799800ee79689449c5ad Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Sun, 11 Apr 2021 20:51:52 +0200 Subject: [PATCH 5/5] Document installation of app, including in production. Add smf manifest so we can run this in a Joyent-branded zone too. --- README.md | 81 +++++++++++++++++++++++++++++++++++++++---------- smf/run.sh | 4 +++ smf/service.xml | 15 +++++++++ 3 files changed, 84 insertions(+), 16 deletions(-) create mode 100755 smf/run.sh create mode 100644 smf/service.xml diff --git a/README.md b/README.md index 72737d9..be1aa5c 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,74 @@ +# Installing in Production + +Be familiar with the steps in [Installation][] below, since it is needed to +build the Angular app first. + +Once the Angular app is built, provision a small base-64-lts 20.4.0 VM, +connected solely to the external network (aka public Internet). From within +the VM, the following steps are needed: + + pkgin in gmake + mkdir -p /opt/spearhead/portal + +From this repo, copy in bin/, cfg/, smf/, static/ (since this is a symlink, +this means the build in app/dist should be copied into static/ in prod), and \*. +Notably, avoid app/ and node\_modules. In production, adjust the config in +/opt/spearhead/portal/cfg/prod.json. Lastly: + + pushd /opt/spearhead/portal + npm install + svccfg import smf/service.xml + svcadm enable portal + popd + +The application will now be running. + # Installation +First install the server-side libraries: + npm install -# Generate server certificates +Then install the Angular compiler needed for the client-side app: -From within the config/ directory: + npm install -g @angular/cli + pushd app && npm install && popd +## Build the client-side app: + + pushd app && npm run build && popd + +## Generate server certificates + + pushd config openssl genrsa -out key.pem openssl req -new -key key.pem -out csr.pem openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem rm csr.pem + popd -# Configuration +## Configuration -Ensure the config file in config/ matches your details. +Ensure the config file in config/ matches your details. If running in +production, name the config file config/prod.json. + +Relevant configuration attributes: + +- server.port: the port this server will serve the app from +- server.key: path to the private key for TLS +- server.cert: path to the PKIX certificate for TLS +- urls.local: the domain or IP the SSO will redirect back to (aka this server) +- urls.sso: the URL to the SSO +- urls.cloudapi: the URL to cloudapi +- key.user: name of Triton user who has "Registered Developer" permission set +- key.id: SSH fingerprint of Triton user (same as what node-triton uses) +- key.path: path to private key of Triton user The SSH key used must be the correct format, e.g. generated with: ssh-keygen -m PEM -t rsa -C "your@email.address" -# Running the server +## Running the server node bin/server.js config/prod.json @@ -31,18 +80,18 @@ and instead: # Endpoints -## GET /* +## GET /\* This is where all the front-end code goes. All files will be served as-is as -found in that directory (by default a symlink to app/dist). The default is -static/index.html. There is no authentication; all files are public. +found in that directory (by default a symlink from static/ to app/dist). The +default is static/index.html. There is no authentication; all files are public. ## GET /api/login Call this endpoint to begin the login cycle. It will redirect you to the SSO login page: an HTTP 302, with a Location header. -## GET/POST/PUT/DELETE/HEAD /api/* +## GET/POST/PUT/DELETE/HEAD /api/\* All calls will be passed through to cloudapi. For these calls to succeed, they MUST provide an X-Auth-Token header, containing the token returned from @@ -50,12 +99,12 @@ SSO. # Interaction cycle -client --- GET /api/login --------> this server - <-- 302 Location #1 ---- + client --- GET /api/login --------> this server + <-- 302 Location #1 ---- -client --- GET --> SSO server - - <-- 302 with token query arg + client --- GET --> SSO server + + <-- 302 with token query arg From now on call this server as if it were a cloudapi server (using [cloudapi paths](https://github.com/joyent/sdc-cloudapi/blob/master/docs/index.md#api-introduction)), @@ -63,8 +112,8 @@ except prefixing any path with "/api". Also always provide the X-Auth-Token. For example, to retrieve a list of packages: -client --- GET /api/my/packages --> this server - <-- 200 JSON body ------ + client --- GET /api/my/packages --> this server + <-- 200 JSON body ------ The most useful cloudapi endpoints to begin with will be ListPackages, GetPackage, ListImages, GetImage, ListMachines, GetMachine, CreateMachine and diff --git a/smf/run.sh b/smf/run.sh new file mode 100755 index 0000000..9a02084 --- /dev/null +++ b/smf/run.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd /opt/spearhead/portal +/opt/local/bin/node bin/server.js cfg/prod.json & diff --git a/smf/service.xml b/smf/service.xml new file mode 100644 index 0000000..ea68512 --- /dev/null +++ b/smf/service.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +