UNPKG

@sassoftware/viya-serverjs

Version:

Easy to use app server for SAS Viya applications

346 lines (308 loc) 11 kB
/* * ------------------------------------------------------------------------------------ * * Copyright (c) SAS Institute Inc. * * 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. * ---------------------------------------------------------------------------------------- * */ let fs = require('fs'); let debug = require('debug')('service'); let debug2 = require('debug')('tls'); // let isDocker = require('is-docker'); let Hapi = require('@hapi/hapi'); let H202 = require('@hapi/h2o2'); // const { isSameSiteNoneCompatible } = require('should-send-same-site-none'); let NodeCache = require("node-cache-promise"); let Vision = require('@hapi/vision'); let inert = require('@hapi/inert'); let selfsigned = require('selfsigned'); import setupAuth from './plugins/setupAuth'; let os = require('os'); function iService (userRouteTable, useDefault, asset, allAppEnv, serverMode, userInfo) { // process.env.APPHOST_ADDR = process.env.APPHOST; const init = async () => { if (process.env.APPHOST === '*') { process.env.APPHOST = os.hostname(); } let defaultMaxBytes = 10485760; let maxBytes; if (isNaN(process.env.PAYLOADMAXBYTES)) { maxBytes = defaultMaxBytes; } else { maxBytes = Number(process.env.PAYLOADMAXBYTES); } let isSameSite = 'None'; let isSecure = false; if (process.env.SAMESITE != null) { let [s1, s2] = process.env.SAMESITE.split(','); isSameSite = s1; isSecure = s2 === 'secure' ? true : false; if (process.env.HTTPS !== 'true') { isSecure = false; } } let sConfig = { port: process.env.APPPORT, host: process.env.APPHOST, state: { isSameSite: isSameSite, isSecure : isSecure, }, routes: { payload: { maxBytes: maxBytes }, cors: { origin : ['*'], credentials: true, "headers": ["Accept", "Authorization", "Content-Type", "If-None-Match", "Accept-language"] /* 'Access-Control-Allow-Methods': ['GET', 'POST', 'OPTIONS'], additionalHeaders : ['multipart/form-data', 'content-disposition'], additionalExposedHeaders : ['location'], */ } }, }; if (process.env.HAPIDEBUG === 'YES') { sConfig.debug = { request: '*' }; } debug(JSON.stringify(sConfig, null,4)); if (process.env.HTTPS === 'true') { sConfig.tls = await getCertificates(); debug('Setup of SSL certificates completed'); } else { debug('Running with no SSL certificates'); } if (asset !== null) { sConfig.routes.files= { relativeTo: asset }; } debug2( `Application information: APPLOC : ${process.env.APPLOC} APPENTRY: ${process.env.APPENTRY} ` ); let hapiServer = Hapi.server(sConfig); /* const cache = hapiServer.cache({ segment: 'sessions', expiresIn: 3 * 24 * 60 * 60 * 1000 }); hapiServer.app.cache = cache; */ let nodeCacheOptions = { stdTTL : 24*60*60*1000, checkPeriod : 3600, errorOnMissing: true, useClones : false, deleteOnExpire: true, }; let storeCache = new NodeCache(nodeCacheOptions); hapiServer.app.cache = storeCache; // common plugins let visionOptions = { engines : { html: require('handlebars') }, relativeTo: __dirname, path : '.', }; await hapiServer.register(Vision); hapiServer.views(visionOptions); await hapiServer.register(inert); if (process.env.HTTPS === 'true') { await hapiServer.register({ plugin: require('hapi-require-https'), options: {} }); } // register H202 for proxy handling // https://hapi.dev/module/h2o2/api/?v=10.0.1 await hapiServer.register(H202); /* await hapiServer.register({ plugin : require('hapi-pino'), options: { prettyPrint: process.env.NODE_ENV !== 'production', level : process.env.LOGLEVEL == null ? 'silent' : process.env.LOGLEVEL, }, }); */ // setup authentication related plugins let options = { serverMode : serverMode, authFlow : process.env.AUTHFLOW, host : process.env.VIYA_SERVER, isSameSite : isSameSite, isSecure : isSecure, ns : (allAppEnv.LOGONPAYLOAD != null) ? allAppEnv.LOGONPAYLOAD.ns : null, nsHost : (allAppEnv.LOGONPAYLOAD != null) ? allAppEnv.LOGONPAYLOAD.nsHost : null, redirect : process.env.REDIRECT, clientId : process.env.CLIENTID, clientSecret : process.env.CLIENTSECRET, redirectTo : `/${process.env.APPNAME}/logon`, allAppEnv : allAppEnv, useHapiCookie : true, appName : process.env.APPNAME, appHost : process.env.APPHOST, appPort : process.env.APPPORT, userRouteTable: userRouteTable, useDefault : useDefault, /* not used - left here for potential reuse */ userInfo : userInfo, https : process.env.HTTPS, authDefault : false, /* set later in setDefaultRoutes */ authLogon : false /* set later in setDefaultRoutes */ }; debug2('Options',options); if (process.env.AUTHFLOW != null) { await setupAuth(hapiServer, options); if (process.env.PREAUTH === 'YES') { console.log('Preauth enabled'); hapiServer.ext('onPreAuth', (request, h) => { debugger; if (!request.auth.isAuthenticated && !request.path.startsWith(`/login`)) { const redirectTo = `${request.path}?${new URLSearchParams(request.query).toString()}`; console.log('Redirect to login', {redirectTo}); debugger; return h.redirect(`/login`).takeover(); } return h.continue; }); } } console.log('Plugin', process.env.PLUGIN); if (process.env.PLUGIN === 'hapi-swagger' && serverMode ==='api') { let swaggerOptions = { "info": { "title" : `API for ${process.env.APPNAME}`, "version" : "0.0.1", "description": "This document was auto-generated at run time" }, "schemes" : ["http", "https"], "cors" : true, "debug" : true, "jsonPath" : `/${options.appName}/swagger.json`, "jsonRoutePath" : `/${options.appName}/swagger.json`, "documentationPage": true, "documentationPath": `/${options.appName}/documentation`, "swaggerUI" : true, "swaggerUIPath" : `/${options.appName}/swaggerui`, auth : options.authDefault }; if (userInfo != null) { let override = userInfo(options, 'SWAGGEROPTIONS'); swaggerOptions = {...swaggerOptions, ...override}; } debug('Swagger Options:' ,swaggerOptions); await hapiServer.register({ plugin: serverMode, options: swaggerOptions }); } else if (process.env.PLUGIN == 'hapi-openapi' && serverMode === 'api') { console.log('hapi-openapi', 'coming soon'); } // // Start server // // eslint-disable-next-line no-unused-vars let allRoutes = hapiServer.table(); await hapiServer.start(); let hh = hapiServer.info.uri; hh = hh.replace(/0.0.0.0/, 'localhost'); console.log('===================================================================================='); console.log('Server Start Time: ', Date()); let msg = options.serverMode === 'app' ? `Visit ${hh}/${process.env.APPNAME} to access application` : `Visit ${hh}/${process.env.APPNAME}/api to access swagger`; console.log('\x1b[1m%s\x1b[0m',msg); console.log('NOTE: If running in container use the exported port'); process.env.APPSERVER = `${hh}/${process.env.APPNAME}`; process.env.HEALTH = 'true'; console.log('===================================================================================='); }; process.on('unhandledRejection', (err) => { console.log(err); process.exit(1); }); init(); } async function getCertificates () { let options = null; let tlsdir = process.env.SSLCERT; if (tlsdir != null && tlsdir.trim().length > 0) { console.log('ssl CERTIFICATES', tlsdir); if (fs.existsSync(`${tlsdir}/key.pem`) === true) { options = {}; options.key = fs.readFileSync(`${tlsdir}/key.pem`, { encoding: 'utf8' }); options.cert = fs.readFileSync(`${tlsdir}/crt.pem`, { encoding: 'utf8' }); if (fs.existsSync(`${tlsdir}/ca.pem`) === true) { options.ca = fs.readFileSync(`${tlsdir}/ca.pem`, { encoding: 'utf8' }); } options.rejectUnauthorized= true; } } else { console.log('No SSL certificates found, generating self-signed certificates'); options = await getTls(); options.rejectUnauthorized= false; } return options; } async function getTls () { let options = { keySize : 2048, days : 360, algorithm : "sha256", clientCertificate: true, extensions : {}, }; let subjt = process.env.TLS_CREATE.replaceAll('"', '').trim(); let subj = subjt.split(','); let d = {}; subj.map(c => { let r = c.split(':'); d[ r[ 0 ] ] = r[ 1 ]; return { value: r[ 1 ] }; }); // TLS_CREATE=C:US,ST:NC,L:Cary,O:SAS Institute,OU:STO,CN:localhost,ALT:na.sas.com let attr = [ { name : 'commonName', value: d.CN /*process.env.APPHOST*/, }, { name : 'countryName', value: d.C }, { shortName: 'ST', value : d.ST }, { name : 'localityName', value: d.L, }, { name : 'organizationName', value: d.O }, { shortName: 'OU', value : d.OU } ]; options.extensions.altNames = [ // { type: 6, value: `http://${process.env.APPHOST}:${process.env.APPPORT}/${process.env.APPNAME}` }, { type: 6, value: `https://${process.env.APPHOST}:${process.env.APPPORT}/${process.env.APPNAME}` }, { type: 6, value: `https://${process.env.APPHOST}:${process.env.APPPORT}/${process.env.APPNAME}/api` }, { type: 6, value: `https://${process.env.APPHOST}:${process.env.APPPORT}/${process.env.APPNAME}/logon` }, { type: 6, value: `https://${process.env.APPHOST}/${process.env.APPNAME}` }, { type: 6, value: `https://${process.env.APPHOST}/${process.env.APPNAME}/api` }, { type: 6, value: `https://${process.env.APPHOST}/${process.env.APPNAME}/logon` }, ]; debug('tls options ', JSON.stringify(options, null,4)); let pems = selfsigned.generate(attr, options); let tls = { cert: pems.cert, key : pems.private }; return tls; } export default iService;