UNPKG

@celastrina/http

Version:

HTTP Function Package for Celastrina

1,234 lines (1,233 loc) 94.7 kB
/* * Copyright (c) 2021, KRI, LLC. * * MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ /** * @author Robert R Murrell * @copyright Robert R Murrell * @license MIT */ "use strict"; const axios = require("axios").default; const {v4: uuidv4} = require("uuid"); const moment = require("moment"); const jwt = require("jsonwebtoken"); const jwkToPem = require("jwk-to-pem"); const cookie = require("cookie"); const {CelastrinaError, CelastrinaValidationError, AddOn, LOG_LEVEL, Configuration, Subject, Sentry, Algorithm, AES256Algorithm, Cryptography, RoleFactory, RoleFactoryParser, Context, BaseFunction, ValueMatch, AttributeParser, ConfigLoader, Authenticator, instanceOfCelastrinaType, AddOnEvent} = require("@celastrina/core"); /** * @typedef _AzureRequestBinging * @property {string} originalUrl * @property {string} method * @property {Object} query * @property {Object} headers * @property {Object} params * @property {Object} body * @property {string} rawBody */ /** * @typedef _AzureResponseBinging * @property {Object} headers * @property {number} status * @property {Object} body * @property {string} rawBody * @property {Array.<Object>} cookies */ /** * @typedef _AZLogger * @function error * @function warn * @function info * @function verbose */ /** * @typedef _TraceContext * @property {string} traceparent */ /** * @typedef _ExecutionContext * @property {string} invocationId * @property {string} functionName * @property {string} functionDirectory */ /** * @typedef _AzureFunctionContext * @property {_ExecutionContext} executionContext * @property {_TraceContext} traceContext * @property {_AZLogger} log * @property {Object} bindings * @property {_AzureRequestBinging} req * @property {_AzureResponseBinging} res * @property {Object} bindingData */ /** * @typedef _jwtpayload * @property {string} aud * @property {string} sub * @property {string} oid * @property {string} iss * @property {number} iat * @property {number} exp * @property {string} nonce */ /** * @typedef _jwt * @property {_jwtpayload} payload * @typedef _ClaimsPayload * @property {moment.Moment} issued * @property {moment.Moment} expires * @property {string} token * @property {string} audience * @property {string} subject * @property {string} issuer */ /** * @typedef {Object} JWKSKEY * @property {string} [kid] * @property {string} [kty] * @property {string} [x5c] * @property {string} [e] * @property {string} [n] * @property {string} [x] * @property {string} [y] * @property {string} [crv] */ /** * @typedef {Object} JWKS * @property {(null|string)} [issuer] * @property {string} type * @property {JWKSKEY} key */ /** * Cookie * @author Robert R Murrell */ class Cookie { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/Cookie#", type: "celastrinajs.http.Cookie"};} /** * @param {string} name * @param {(null|string)} [value=null] * @param {Object} [options={}] * @param {boolean} [dirty=false] */ constructor(name, value = null, options = {}, dirty = false, ) { if(typeof name !== "string" || name.trim().length === 0) throw CelastrinaValidationError.newValidationError("Invalid String. Attribute 'name' cannot be undefined, null, or zero length.", "cookie.name"); this._name = name.trim(); this._value = value; this._options = options; this._dirty = dirty; } /**@return{boolean}*/get doSetCookie() {return this._dirty}; /**@return{string}*/get name() {return this._name;} /**@return{string}*/get parseValue() { if(this._value == null) return ""; else return this._value; } /**@return{null|string}*/get value() {return this._value;} /**@param{null|string}value*/set value(value) { this._value = value; this._dirty = true; } /**@return{Object}*/get options() {return this._options;} /**@param{Object}options*/set options(options) { if(options == null || typeof options === "undefined") options = {}; this._options = options; this._dirty = true; } /** * @param {string} name * @param {*} value */ setOption(name, value) { this._options[name] = value; this._dirty = true; } /** * @returns {string} */ serialize() { return cookie.serialize(this._name, this.parseValue, this._options); } /** * @return {Promise<{name: string, value: string}>} */ async toAzureCookie() { let _obj = {name: this._name, value: this.parseValue}; Object.assign(_obj, this._options); return _obj; } /**@param{number}age*/set maxAge(age) {this.setOption("maxAge", age);} /**@param{Date}date*/set expires(date) {this.setOption("expires", date);} /**@param{boolean}http*/set httpOnly(http) {this.setOption("httpOnly", http);} /**@param{string}domain*/set domain(domain) {this.setOption("domain", domain);} /**@param{string}path*/set path(path) {this.setOption("path", path);} /**@param{boolean}secure*/set secure(secure) {this.setOption("secure", secure);} /**@param("lax"|"none"|"strict")value*/set sameSite(value) {this.setOption("sameSite", value)}; /**@return{number}*/get maxAge() {return this._options["maxAge"];} /**@return{Date}*/get expires() {return this._options["expires"];} /**@return{boolean}*/get httpOnly() {return this._options["httpOnly"];} /**@return{string}*/get domain() {return this._options["domain"];} /**@return{string}*/get path() {return this._options["path"];} /**@return{boolean}*/get secure() {return this._options["secure"];} /**@return("lax"|"none"|"strict")*/get sameSite() {return this._options["sameSite"];}; /**@param {string} value*/encodeStringToValue(value) {this.value = Buffer.from(value).toString("base64");} /**@param {Object} _object*/encodeObjectToValue(_object) {this.encodeStringToValue(JSON.stringify(_object));} /**@return{string}*/decodeStringFromValue() {return Buffer.from(this.value).toString("ascii");} /**@return{any}*/decodeObjectFromValue() {return JSON.parse(this.decodeStringFromValue());} delete() { this.value = null; let _epoch = moment("1970-01-01T00:00:00Z"); this.expires = _epoch.utc().toDate(); } /** * @param {string} name * @param {(null|string)} [value=null] * @param {Object} [options={}] * @returns {Cookie} A new cookie whos dirty marker is set to 'true', such that doSerializeCookie will generte a * value to the Set-Cookie header. */ static newCookie(name, value = null, options = {}) { return new Cookie(name, value, options, true); } /** * @param {string} name * @param {(null|string)} [value=null] * @param {Object} [options={}] * @returns {Promise<Cookie>} A new cookie whos dirty marker is set to 'false', such that doSerializeCookie will * NOT generte a value to the Set-Cookie header. */ static async loadCookie(name, value = null, options = {}) { return new Cookie(name, value, options); } /** * @param {string} value * @param {Array.<Cookie>} [results=[]]; * @returns {Promise<Array.<Cookie>>} A new cookie whos dirty marker is set to 'false', such that doSerializeCookie * will NOT generte a value to the Set-Cookie header. */ static async parseCookies(value,results = []) { let _cookies = cookie.parse(value); for(let _name in _cookies) { if(_cookies.hasOwnProperty(_name)) { results.unshift(new Cookie(_name, _cookies[_name])); } } return results; } } /** * JwtSubject * @author Robert R murrell */ class JwtSubject { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/JwtSubject#", type: "celastrinajs.http.JwtSubject"};} static PROP_JWT_HEADER = "celastrinajs.jwt.header"; static PROP_JWT_SIGNATURE = "celastrinajs.jwt.signature"; static PROP_JWT_NONCE = "nonce"; static PROP_JWT_TOKEN = "celastrinajs.jwt"; static PROP_JWT_AUD = "aud"; static PROP_JWT_ISS = "iss"; static PROP_JWT_ISSUED = "iat"; static PROP_JWT_NOTBEFORE = "nbf"; static PROP_JWT_EXP = "exp"; /** * @param {Subject} subject */ constructor(subject) { /**@type{Subject}*/this._subject = subject } /**@return{Subject}*/get subject() {return this._subject;} /**@return{Object}*/get header() {return this._subject.getClaimSync(JwtSubject.PROP_JWT_HEADER);} /**@return{Object}*/get signature() {return this._subject.getClaimSync(JwtSubject.PROP_JWT_SIGNATURE);} /**@return{string}*/get token() {return this._subject.getClaimSync(JwtSubject.PROP_JWT_TOKEN);} /**@return{string}*/get nonce(){return this._subject.getClaimSync(JwtSubject.PROP_JWT_NONCE);} /**@return{string}*/get audience() {return this._subject.getClaimSync(JwtSubject.PROP_JWT_AUD);} /**@return{string}*/get issuer(){return this._subject.getClaimSync(JwtSubject.PROP_JWT_ISS);} /**@return{moment.Moment}*/get issued(){return moment.unix(this._subject.getClaimSync(JwtSubject.PROP_JWT_ISSUED));} /**@return{moment.Moment}*/get notBefore(){ //optional payload so we must check. let _nbf = this._subject.getClaimSync(JwtSubject.PROP_JWT_NOTBEFORE); if(_nbf != null) _nbf = moment.unix(_nbf); return _nbf; } /**@return{moment.Moment}*/get expires(){return moment.unix(this._subject.getClaimSync(JwtSubject.PROP_JWT_EXP));} /**@return{boolean}*/get expired(){ return moment().isSameOrAfter(this.expires);} /** * @param {undefined|null|object}headers * @param {string}[name="Authorization] * @param {string}[scheme="Bearer "] * @return {Promise<object>} */ async setAuthorizationHeader(headers, name = "Authorization", scheme = "Bearer ") { if(typeof headers === "undefined" || headers == null) headers = {}; headers[name] = scheme + this._subject.getClaimSync(JwtSubject.PROP_JWT_TOKEN); return headers; } /** * @param {string}name * @param {null|string}defaultValue * @return {Promise<number|string|Array.<string>>} */ async getHeader(name, defaultValue = null) { let header = this.header[name]; if(typeof header === "undefined" || header == null) header = defaultValue; return header; } /** * @param {Subject} subject * @param {string} token * @return {Promise<JwtSubject>} */ static async decode(subject, token) {return JwtSubject.decodeSync(subject, token);} /** * @param {Subject} subject * @param {string} token * @return {JwtSubject} */ static decodeSync(subject, token) { if(typeof token !== "string" || token.trim().length === 0) throw CelastrinaError.newError("Not Authorized.", 401); /** @type {null|Object} */let decoded = /** @type {null|Object} */jwt.decode(token, {complete: true}); if(typeof decoded === "undefined" || decoded == null) throw CelastrinaError.newError("Not Authorized.", 401); subject.addClaims(decoded.payload); subject.addClaim(JwtSubject.PROP_JWT_HEADER, decoded.header); subject.addClaim(JwtSubject.PROP_JWT_SIGNATURE, decoded.signature); subject.addClaim(JwtSubject.PROP_JWT_TOKEN, token); return new JwtSubject(subject); } /** * @param {Subject} subject * @param {Object} decoded * @param {String} token * @return {Promise<JwtSubject>} */ static async wrap(subject, decoded, token) { subject.addClaims(decoded.payload); subject.addClaim(JwtSubject.PROP_JWT_HEADER, decoded.header); subject.addClaim(JwtSubject.PROP_JWT_SIGNATURE, decoded.signature); subject.addClaim(JwtSubject.PROP_JWT_TOKEN, token); return new JwtSubject(subject); } } /** * Issuer * @abstract * @author Robert R Murrell */ class Issuer { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/Issuer#", type: "celastrinajs.http.Issuer"};} /** * @param {null|string} issuer * @param {(Array.<string>|null)} [audiences=null] * @param {(Array.<string>|null)} [assignments=[]] The role assignments to escalate the subject to if the JWT token * is valid for this issuer. * @param {boolean} [validateNonce=false] */ constructor(issuer = null, audiences = null, assignments = null, validateNonce = false) { this._issuer = issuer; this._audiences = audiences; this._assignments = assignments; this._validateNonce = validateNonce; } /**@return{string}*/get issuer(){return this._issuer;} /**@param{string}issuer*/set issuer(issuer){this._issuer = issuer;} /**@return{Array.<string>}*/get audiences(){return this._audiences;} /**@param{Array.<string>}audience*/set audiences(audience){this._audiences = audience;} /**@return{Array<string>}*/get assignments(){return this._assignments;} /**@param{Array<string>}assignments*/set assignments(assignments){this._assignments = assignments;} /**@return{boolean}*/get validateNonce() {return this._validateNonce;} /**@param{boolean}validate*/set validateNonce(validate) {return this._validateNonce = validate;} /** * @param {HTTPContext} context * @param {JwtSubject} subject * @return {Promise<*>} * @abstract */ async getKey(context, subject) {throw CelastrinaError.newError("Not Implemented", 501);} /** * @param {HTTPContext} context * @param {JwtSubject} subject * @return {Promise<(null|string)>} */ async getNonce(context, subject) {return null;} /** * @param {HTTPContext} context * @param {JwtSubject} _jwt * @return {Promise<{verified:boolean,assignments?:(null|Array<string>)}>} */ async verify(context, _jwt) { if(_jwt.issuer === this._issuer) { try { let decoded = jwt.verify(_jwt.token, await this.getKey(context, _jwt), {algorithm: "RSA"}); if(typeof decoded === "undefined" || decoded == null) { context.log("Failed to verify token.", LOG_LEVEL.THREAT, "Issuer.verify(context, _jwt)"); return {verified: false}; } if(this._audiences != null) { if(!this._audiences.includes(_jwt.audience)) { context.log("Subject '" + _jwt.subject.id + "' with audience '" + _jwt.audience + "' failed to match audiences.", LOG_LEVEL.THREAT, "Issuer.verify(context, _jwt)"); return {verified: false}; } } if(this._validateNonce) { let nonce = await this.getNonce(context, _jwt); if(typeof nonce === "string" && nonce.trim().length > 0) { if(_jwt.nonce !== nonce) { context.log("Subject '" + _jwt.subject.id + "' failed to pass nonce validation.", LOG_LEVEL.THREAT, "Issuer.verify(context, _jwt)"); return {verified: false}; } } } return {verified: true, assignments: this._assignments}; } catch(exception) { context.log("Exception authenticating JWT: " + exception, LOG_LEVEL.THREAT, "Issuer.verify(context, _jwt)"); return {verified: false}; } } else return {verified: false}; } } /** * LocalJwtIssuer * @author Robert R Murrell */ class LocalJwtIssuer extends Issuer { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/LocalJwtIssuer#", type: "celastrinajs.http.LocalJwtIssuer"};} /** * @param {(null|string)} issuer * @param {(null|string)} key * @param {(Array.<string>|null)} [audiences=null] * @param {(Array.<string>|null)} [assignments=[]] The role assignments to escalate the subject to if the JWT token * is valid for this issuer. * @param {boolean} [validateNonce=false] */ constructor(issuer = null, key = null, audiences = null, assignments = null, validateNonce = false) { super(issuer, audiences, assignments, validateNonce); this._key = key; } /** * @param {HTTPContext} context * @param {JwtSubject} subject * @return {Promise<*>} */ async getKey(context, subject) { return this._key; } /**@return{string}*/get key() {return this._key;} /**@param{string}key*/set key(key) {this._key = key;} } /** * OpenIDJwtValidator * @description Use the following OpenID IDP's for OpenIDJwtValidator * <ul> * <li>Microsoft Azure AD IDP: * https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration</li> * <li>Microsoft Azure ADB2C IDP: https://[tenant name].b2clogin.com/[tenant * name].onmicrosoft.com/{claim[tfp]}/v2.0/.well-known/openid-configuration</li> * </ul> * All values in curly-brace {} will be replaced with a value from the claim name {claim} in the decoded * JWT. * @author Robert R Murrell */ class OpenIDJwtIssuer extends Issuer { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/OpenIDJwtIssuer#", type: "celastrinajs.http.OpenIDJwtIssuer"};} /** * @param {null|string} issuer * @param {null|string} configUrl * @param {(Array.<string>|null)} [audiences=null] * @param {(Array.<string>|null)} [assignments=[]] The role assignments to escalate the subject to if the JWT token * is valid for this issuer. * @param {boolean} [validateNonce=false] */ constructor(issuer = null, configUrl = null, audiences = null, assignments = null, validateNonce = false) { super(issuer, audiences, assignments, validateNonce); this._configUrl = configUrl; } get configURL() {return this._configUrl;} set configURL(url) {this._configUrl = url;} /** * @param {HTTPContext} context * @param {JwtSubject} _jwt * @param {string} url * @return {Promise<string>} */ static async _replaceURLEndpoint(context, _jwt, url) { /**@type {RegExp}*/let regex = RegExp(/{([^}]*)}/g); let match; let matches = new Set(); while((match = regex.exec(url)) !== null) { matches.add(match[1]); } for(const match of matches) { let value = _jwt.subject.getClaimSync(match); if(value == null) { context.log("Claim '" + match + "' not found for subject '" + _jwt.subject.id + "' while building OpenID configuration URL.", LOG_LEVEL.THREAT, "OpenIDJwtIssuer._replaceURLEndpoint(context, _jwt, url)"); throw CelastrinaError.newError("Not Authorized.", 401); } if(Array.isArray(value)) value = value[0]; url = url.split("{" + match + "}").join(/**@type{string}*/value); } return url; } /** * @param {HTTPContext} context * @param {JwtSubject} _jwt * @param {string} configUrl * @return {Promise<(null|JWKS)>} * @private */ static async _getKey(context, _jwt, configUrl) { let _endpoint = await OpenIDJwtIssuer._replaceURLEndpoint(context, _jwt, configUrl); try { let _response = await axios.get(_endpoint); let _issuer = _response.data["issuer"]; _endpoint = _response.data["jwks_uri"]; _response = await axios.get(_endpoint); /**@type{(null|JWKS)}*/let _key = null; for (const key of _response.data.keys) { if(key.kid === _jwt.header.kid) { _key = {issuer: _issuer, type: key.kty, key: key}; break; } } return _key; } catch(exception) { context.log("Exception getting OpenID configuration for subject " + _jwt.subject.id + ": " + exception, LOG_LEVEL.ERROR, "OpenIDJwtIssuer._getKey(subject, context)"); CelastrinaError.newError("Exception authenticating user.", 401); } } /** * @param {(null|JWKS)} key * @param {HTTPContext} context * @return {Promise<string>} * @private */ async _getPemX5C(key, context) { return "-----BEGIN PUBLIC KEY-----\n" + key.key.x5c + "\n-----END PUBLIC KEY-----\n"; } /** * @param {HTTPContext} context * @param {JwtSubject} _jwt * @return {Promise<void>} * @private */ async getKey(context, _jwt) { /**@type{(null|JWKS)}*/let key = await OpenIDJwtIssuer._getKey(context, _jwt, this._configUrl); let pem; if(typeof key.key.x5c === "undefined" || key.key.x5c == null) pem = jwkToPem(key.key); else pem = await this._getPemX5C(key, context); return pem; } } /** * IssuerParser * @author Robert R Murrell * @abstract */ class IssuerParser extends AttributeParser { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/IssuerParser#", type: "celastrinajs.http.IssuerParser"};} /** * @param {AttributeParser} [link=null] * @param {string} [type="Object"] * @param {string} [version="1.0.0"] */ constructor(link = null, type = "BaseIssuer", version = "1.0.0") { super(type, link, version); } /** * @param {Object} _Issuer * @param {Issuer} _issuer */ _loadIssuer(_Issuer, _issuer) { if(!(_Issuer.hasOwnProperty("issuer")) || (typeof _Issuer.issuer !== "string") || _Issuer.issuer.trim().length === 0) throw CelastrinaValidationError.newValidationError( "[IssuerParser._loadIssuer(_Issuer, _issuer)][_Issuer.issuer]: Issuer cannot be null, undefined, or empty.", "_Issuer.issuer"); if(!(_Issuer.hasOwnProperty("audiences")) || !(Array.isArray(_Issuer.audiences)) || _Issuer.audiences.length === 0) throw CelastrinaValidationError.newValidationError( "[IssuerParser._loadIssuer(_Issuer, _issuer)][_Issuer.audiences]: Audiences cannot be null.", "_Issuer.audiences"); let assignments = []; if((_Issuer.hasOwnProperty("assignments")) && (Array.isArray(_Issuer.assignments)) && _Issuer.assignments.length > 0) assignments = _Issuer.assignments; let _validate = false; if(_Issuer.hasOwnProperty("validateNonce") && (typeof _Issuer.validateNonce === "boolean")) _validate = _Issuer.validateNonce; _issuer.issuer = _Issuer.issuer.trim(); _issuer.audiences = _Issuer.audiences; _issuer.assignments = assignments; _issuer.validateNonce = _validate; } } /** * LocalJwtIssuerParser * @author Robert R Murrell */ class LocalJwtIssuerParser extends IssuerParser { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/LocalJwtIssuerParser#", type: "celastrinajs.http.LocalJwtIssuerParser"};} /** * @param {AttributeParser} [link=null] * @param {string} [version="1.0.0"] */ constructor(link = null, version = "1.0.0") { super(link, "LocalJwtIssuer", version); } /** * @param {Object} _LocalJwtIssuer * @param {PropertyManager} pm * @return {Promise<LocalJwtIssuer>} * @private */ async _create(_LocalJwtIssuer, pm) { let _issuer = new LocalJwtIssuer(); await this._loadIssuer(_LocalJwtIssuer, _issuer); if(!(_LocalJwtIssuer.hasOwnProperty("key")) || (typeof _LocalJwtIssuer.key !== "string") || _LocalJwtIssuer.key.trim().length === 0) throw CelastrinaValidationError.newValidationError( "[LocalJwtIssuerParser._create(_LocalJwtIssuer)][_LocalJwtIssuer.key]: ", "_LocalJwtIssuer.key"); _issuer.key = _LocalJwtIssuer.key.trim(); return _issuer; } } /** * LocalJwtIssuerParser * @author Robert R Murrell */ class OpenIDJwtIssuerParser extends IssuerParser { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/OpenIDJwtIssuerParser#", type: "celastrinajs.http.OpenIDJwtIssuerParser"};} /** * @param {AttributeParser} [link=null] * @param {string} [version="1.0.0"] */ constructor(link = null, version = "1.0.0") { super(link, "OpenIDJwtIssuer", version); } /** * @param {Object} _OpenIDJwtIssuer * @param {PropertyManager} pm * @return {Promise<OpenIDJwtIssuer>} * @private */ async _create(_OpenIDJwtIssuer, pm) { let _issuer = new OpenIDJwtIssuer(); await this._loadIssuer(_OpenIDJwtIssuer, _issuer); if(!(_OpenIDJwtIssuer.hasOwnProperty("configURL")) || (typeof _OpenIDJwtIssuer.configURL !== "string") || _OpenIDJwtIssuer.configURL.trim().length === 0) throw CelastrinaValidationError.newValidationError( "[OpenIDJwtIssuerParser._create(_OpenIDJwtIssuer)][_OpenIDJwtIssuer.configURL]: configURL cannot be null or empty.", "_OpenIDJwtIssuer.configURL"); _issuer.configURL = _OpenIDJwtIssuer.configURL.trim(); return _issuer; } } /** * HTTPParameter * @abstract * @author Robert R Murrell */ class HTTPParameter { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/HTTPParameter#", type: "celastrinajs.http.HTTPParameter"};} /** * @param{string}[type] * @param{boolean}[readOnly] */ constructor(type = "HTTPParameter", readOnly = false) { this._type = type; this._readOnly = readOnly } /**@return{string}*/get type() {return this._type;} /**@return{boolean}*/get readOnly() {return this._readOnly;} /** * @param {HTTPContext} context * @param {string} key * @return {*} * @abstract */ async _getParameter(context, key) { throw CelastrinaError.newError("Not Implemented.", 501); } /** * @param {HTTPContext} context * @param {string} key * @param {*} [defaultValue] * @return {Promise<*>} */ async getParameter(context, key, defaultValue = null) { let _value = await this._getParameter(context, key); if(typeof _value === "undefined" || _value == null) _value = defaultValue; return _value; } /** * @param {HTTPContext} context * @param {string} key * @param {*} [value = null] * @abstract */ async _setParameter(context, key, value = null) {} /** * @param {HTTPContext} context * @param {string} key * @param {*} [value = null] * @return {Promise<void>} */ async setParameter(context, key, value = null) { if(this._readOnly) throw CelastrinaError.newError("Set Parameter not supported."); await this._setParameter(context, key, value); } } /** * HeaderParameter * @author Robert R Murrell */ class HeaderParameter extends HTTPParameter { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/HeaderParameter#", type: "celastrinajs.http.HeaderParameter"};} constructor(type = "header"){super(type);} /** * @param {HTTPContext} context * @param {string} key * @return {(null|string)} */ async _getParameter(context, key) { return context.getRequestHeader(key); } /** * @param {HTTPContext} context * @param {string} key * @param {(null|string)} [value = null] */ async _setParameter(context, key, value = null) { await context.setResponseHeader(key, value); } } /** * CookieParameter * @author Robert R Murrell */ class CookieParameter extends HTTPParameter { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/CookieParameter#", type: "celastrinajs.http.CookieParameter"};} constructor(type = "cookie"){super(type);} /** * @param {HTTPContext} context * @param {string} key * @return {Cookie} */ async _getParameter(context, key) { let cookie = await context.getCookie(key, null); if(cookie != null) cookie = cookie.value; return cookie; } /** * @param {HTTPContext} context * @param {string} key * @param {null|string} [value = null] */ async _setParameter(context, key, value = null) { let cookie = await context.getCookie(key, null); if(cookie == null) cookie = Cookie.newCookie(key, value); else cookie.value = value; await context.setCookie(cookie); } } /** * QueryParameter * @author Robert R Murrell */ class QueryParameter extends HTTPParameter { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/QueryParameter#", type: "celastrinajs.http.QueryParameter"};} constructor(type = "query"){super(type, true);} /** * @param {HTTPContext} context * @param {string} key * @return {(null|string)} */ async _getParameter(context, key) { return context.getQuery(key); } /** * @param {HTTPContext} context * @param {string} key * @param {(null|string)} [value = null] */ async _setParameter(context, key, value = null) { throw CelastrinaError.newError("QueryParameter.setParameter not supported.", 501); } } /** * BodyParameter * @author Robert R Murrell */ class BodyParameter extends HTTPParameter { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/BodyParameter#", type: "celastrinajs.http.BodyParameter"};} constructor(type = "body"){super(type);} /** * @param {HTTPContext} context * @param {string} key * @return {*} */ async _getParameter(context, key) { let _value = context.requestBody; /**@type{Array<string>}*/let _attrs = key.split("."); for(const _attr of _attrs) { _value = _value[_attr]; } return _value; } /** * @param {HTTPContext} context * @param {string} key * @param {*} [value = null] */ async _setParameter(context, key, value = null) { let _value = context.responseBody; /**@type{Array<string>}*/let _attrs = key.trim().split("."); for(let idx = 0; idx < _attrs.length - 2; ++idx) { _value = _value[_attrs[idx]]; if(typeof _value === "undefined" || _value == null) throw CelastrinaError.newError("Invalid object path '" + key + "'."); } _value[_attrs[_attrs.length - 1]] = value; } } /** * HTTPParameterParser * @author Robert R Murrell */ class HTTPParameterParser extends AttributeParser { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/HTTPParameterParser#", type: "celastrinajs.http.HTTPParameterParser"};} /** * @param {AttributeParser} [link=null] * @param {string} [version="1.0.0"] */ constructor(link = null, version = "1.0.0") { super("HTTPParameter", link, version); } /** * @param {Object} _HTTPParameter * @param {PropertyManager} pm * @return {Promise<HTTPParameter>} */ async _create(_HTTPParameter, pm) { let _parameter = "header"; if(_HTTPParameter.hasOwnProperty("parameter") && (typeof _HTTPParameter.parameter === "string") && _HTTPParameter.parameter.trim().length > 0) _parameter = _HTTPParameter.parameter.trim(); return HTTPParameterParser.createHTTPParameter(_parameter); } /** * @param {string} type * @return {HTTPParameter} */ static createHTTPParameter(type) { switch(type) { case "header": return new HeaderParameter(); case "cookie": return new CookieParameter(); case "query": return new QueryParameter(); case "body": return new BodyParameter(); default: throw CelastrinaValidationError.newValidationError( "[HTTPParameterParser.getHTTPParameter(type)][type]: '" + type + "' is not supported.", "type"); } } } /** * Session * @author Robert R Murrell */ class Session { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/Session#", type: "celastrinajs.http.Session"};} /** * @param {Object} [values = {}] * @param {boolean} [isNew = false] * @param {(null|string)} [id = null] */ constructor(values = {}, isNew = false, id = uuidv4()) { if(typeof values.id === "undefined" || values.id == null) values.id = id; this._values = values; /**@type{boolean}*/this._dirty = isNew; /**@type{boolean}*/this._new = isNew; } /**@return{string}*/get id() {return this._values.id;} /**@return{boolean}*/get isNew() {return this._new;} /** * @param {string} name * @param {*} defaultValue * @return {Promise<*>} */ async getProperty(name, defaultValue = null) { let _value = this._values[name]; if(typeof _value === "undefined" || _value == null) return defaultValue; else return _value; } /** * @param {string} name * @param {*} value * @return {Promise<void>} */ async setProperty(name, value) { this._values[name] = value; this._dirty = true; } /** * @param {string} name * @return {Promise<void>} */ async deleteProperty(name) {delete this._values[name]; this._dirty = true;} /**@type{boolean}*/get doWriteSession() {return this._dirty;} /** * @param {Object} values */ static load(values) { return new Session(values); } } /** * SessionManager * @author Robert R Murrell */ class SessionManager { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/SessionManager#", type: "celastrinajs.http.SessionManager"};} /** * @param {HTTPParameter} parameter * @param {string} [name = "celastrinajs_session"] * @param {boolean} [createNew = true] */ constructor(parameter, name = "celastrinajs_session", createNew = true) { if(typeof parameter === "undefined" || parameter == null) throw CelastrinaValidationError.newValidationError("Argument 'parameter' cannot be null.", "parameter"); if(typeof name !== "string" || name.trim().length === 0) throw CelastrinaValidationError.newValidationError("Argument 'name' cannot be null or empty.", "name"); this._parameter = parameter; this._name = name.trim(); this._createNew = createNew; } /**@return{HTTPParameter}*/get parameter() {return this._parameter;} /**@return{string}*/get name() {return this._name;} /**@return{boolean}*/get createNew() {return this._createNew;} /** * @param azcontext * @param {Object} config * @return {Promise<void>} */ async initialize(azcontext, config) {} /** * @return {Promise<Session>} */ async newSession() {this._session = new Session({}, true); return this._session;} /** * @param {*} session * @param {HTTPContext} context * @return {(null|string)} */ async _loadSession(session, context) {return session;} /** * @param {HTTPContext} context * @return {Promise<Session>} */ async loadSession(context) { let _session = await this._parameter.getParameter(context, this._name); if((typeof _session === "undefined" || _session == null)) { if(this._createNew) _session = this.newSession(); else return null; } else { /**@type{string}*/let _obj = await this._loadSession(_session, context); if(typeof _obj == "undefined" || _obj == null || _obj.trim().length === 0) { if(this._createNew) _session = await this.newSession(); else return null; } else _session = Session.load(JSON.parse(_obj)); } return _session; } /** * @param {string} session * @param {HTTPContext} context * @return {(null|string)} */ async _saveSession(session, context) {return session;} /** * @param {Session} [session = null] * @param {HTTPContext} context * @return {Promise<void>} */ async saveSession(session = null, context) { if(instanceOfCelastrinaType(Session, session)) { if(session.doWriteSession && !this._parameter.readOnly) await this._parameter.setParameter(context, this._name, await this._saveSession(JSON.stringify(session), context)); } } } /** * SecureSessionManager * @author Robert R Murrell */ class SecureSessionManager extends SessionManager { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/SecureSessionManager#", type: "celastrinajs.http.SecureSessionManager"};} /** * @param {Algorithm} algorithm * @param {HTTPParameter} parameter * @param {string} [name = "celastrinajs_session"] * @param {boolean} [createNew = true] */ constructor(algorithm, parameter, name = "celastrinajs_session", createNew = true) { super(parameter, name, createNew); this._crypto = new Cryptography(algorithm); } /**@return{Cryptography}*/get cryptography() {return this._crypto;} /** * @param azcontext * @param {Object} config * @return {Promise<void>} */ async initialize(azcontext, config) { await super.initialize(azcontext, config); await this._crypto.initialize(); } /** * @param {*} session * @param {HTTPContext} context * @return {(null|string)} */ async _loadSession(session, context) { return this._crypto.decrypt(session); } /** * @param {string} session * @param {HTTPContext} context * @return {(null|string)} */ async _saveSession(session, context) { return this._crypto.encrypt(session); } } /** * AESSessionManager * @author Robert R Murrell */ class AESSessionManager extends SecureSessionManager { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/AESSessionManager#", type: "celastrinajs.http.AESSessionManager"};} /** * @param {(undefined|null|{key:string,iv:string})} options * @param {HTTPParameter} parameter * @param {string} [name = "celastrinajs_session"] * @param {boolean} [createNew = true] */ constructor(options, parameter, name = "celastrinajs_session", createNew = true) { if(typeof options === "undefined" || options == null) throw CelastrinaValidationError.newValidationError( "Argement 'options' cannot be undefined or null", "options"); if(typeof options.key !== "string" || options.key.trim().length === 0) throw CelastrinaValidationError.newValidationError( "Argement 'key' cannot be undefined, null or zero length.", "options.key"); if(typeof options.iv !== "string" || options.iv.trim().length === 0) throw CelastrinaValidationError.newValidationError( "Argement 'iv' cannot be undefined, null or zero length.", "options.iv"); super(AES256Algorithm.create(options), parameter, name, createNew); } } /** * SessionRoleFactory * @author Robert R Murrell */ class SessionRoleFactory extends RoleFactory { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/SessionRoleFactory#", type: "celastrinajs.http.SessionRoleFactory"};} /** * @param {string} [key="roles"] */ constructor(key = "roles") { super(); this._key = key; } /**@return{string}*/get key() {return this._key;} /** * @param {Context|HTTPContext} context * @param {Subject} subject * @return {Promise<Array.<string>>} */ async getSubjectRoles(context, subject) { let _roles = await context.session.getProperty(this._key, []); if(!Array.isArray(_roles)) throw CelastrinaError.newError("Invalid role assignments for session key '" + this._key + "'."); return _roles; } } /** * SessionRoleFactoryParser * @author Robert R Murrell */ class SessionRoleFactoryParser extends RoleFactoryParser { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/SessionRoleFactoryParser#", type: "celastrinajs.http.SessionRoleFactoryParser"};} /** * @param {AttributeParser} link * @param {string} version */ constructor(link = null, version = "1.0.0") { super(link, "SessionRoleFactory", version); } /** * @param {Object} _SessionRoleFactory * @param {PropertyManager} pm * @return {Promise<SessionRoleFactory>} */ async _create(_SessionRoleFactory, pm) { let _key = "roles"; if(_SessionRoleFactory.hasOwnProperty("key") && (typeof _SessionRoleFactory.key === "string")) _key = _SessionRoleFactory.key; return new SessionRoleFactory(_key); } } /** * AESSessionManagerParser * @author Robert R Murrell */ class AESSessionManagerParser extends AttributeParser { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/AESSessionManagerParser#", type: "celastrinajs.http.AESSessionManagerParser"};} /** * @param {AttributeParser} [link=null] * @param {string} [version="1.0.0"] */ constructor(link = null, version = "1.0.0") { super("AESSessionManager", link, version); } /** * @param {Object} _AESSessionManager * @param {PropertyManager} pm * @return {Promise<AESSessionManager>} */ async _create(_AESSessionManager, pm) { let _paramtype = "cookie"; let _paramname = "celastrinajs_session"; if(_AESSessionManager.hasOwnProperty("parameter") && (typeof _AESSessionManager.parameter === "string")) _paramtype = _AESSessionManager.parameter; if(_AESSessionManager.hasOwnProperty("name") && (typeof _AESSessionManager.name === "string")) _paramname = _AESSessionManager.name; let _createnew = true; if(_AESSessionManager.hasOwnProperty("createNew") && (typeof _AESSessionManager.createNew === "boolean")) _createnew = _AESSessionManager.createNew; let _options = null; if(_AESSessionManager.hasOwnProperty("options") && (typeof _AESSessionManager.options === "object") && _AESSessionManager.options != null) _options = _AESSessionManager.options; else { throw CelastrinaValidationError.newValidationError( "[AESSessionManagerParser._create(_AESSessionManager)][AESSessionManager.options]: Argument 'optiosn' cannot be null or undefined.", "AESSessionManager.options"); } if(!(_options.hasOwnProperty("iv")) || (typeof _options.iv !== "string") || _options.iv.trim().length === 0) throw CelastrinaValidationError.newValidationError( "[AESSessionManagerParser._create(_AESSessionManager)][AESSessionManager.options.iv]: Aregument 'iv' cannot be null or empty.", "AESSessionManager.options.iv"); if(!(_options.hasOwnProperty("key")) || (typeof _options.key !== "string") || _options.key.trim().length === 0) throw CelastrinaValidationError.newValidationError( "[AESSessionManagerParser._create(_AESSessionManager)][AESSessionManager.options.key]: Argument 'key' cannot be null or empty.", "AESSessionManager.options.key"); return new AESSessionManager(_options, HTTPParameterParser.createHTTPParameter(_paramtype), _paramname, _createnew); } } /** * HMAC * @author Robert R Murrell */ class HMAC { /**@return{Object}*/static get $object() {return {schema: "https://celastrinajs/schema/v1.0.0/http/HMAC#", type: "celastrinajs.http.HMAC"};} /** * @param {string} secret * @param {HTTPParameter} parameter * @param {string} name * @param {string} algorithm * @param {BinaryToTextEncoding|string} encoding * @param {Array<string>} assignments */ constructor(secret, parameter = new HeaderParameter(), name = "x-ms-content-sha256", algorithm = "sha256", encoding = "hex", assignments = []) { this._parameter = parameter; this._name = name; this._secret = secret; this._algorithm = algorithm; /**@type{Array<string>}*/this._assignments = assignments; /**@type{BinaryToTextEncoding}*/this._encoding = encoding; } /**@return{string}*/get name() {return this._name;} /**@return{string}*/get secret() {return this._secret;} /**@return{string}*/get algorithm() {return this._algorithm;} /**@return{BinaryToTextEncoding}*/get encoding() {return this._encoding;} /**@return{HTTPParameter}*/get parameter() {return this._parameter;} /**@type{Array<string>}*/get assignments() {return this._assignments;} /**@param{Array<string>}assignments*/set assignments(assignments) {return this._assignments = assignments;} } /*** * @typedef HTTPAddOnEvent * @ex