Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
f3adbfba0f
108
README.md
108
README.md
@ -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
|
||||
|
||||
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
|
||||
|
||||
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 /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 from static/ 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
|
||||
<-- 302 Location #1 ----
|
||||
client --- GET /api/login --------> this server
|
||||
<-- 302 Location #1 ----
|
||||
|
||||
client --- GET <Location #1> --> SSO server
|
||||
<separate SSO cycle>
|
||||
<-- 302 Location #2 ----
|
||||
|
||||
client --- GET <Location #2> --> this server
|
||||
<-- 204 X-Auth-Token ----
|
||||
client --- GET <Location #1> --> SSO server
|
||||
<separate SSO cycle>
|
||||
<-- 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
|
||||
<-- 200 JSON body ------
|
||||
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,
|
||||
GetPackage, ListImages, GetImage, ListMachines, GetMachine, CreateMachine and
|
||||
|
75
bin/server.js
Normal file → Executable file
75
bin/server.js
Normal file → Executable file
@ -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().
|
||||
@ -15,6 +16,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
|
||||
@ -38,16 +43,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) {
|
||||
@ -58,7 +66,7 @@ function proxy(req, res, cb) {
|
||||
headers.authorization = authz;
|
||||
|
||||
const opts = {
|
||||
path: req.url,
|
||||
path: url,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
@ -80,7 +88,7 @@ function proxy(req, res, cb) {
|
||||
function login(req, res, cb) {
|
||||
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 +106,21 @@ function login(req, res, cb) {
|
||||
const signature = signer.sign(PRIVATE_KEY, 'base64');
|
||||
url += '&sig=' + encodeURIComponent(signature);
|
||||
|
||||
res.redirect(url, cb);
|
||||
res.json({ url });
|
||||
}
|
||||
|
||||
|
||||
// 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();
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -136,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,
|
||||
@ -158,24 +171,30 @@ function main() {
|
||||
};
|
||||
|
||||
const server = mod_restify.createServer(options);
|
||||
server.use(mod_restify.requestLogger());
|
||||
server.use(mod_restify.authorizationParser());
|
||||
server.use(mod_restify.bodyReader());
|
||||
|
||||
// where to server static content from
|
||||
server.get(/^\/static.*/, mod_restify.plugins.serveStatic({
|
||||
// log requests
|
||||
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',
|
||||
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
|
||||
server.listen(CONFIG.server.port, function listening() {
|
||||
|
4
smf/run.sh
Executable file
4
smf/run.sh
Executable 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
15
smf/service.xml
Normal 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,5 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
Hi!
|
||||
</body>
|
||||
</html>
|
@ -1 +0,0 @@
|
||||
.
|
Reference in New Issue
Block a user