Merge remote-tracking branch 'origin/master'

This commit is contained in:
Dragos 2021-04-26 15:34:26 +03:00
commit f3adbfba0f
7 changed files with 145 additions and 64 deletions

108
README.md
View File

@ -1,71 +1,119 @@
# 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 # Installation
First install the server-side libraries:
npm install 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 genrsa -out key.pem
openssl req -new -key key.pem -out csr.pem openssl req -new -key key.pem -out csr.pem
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
rm csr.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: The SSH key used must be the correct format, e.g. generated with:
ssh-keygen -m PEM -t rsa -C "your@email.address" ssh-keygen -m PEM -t rsa -C "your@email.address"
# Running the server ## Running the server
node bin/server.js config/prod.json 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 # Endpoints
## GET /static/* ## GET /\*
This is where all the front-end code goes. All files will be served as-is as 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 found in that directory (by default a symlink from static/ to app/dist). The
authentication; all files are public. 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 Call this endpoint to begin the login cycle. It will redirect you to the SSO
login page: an HTTP 302, with a Location header. 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 All calls will be passed through to cloudapi. For these calls to succeed,
endpoint will return a 204, along with a X-Auth-Token header that must be saved they MUST provide an X-Auth-Token header, containing the token returned from
by the front-end code. All subsequent calls should provide this X-Auth-Token SSO.
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.
# Interaction cycle # Interaction cycle
client --- GET /login --------> this server client --- GET /api/login --------> this server
<-- 302 Location #1 ---- <-- 302 Location #1 ----
client --- GET <Location #1> --> SSO server client --- GET <Location #1> --> SSO server
<separate SSO cycle> <separate SSO cycle>
<-- 302 Location #2 ---- <-- 302 with token query arg
client --- GET <Location #2> --> this server
<-- 204 X-Auth-Token ----
From now on call this server as if it were a cloudapi server (using [cloudapi 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)), 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:
<-- 200 JSON body ------
client --- GET /api/my/packages --> this server
<-- 200 JSON body ------
The most useful cloudapi endpoints to begin with will be ListPackages, The most useful cloudapi endpoints to begin with will be ListPackages,
GetPackage, ListImages, GetImage, ListMachines, GetMachine, CreateMachine and GetPackage, ListImages, GetImage, ListMachines, GetMachine, CreateMachine and

75
bin/server.js Normal file → Executable file
View File

@ -6,6 +6,7 @@ const mod_cueball = require('cueball');
const mod_crypto = require('crypto'); const mod_crypto = require('crypto');
const mod_fs = require('fs'); const mod_fs = require('fs');
const mod_sdcauth = require('smartdc-auth'); 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(). // Globals that are assigned to by main(). They are used by proxy() and login().
@ -15,6 +16,10 @@ let CLOUDAPI = {};
let CLOUDAPI_HOST = ''; let CLOUDAPI_HOST = '';
let SIGNER = {}; 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 // 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 // HTTP-Signature header, and pass it along to cloudapi. Return any response
@ -38,16 +43,19 @@ function proxy(req, res, cb) {
// check the X-Auth-Token is present // check the X-Auth-Token is present
if (req.header('X-Auth-Token') == undefined) { 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); res.send(401);
return cb(); 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 // sign the request before forwarding to cloudapi
let headers = req.headers; let headers = req.headers;
var rs = mod_sdcauth.requestSigner({ sign: SIGNER }); var rs = mod_sdcauth.requestSigner({ sign: SIGNER });
headers.date = rs.writeDateHeader(); headers.date = rs.writeDateHeader();
rs.writeTarget(req.method, req.url); rs.writeTarget(req.method, url);
rs.sign(function signedCb(err, authz) { rs.sign(function signedCb(err, authz) {
if (err) { if (err) {
@ -58,7 +66,7 @@ function proxy(req, res, cb) {
headers.authorization = authz; headers.authorization = authz;
const opts = { const opts = {
path: req.url, path: url,
headers: headers headers: headers
}; };
@ -80,7 +88,7 @@ function proxy(req, res, cb) {
function login(req, res, cb) { function login(req, res, cb) {
const query = { const query = {
permissions: '{"cloudapi":["/my/*"]}', permissions: '{"cloudapi":["/my/*"]}',
returnto: CONFIG.urls.local + '/token', returnto: CONFIG.urls.local,
now: new Date().toUTCString(), now: new Date().toUTCString(),
keyid: '/' + CONFIG.key.user + '/keys/' + CONFIG.key.id, keyid: '/' + CONFIG.key.user + '/keys/' + CONFIG.key.id,
nonce: mod_crypto.randomBytes(15).toString('base64') nonce: mod_crypto.randomBytes(15).toString('base64')
@ -98,19 +106,21 @@ function login(req, res, cb) {
const signature = signer.sign(PRIVATE_KEY, 'base64'); const signature = signer.sign(PRIVATE_KEY, 'base64');
url += '&sig=' + encodeURIComponent(signature); url += '&sig=' + encodeURIComponent(signature);
res.redirect(url, cb); res.json({ url });
} }
// Once a user successfully logs in, they are redirected to here. We convert // Logging function useful to silence bunyan-based logging systems
// the token that was returned to use as query arg into an X-Auth-Token header function silentLogger() {
// that is returned to the client caller. This header must be provided by the let stub = function () {};
// client from now on in order to communicate with Cloudapi.
function token(req, res, cb) { return {
const token = decodeURIComponent(req.query().split('=')[1]); child: silentLogger,
res.header('X-Auth-Token', token); warn: stub,
res.send(204); trace: stub,
return cb(); info: stub,
debug: stub
}
} }
@ -136,8 +146,11 @@ function main() {
CLOUDAPI = mod_restify.createStringClient({ CLOUDAPI = mod_restify.createStringClient({
url: CONFIG.urls.cloudapi, url: CONFIG.urls.cloudapi,
agent: new mod_cueball.HttpsAgent({ agent: new mod_cueball.HttpsAgent({
spares: 0, log: silentLogger(), // temporary
maximum: 4, spares: 2,
maximum: 5,
ping: '/',
pingInterval: 5000, // in ms
recovery: { recovery: {
default: { default: {
timeout: 2000, timeout: 2000,
@ -158,24 +171,30 @@ function main() {
}; };
const server = mod_restify.createServer(options); const server = mod_restify.createServer(options);
server.use(mod_restify.requestLogger());
server.use(mod_restify.authorizationParser()); server.use(mod_restify.authorizationParser());
server.use(mod_restify.bodyReader()); server.use(mod_restify.bodyReader());
// where to server static content from // log requests
server.get(/^\/static.*/, mod_restify.plugins.serveStatic({ server.on('after', mod_restify.auditLogger({
log: mod_bunyan.createLogger({ name: 'proxy' })
}));
// 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', directory: 'static',
default: 'index.html' default: 'index.html'
})); }));
// route HTTP requests to proper functions
server.get('/login', login);
server.get('/token', token);
server.get(/^/, proxy);
server.put(/^/, proxy);
server.del(/^/, proxy);
server.post(/^/, proxy);
server.head(/^/, proxy);
// enable HTTP server // enable HTTP server
server.listen(CONFIG.server.port, function listening() { server.listen(CONFIG.server.port, function listening() {

4
smf/run.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
cd /opt/spearhead/portal
/opt/local/bin/node bin/server.js cfg/prod.json &

15
smf/service.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='portal:default'>
<service name='spearhead/portal' type='service' version='1'>
<create_default_instance enabled='false' />
<single_instance />
<method_context>
<method_credential user='root' group='root' />
</method_context>
<exec_method name='start' type='method' exec='/opt/spearhead/portal/smf/run.sh' timeout_seconds='60'/>
<exec_method name='stop' type='method' exec=':kill' timeout_seconds='60'/>
</service>
</service_bundle>

1
static Symbolic link
View File

@ -0,0 +1 @@
app/dist

View File

@ -1,5 +0,0 @@
<html>
<body>
Hi!
</body>
</html>

View File

@ -1 +0,0 @@
.