interfaces/http/main.js

!function HTTPInterfaceWrapper() {
  let core; let myInterface; let o = {}; const pipelines = function() {}; o.utils = {};
  // eslint-disable-next-line no-extend-native
  String.prototype.endsWith = function(suffix) {
    return this.indexOf(suffix, this.length - suffix.length) !== -1;
  };

  /**
   * HTTP Interface
   *
   * @public
   * @class Server.Interfaces.HTTP
   * @augments Server.Modules.Core.Interface
   * @param {Server.Modules.Core} coreObj - The Core Module Singleton
   * @return {Server.Interfaces.HTTP} interface - The HTTP Interface Singleton
   *
   * @todo Allow view templating engine to be configured from within config file (mustache, handlebars and hogan)
   * @todo Replace formidable with a proprietary solution
   * @todo Replace Cheerio functionality with Utilities Module Transform Library (proprietary)
   * @todo Remove/fix the quickDelete function
   *
   * @description This is the HTTP Interface of the Blackrock Application Server.
   * It is responsible for providing an interface to other clients and servers via
   * the HTTP and HTTPS protocols.
   *
   * @example
   * const httpInterfaceSingleton = core.module('http', 'interface');
   *
   * @author Darren Smith
   * @copyright Copyright (c) 2021 Darren Smith
   * @license Licensed under the LGPL license.
   */
  module.exports = function HTTPInterface(coreObj) {
    core = coreObj; myInterface = new core.Interface('HTTP'); o.log = core.module('logger').log;
    o.config = core.cfg(); o.quickDelete = require('./_support/delete.js');
    o.mustache = require('./_support/template-engines/mustache.js'); o.formidable = require('./_support/formidable');
    o.cheerio = require('./_support/cheerio.js'); o.instances = []; o.viewCache = {};
    o.log('debug', 'HTTP Interface > Initialising...',
        {interface: myInterface.name}, 'INTERFACE_INIT');
    myInterface.client = o.client; myInterface.startInstance = startInstance; myInterface.hook = o.hook;
    myInterface.cheerio = o.cheerio; myInterface.formidable = o.formidable; myInterface.mustache = o.mustache;
    core.on('CORE_START_INTERFACES', function() {
      myInterface.startInstances();
    });
    return myInterface;
  };

  /**
   * Start HTTP Interface Instance
   *
   * @public
   * @memberof Server.Interfaces.HTTP
   * @name startInstance
   * @function
   * @ignore
   * @param {string} name - The name of the instance
   *
   * @description
   * Starts a single instance of the HTTP Interface. Take note - this is called automatically when the interface
   * is loaded by the application server, and thus there should never be any need to call this yourself.
   *
   * @example
   * const httpInterfaceSingleton = core.module('htp', 'interface');
   * httpInterfaceSingleton.startInstance('default');
   */
  const startInstance = function HTTPStartInstance(name) {
    const self = this; const cfg = o.config.interfaces.http[name];
    const port = parseInt(process.env.PORT || cfg.port); let protocol;
    // noinspection JSUnresolvedVariable
    if (cfg.ssl === true) protocol = 'HTTPS';
    else protocol = 'HTTP';
    o.log('startup',
        'HTTP Interface > Attempting to start ' + protocol + ' interface (' + name + ') on port ' + port + '.',
        {interface: myInterface.name, instance: name, protocol: protocol, port: port}, 'INTERFACE_STARTING');
    const routers = [];
    for (const routerName in o.config.router.instances) {
      // noinspection JSUnfilteredForInLoop
      if (o.config.router.instances[routerName].interfaces &&
        (o.config.router.instances[routerName].interfaces.includes('*') ||
            o.config.router.instances[routerName].interfaces.includes(name))) {
        // noinspection JSUnfilteredForInLoop
        routers.push(core.module('router').get(routerName));
      }
    }
    if (routers.length <= 0) {
      o.log('error',
          'HTTP Interface > Cannot start ' + protocol + ' interface (' + name + ') on port ' +
          cfg.port + ' as it is not mapped to any routers.',
          {interface: myInterface.name, instance: name, protocol: protocol, port: port}, 'INTERFACE_ERR_NO_ROUTER_MAP');
      return;
    }
    o.utils.isPortTaken(port, function HTTPIsPortTakenHandler(err, result) {
      let inst;
      if (result !== false) {
        o.log('error',
            'HTTP Interface > Cannot load HTTP interface (' + name + ') as the defined port (' +
            port + ') is already in use.',
            {interface: myInterface.name, instance: name, protocol: protocol, port: port}, 'INTERFACE_ERR_PORT_IN_USE');
        return;
      }
      // noinspection JSUnresolvedVariable
      if (cfg.ssl && (!cfg.key || !cfg.cert)) {
        o.log('error',
            'HTTP Interface > Cannot load SSL interface as either the key or cert has not been defined (' + name + ').',
            {interface: myInterface.name, instance: name,
              protocol: protocol, port: port, message: 'NO_SSL_CERT'}, 'INTERFACE_ERR_GENERAL');
        return;
      }
      let httpLib;
      try {
        // noinspection JSUnresolvedVariable
        if (cfg.ssl) httpLib = 'https';
        else httpLib = 'http';
        // noinspection JSValidateTypes
        o.instances[name] = inst = self.instances[name] = new core.Base().extend({});
        inst.listening = false;
        inst.port = port;
        inst.hooks = {
          onIncomingRequest: {}, onOutgoingResponsePostRender: {}, onOutgoingRequest: {}, onIncomingResponse: {},
        };
        inst.hookIdDirectory = {};
        const serverLib = require('./_support/' + httpLib);
        // noinspection JSUnresolvedVariable
        if (cfg.ssl) {
          inst.server = serverLib(cfg.key, cfg.cert);
        } else {
          inst.server = serverLib();
        }
      } catch (err) {
        o.log('error',
            'HTTP Interface > Error instantiating ' + httpLib.toUpperCase() + ' interface (' + name + ').',
            {message: 'CANNOT_START_INSTANCE', error: err, interface: myInterface.name,
              instance: name, protocol: protocol, port: port}, 'INTERFACE_ERR_GENERAL');
        if (inst) o.quickDelete(inst);
        return;
      }
      inst.server.on('request', function HTTPPrimaryReqHandler(request, response) {
        const myMsg = {
          httpVersion: request.httpVersion,
          host: request.headers.host,
          verb: request.method,
          url: request.url,
          headers: request.headers,
        };
        o.log('debug',
            'HTTP Interface > Received Incoming Request - ' + request.headers.host + request.url,
            {
              request: myMsg, interface: myInterface.name, instance: name,
              protocol: protocol, port: port, host: request.headers.host, path: request.url
            }, 'INTERFACE_NEW_REQ');
        request.interface = name;
        for (let i = 0; i < routers.length; i++) {
          request.router = routers[i];
          request.secure = protocol === 'HTTPS';
          o.executeHookFns(name, 'onIncomingRequest', request).then(function HTTPExecHookFnsPromiseThen(output) {
            pipelines.processRequestStream({req: output, res: response});
          }).catch(function HTTPExecHookFnsPromiseCatch(output) {
            pipelines.processRequestStream({req: output, res: response});
          });
        }
      });
      inst.server.listen(port, function HTTPListenHandler() {
        o.log('startup',
            'HTTP Interface > ' + httpLib.toUpperCase() + ' Interface (' +
            name + ') started successfully on port ' + cfg.port,
            {interface: myInterface.name, instance: name, protocol: protocol, port: port}, 'INTERFACE_STARTED');
        inst.listening = true;
      });
      myInterface.instances = o.instances;
    });
  };

  /**
   * HTTP Hook Singleton
   *
   * @public
   * @class Server.Interfaces.HTTP.Hook
   * @hideconstructor
   *
   * @description
   * This is the HTTP Hook Singleton. It does not need to be instantiated, so you can safely ignore
   * the constructor. You can access methods on it in this way - "interface.hook.method". This
   * class contains methods that allow you to hook in to HTTP requests and responses, and execute
   * intermediate functions on the content of these messages.
   *
   * @example
   * const hook = o.hook;
   */
  o.hook = function HTTPHook() {};


  /**
   * Add Hook Function
   *
   * @public
   * @memberof Server.Interfaces.HTTP.Hook
   * @name add
   * @function
   * @param {string|array} names - Array or String Containing Interface Name(s)
   * @param {string} hookType - Type of Hook
   * @param {function} hookFn - Hook Function
   * @return {Promise} promise - Promise
   *
   * @description
   * This method (Add Hook) allows apps to register a hook function that intercepts the
   * HTTP messages at one of four supported stages (as defined by the hookType parameter).
   * The possible values for the hookType parameter, each representing a distinct stage of the
   * HTTP handling lifecycle, are - onIncomingRequest, onOutgoingResponsePostRender,
   * onOutgoingRequest and onIncomingResponse.
   *
   * @example
   * const http = core.module('http', 'interface');
   * http.hook.add('*', 'onOutgoingResponsePostRender', function(input, cb) {
   *   const output = '<h1>THIS HTML WILL BE INJECTED INTO THE TOP OF EVERY PAGE' + input;
   *   cb(output);
   * }).then(function(addResult) {
   *     const hookId = addResult.hookId
   *     console.log(hookId);
   *     // OUTPUT: "12345-1234-12345-1234"
   * });
   */
  o.hook.add = function HTTPAddHook(names, hookType, hookFn) {
    // eslint-disable-next-line no-undef
    return new Promise((resolve, reject) => {
      let uniqueId;
      let hookCount = 0; const hooksSet = [];
      const addNow = function(inst, hook, fn) {
        uniqueId = core.module('utilities').uuid4();
        inst.hooks[o.hook][uniqueId] = fn;
        inst.hookIdDirectory[uniqueId] = o.hook;
        hooksSet.push(uniqueId);
      };
      if (Array.isArray(names)) {
        hookCount = names.length;
        for (const name in names) {
          // noinspection JSUnfilteredForInLoop
          if (o.instances && o.instances[name]) {
            // noinspection JSUnfilteredForInLoop
            addNow(o.instances[name], hookType, hookFn);
          }
        }
      } else if (names === '*') {
        hookCount = o.instances.length;
        // eslint-disable-next-line guard-for-in
        for (const name in o.instances) {
          // noinspection JSUnfilteredForInLoop
          addNow(o.instances[name], hookType, hookFn);
        }
      } else if (o.instances && o.instances[names]) {
        hookCount = 1;
        addNow(o.instances[names], hookType, hookFn);
      } else {
        o.log('debug',
            'HTTP Interface > Failed to Add New Hooks',
            {interface: myInterface.name, names: names, type: hookType}, 'HTTP_FAILED_TO_ADD_HOOKS');
        // eslint-disable-next-line prefer-promise-reject-errors
        reject('No Valid Hook Targets Defined');
        return;
      }
      const interval = setInterval(function() {
        if (hooksSet.length >= hookCount) {
          clearInterval(interval);
          o.log('debug',
              'HTTP Interface > New Hooks Added',
              {interface: myInterface.name, names: names, type: hookType, hooks: hooksSet}, 'HTTP_HOOKS_ADDED');
          resolve({result: 'Hooked Added Successfully', hookId: uniqueId});
        }
      }, 10);
    });
  };

  /**
   * Remove Defined Hook Function
   *
   * @public
   * @memberof Server.Interfaces.HTTP.Hook
   * @name remove
   * @function
   * @param {string} hookId - The Hook UUID to Remove
   * @return {boolean} result - True | False
   *
   * @description
   * This method (the Remove Hook Method) does exactly what its name implies. It removes
   * a hook that was added via the Add Hook method from the HTTP processing pipelines, so
   * that the message is no longer intercepted by THAT particular hook function. Of courses,
   * multiple hooks may have been added, and therefore others may remain.
   *
   * @example
   * const http = core.module('http', 'interface');
   * http.hook.remove('12345-1234-12345-1234');
   */
  o.hook.remove = function HTTPRemoveHook(hookId) {
    for (const name in o.instances) {
      // noinspection JSUnfilteredForInLoop
      if (o.instances[name].hookIdDirectory[hookId]) {
        // noinspection JSUnfilteredForInLoop
        if (o.instances[name].hooks[o.instances[name].hookIdDirectory[hookId]] &&
            o.instances[name].hooks[o.instances[name].hookIdDirectory[hookId]][hookId]) {
          // noinspection JSUnfilteredForInLoop
          delete o.instances[name].hookIdDirectory[hookId];
          // noinspection JSUnfilteredForInLoop
          delete o.instances[name].hooks[o.instances[name].hookIdDirectory[hookId]][hookId];
        }
      }
    }
    o.log('debug', 'HTTP Interface > Hook Removed',
        {interface: myInterface.name, id: hookId}, 'HTTP_HOOK_REMOVED');
    return true;
  };

  /**
   * Execute Hook Functions
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name executeHookFns
   * @function
   * @ignore
   * @param {string} name - Name
   * @param {string} type - Type
   * @param {*} input - Input
   * @return {Promise} promise - Promise
   *
   * @description
   * This method (the Execute Hooks Method) is a private method that is called at key stages of the
   * HTTP processing pipelines. Upon this method being called - the HTTP Module will check the Hook
   * Directory to see if any hooks have been registered (added), and if so it will execute the
   * associated function for each hook - passing it the HTTP message. And then, once the hook
   * function has performed business logic and altered the HTTP message, it will call a callback
   * function with the amended HTTP message, which then continues on through the pipeline.
   *
   * @example
   * o.executeHookFns(msg.interface, 'onOutgoingResponsePostRender', result).then(function(output) {
   *   response.end(output);
   * }).catch(function(err) {
   *   response.end(result);
   * });
   */
  o.executeHookFns = function HTTPExecuteHooks(name, type, input) {
    // eslint-disable-next-line no-undef
    return new Promise((resolve, reject) => {
      Object.keys(o.instances[name].hooks[type]).length;
      const hookStack = [];
      // eslint-disable-next-line guard-for-in
      for (const hookId in o.instances[name].hooks[type]) {
        // noinspection JSUnfilteredForInLoop
        hookStack.push(o.instances[name].hooks[type][hookId]);
      }
      const executeNow = function HTTPExecuteHooksInnerFn(newInput) {
        if (!o.instances[name]) {
          // eslint-disable-next-line prefer-promise-reject-errors
          reject('Invalid Instance');
        }
        const types = ['onIncomingRequest', 'onOutgoingResponsePostRender', 'onOutgoingRequest', 'onIncomingResponse'];
        if (!types.includes(type)) {
          // eslint-disable-next-line prefer-promise-reject-errors
          reject('Invalid Type');
        }
        if (hookStack.length > 0) {
          const hookFn = hookStack.pop();
          hookFn(newInput, function(output) {
            executeNow(output);
          });
        } else {
          o.log('debug',
              'HTTP Interface > Hooks Executed',
              {interface: myInterface.name, name: name, type: type}, 'HTTP_HOOK_EXECUTED');
          resolve(newInput);
        }
      };
      executeNow(input);
    });
  };


  /**
   * Pipelines: [1] Request Processing Pipeline
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream
   * @function
   * @ignore
   * @return {object} pipeline - The Pipeline Object
   *
   * @description
   * This is the Request Processing Pipeline.
   * It processes the Incoming Request Stream [HTTP/S].
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   [Pipeline Step 1],
   *   [Pipeline Step 2],
   *   [Pipeline Step 3],
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream = function HTTPProcessReqStreamPipeline(evt) {
    // noinspection JSUnresolvedFunction
    core.lib.rxPipeline(evt).pipe(

        // Run this pipeline for every incoming request:
        pipelines.processRequestStream.checkErrors,
        pipelines.processRequestStream.determineContentType,
        pipelines.processRequestStream.parseMultiPart,
        pipelines.processRequestStream.parseNonMultiPart,
        pipelines.processRequestStream.processRequestData,
        pipelines.processRequestStream.parseCookies,
        pipelines.processRequestStream.processHostPathAndQuery,
        pipelines.processRequestStream.fetchIPAddresses,
        pipelines.processRequestStream.isRequestSecure,
        pipelines.processRequestStream.prepareRequestMessage,
        pipelines.processRequestStream.fixTrailingSlash,
        pipelines.processRequestStream.lookupRequestRoute,
        pipelines.processRequestStream.pipeFilesystemFiles,
        pipelines.processRequestStream.routeRequest

    ).subscribe();
  };

  /**
   * Pipelines: Processes the Outgoing Response Stream [HTTP/S]
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream
   * @function
   * @ignore
   * @return {object} pipeline - The Pipeline Object
   *
   * @description
   * This is the Response Processing Pipeline.
   * It processes the Outgoing Response Stream [HTTP/S].
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   [Pipeline Step 1],
   *   [Pipeline Step 2],
   *   [Pipeline Step 3],
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream = function HTTPProcessResponseStreamPipeline(evt) {
    // noinspection JSUnresolvedFunction
    core.lib.rxPipeline(evt).pipe(

        pipelines.processResponseStream.preventDuplicateResponses,
        pipelines.processResponseStream.setStatusCookiesAndHeaders,
        pipelines.processResponseStream.checkAndFinaliseLocationRequest,
        pipelines.processResponseStream.checkAndFinaliseResponseWithoutView,
        pipelines.processResponseStream.checkAndFinaliseFileResponse,
        pipelines.processResponseStream.checkAndSetMIMEType,
        pipelines.processResponseStream.detectViewType,
        pipelines.processResponseStream.processObjectViewResponse,
        pipelines.processResponseStream.processFileViewResponse

    ).subscribe();
  };


  /**
   * Request Stream Processing Methods:
   */

  /**
   * Request Stream Method [1]: Check HTTP Request For Errors
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.checkErrors
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This is a Request Pipeline function that checks for errors within the incoming request
   * stream, and also attaches a listener to the response object that is triggered if any
   * errors occur during the response.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.checkErrors,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.checkErrors = function HTTPCheckErrors(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLBindSearchOp(observer, evt) {
      evt.res.resReturned = false;
      evt.req.on('error', (err) => {
        o.log('error', 'HTTP Interface > Error processing incoming request',
            {interface: myInterface.name, error: err}, 'HTTP_REQ_ERR_PROCESS_REQ');
        evt.res.statusCode = 400; evt.res.end(); evt.res.resReturned = true;
      });
      evt.res.on('error', (err) => {
        o.log('error', 'HTTP Interface > Error processing outgoing response',
            {interface: myInterface.name, error: err}, 'HTTP_REQ_ERR_PROCESS_RES');
      });
      o.log('debug', 'HTTP Interface > [1] Checked Request for Errors',
          {interface: myInterface.name}, 'HTTP_REQ_ERR_CHECK');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [2]: Determine Content-Type of Request Message
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.determineContentType
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This is a Request Pipeline function that determines the content type of the
   * request message, including determining whether the request is multi-part or
   * not. If the request is determined to be a multi-part request, it adds a flag
   * to the event that is sent to the next pipeline function indicating this, so
   * that later functions can make decisions accordingly.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.determineContentType,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.determineContentType = function HTTPDetermineContentType(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPDetermineContentTypeOp(observer, evt) {
      const {headers} = evt.req;
      let contentType; let boundary; let multipart = false;
      // eslint-disable-next-line guard-for-in
      for (let header in headers) {
        // noinspection JSUnfilteredForInLoop
        header = header.toLowerCase();
        if (header === 'content-type') {
          // noinspection JSUnfilteredForInLoop
          contentType = headers[header];
        }
      }
      if (contentType) {
        contentType = contentType.split(';');
        for (let i = 0; i < contentType.length; i++) {
          contentType[i] = contentType[i].trim();
          if (contentType[i] === 'multipart/form-data') multipart = true;
          if (contentType[i].startsWith('boundary=')) {
            boundary = contentType[i].split('=');
            boundary = boundary[1];
          }
        }
      }
      // if (!boundary) boundary = '';
      evt.req.multipart = multipart;
      o.log('debug', 'HTTP Interface > [2] Content Type Determined',
          {interface: myInterface.name}, 'HTTP_REQ_CONTENT_TYPE_DETERMINED');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [3a]: Parses a multi-part http message
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.parseMultiPart
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This is a Request Pipeline function that checks the multi-part flag that was set
   * against the event by the Request Pipeline determineContentType() function. If
   * it is true, this function processes the incoming multi-part request as a file upload,
   * storing the file in to the upload folder that is defined within the Application Server
   * config file. It then records the path to the file (and the fact that a file upload took
   * place) against the outgoing event, which eventually makes it's way to the App Controller
   * where further processing against or decisions relating to the file can be made.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.parseMultiPart,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.parseMultiPart = function HTTPParseMultiPart(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPParseMultiPartOp(observer, evt) {
      if(evt.req.multipart) {
        const form = new o.formidable.IncomingForm();
        // noinspection JSUnresolvedVariable
        if (o.config.interfaces.http[evt.req.interface].fileUploadPath) {
          // noinspection JSUnresolvedVariable
          form.uploadDir = o.config.interfaces.http[evt.req.interface].fileUploadPath;
        } else {
          form.uploadDir = core.fetchBasePath('root') + './upload/';
        }
        // noinspection JSUnresolvedVariable
        if (o.config.interfaces.http[evt.req.interface].maxUploadFileSizeMb) {
          // noinspection JSUnresolvedVariable
          form.maxFileSize = o.config.interfaces.http[evt.req.interface].maxUploadFileSizeMb * 1024 * 1024;
        } else {
          form.maxFileSize = 50 * 1024 * 1024;
        }
        try {
          form.parse(evt.req, function HTTPParseMultiPartFormParser(err, fields, files) {
            const body = fields;
            body.files = files;
            body.error = err;
            evt.data = body;
            observer.next(evt);
          });
        } catch (err) {
          evt.data = {error: 'File Upload Size Was Too Large'};
          observer.next(evt);
        }
        o.log('debug',
            'HTTP Interface > [3] Parsed Multi-Part Request Message',
            {interface: myInterface.name}, 'HTTP_REQ_PARSED_MULTI_PART');
      } else {
        observer.next(evt);
      }
    }, source);
  };

  /**
   * Request Stream Method [3b]: Parses a non-multi-part http message
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.parseNonMultiPart
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This is a Request Pipeline function that checks the multi-part flag that was set
   * against the event by the Request Pipeline determineContentType() function. If the
   * flag is false then it determines that the request is not a multi-part request and
   * combines each of the chunks of the incoming request body into a buffer. It then
   * attaches this buffer to the outgoing event where it continues down the request
   * pipeline and is subject to further processing before reaching the controller.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.parseNonMultiPart,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.parseNonMultiPart = function HTTPParseNonMultiPart(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPParseNonMultiPartOp(observer, evt) {
      if(!evt.req.multipart) {
        const data = [];
        evt.req.on('data', (chunk) => {
          data.push(chunk);
        }).on('end', () => {
          evt.data = data;
          observer.next(evt);
        });
        o.log('debug',
            'HTTP Interface > [3] Parsed Non-Multi-Part Request Message',
            {interface: myInterface.name}, 'HTTP_REQ_PARSED_NON_MULTI_PART');
      } else {
        observer.next(evt);
      }
    }, source);
  };

  /**
   * Request Stream Method [4]: Process Request Data
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.processRequestData
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This is a Request Pipeline function that checks to see whether any request body
   * data is present within the request message, and if so - converts the buffer, within
   * which the body data sits, to a string.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.processRequestData,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.processRequestData = function HTTPProcessRequestData(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPProcessRequestDataOp(observer, evt) {
      let data = evt.data;
      try {
        if (Buffer.from(data)) {
          data = Buffer.concat(data).toString();
        }
      } catch (err) {
        o.log('error', 'HTTP Interface > [4] Error Processing Request Data',
            {interface: myInterface.name, error: err}, 'ERR_PROC_REQ_DATA');
      }
      if (data && core.module('utilities').isJSON(data) === 'json_string') {
        data = JSON.parse(data);
      } else if (data && core.module('utilities').isJSON(data) !== 'json_object') {
        data = require('querystring').parse(data);
      }
      evt.data = data;
      o.log('debug', 'HTTP Interface > [4] Request Body Data Processed',
          {interface: myInterface.name}, 'HTTP_REQ_BODY_DATA_PROCESSED');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [5]: Parses Cookies From Headers
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.parseCookies
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This is a Request Pipeline function that parses the request message for cookie headers,
   * and if it finds any then it extracts them and places them in to a cookies object that
   * is then attached to the request object that is passed through to the App Controller.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.parseCookies,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.parseCookies = function HTTPProcessCookies(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPProcessCookiesOp(observer, evt) {
      const list = {}; const rc = evt.req.headers.cookie;
      rc && rc.split(';').forEach(function( cookie ) {
        const parts = cookie.split('=');
        list[parts.shift().trim()] = decodeURI(parts.join('='));
      });
      evt.req.cookieObject = list;
      o.log('debug', 'HTTP Interface > [5] Cookies Parsed',
          {interface: myInterface.name}, 'HTTP_REQ_COOKIES_PARSED');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [6]: Parse Host, Path & Query From URL
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.processHostPathAndQuery
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This pipeline method takes the incoming HTTP request and parses it to extract
   * the host(name) and path into two string variables, and the querystring
   * parameters in to an object (req.query[name]).
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.processHostPathAndQuery,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.processHostPathAndQuery = function HTTPProcessHostPathAndQuery(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPProcessHostPathAndQueryOp(observer, evt) {
      const url = evt.req.url; const headers = evt.req.headers;
      const splitPath = url.split('?'); evt.req.queryStringObject = {};
      if (splitPath[1]) {
        const splitQuery = splitPath[1].split('&');
        for (let i = 0; i < splitQuery.length; i++) {
          const moreSplitQuery = splitQuery[i].split('=');
          if (!evt.req.queryStringObject[moreSplitQuery[0]]) {
            evt.req.queryStringObject[moreSplitQuery[0]] = moreSplitQuery[1];
          } else {
            const oldValue = evt.req.queryStringObject[moreSplitQuery[0]];
            evt.req.queryStringObject[moreSplitQuery[0]] = [];
            evt.req.queryStringObject[moreSplitQuery[0]].push(oldValue);
            evt.req.queryStringObject[moreSplitQuery[0]].push(moreSplitQuery[1]);
          }
        }
      }
      const splitHost = headers.host.split(':'); const host = splitHost[0]; const port = splitHost[1];
      evt.req.theHost = host; evt.req.thePort = port; evt.req.thePath = splitPath[0];
      o.log('debug', 'HTTP Interface > [6] Host Path & Query Processed',
          {interface: myInterface.name}, 'HTTP_REQ_PATH_QUERY_PROCESSED');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [7]: Parse IP Addresses
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.fetchIPAddresses
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This pipeline method parses the incoming HTTP request message in order to identify
   * and extract the IPv4 and IPv6 IP addresses. And then sets these values against
   * the corresponding request object variables.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.fetchIPAddresses,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.fetchIPAddresses = function HTTPFetchIPAddresses(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPFetchIPAddressesOp(observer, evt) {
      const {headers, connection} = evt.req;
      let reqIpAddress;
      if (headers['X-Forwarded-For']) reqIpAddress = headers['X-Forwarded-For'];
      else if (connection.remoteAddress) reqIpAddress = connection.remoteAddress;
      else reqIpAddress = '';
      if (reqIpAddress.indexOf(':') > -1) {
        const startPos = reqIpAddress.lastIndexOf(':'); const endPos = reqIpAddress.length;
        const ipv4 = reqIpAddress.slice(startPos + 1, endPos); const ipv6 = reqIpAddress.slice(0, startPos - 1);
        evt.req.reqIpAddress = ipv4; evt.req.reqIpAddressV6 = ipv6;
      }
      o.log('debug', 'HTTP Interface > [7] IP Addresses Processed',
          {interface: myInterface.name}, 'HTTP_REQ_IP_ADDR_PROCESSED');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [8]: Parse Whether Request is Secure
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.isRequestSecure
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This pipeline method parses the incoming HTTP request message in order to identify
   * and extract the SSL flag - which is an indication of whether the request has come
   * in via a secure protocol. This may mean that it came in via an HTTPS instance of
   * this interface directly, or it may mean that the initial leg of a request that
   * came in via a reverse proxy was SSL encrypted (HTTPS), but the second leg - from
   * the reverse proxy server to Blackrock may not have necessarily been SSL encrypted.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.isRequestSecure,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.isRequestSecure = function HTTPIsRequestSecure(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPIsRequestSecureOp(observer, evt) {
      const {headers} = evt.req; const request = evt.req;
      if (headers['X-Forwarded-Proto'] && headers['X-Forwarded-Proto'] === 'http') {
        evt.req.reqSecure = false;
      } else if (headers['X-Forwarded-Proto'] && headers['X-Forwarded-Proto'] === 'https') {
        evt.req.reqSecure = true;
      } else evt.req.reqSecure = request.secure;
      o.log('debug',
          'HTTP Interface > [8] Request SSL (Secure) Enabled Flag Processed',
          {interface: myInterface.name}, 'HTTP_REQ_SSL_FLAG_PROCESSED');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [9]: Prepare Request Message
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.prepareRequestMessage
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This pipeline method takes all of the request attributes that were parsed and extracted in
   * previous pipeline methods and compiles them in to a request message object that is compatible
   * with what the Router Module expects to see.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.prepareRequestMessage,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.prepareRequestMessage = function HTTPPrepareRequestMessage(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPPrepareRequestMessageOp(observer, evt) {
      const request = evt.req; const msgId = core.module('utilities').uuid4(); const {method} = request;
      evt.req.theMessage = {
        'type': 'http', 'interface': request.interface, 'msgId': msgId,
        'state': 'incoming', 'directional': 'request/response',
        'request': {
          'path': evt.req.thePath, 'host': evt.req.theHost, 'port': evt.req.thePort,
          'headers': request.headers, 'params': null, 'cookies': evt.req.cookieObject,
          'ip': evt.req.reqIpAddress, 'ipv6': evt.req.reqIpAddressV6, 'verb': method,
          'secure': evt.req.reqSecure, 'body': evt.data, 'query': evt.req.queryStringObject
        },
      };
      o.log('debug', 'HTTP Interface > [9] Request Message Prepared',
          {interface: myInterface.name}, 'HTTP_REQ_MSG_PREPARED');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [10]: Fix Trailing Slash
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.fixTrailingSlash
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This pipeline method fixes the trailing slash on the URL when the path is empty by
   * removing it from the path that is sent in via the request object to the Router Module.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.fixTrailingSlash,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.fixTrailingSlash = function HTTPFixTrailingSlash(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPFixTrailingSlashOp(observer, evt) {
      const {theMessage} = evt.req; const response = evt.res;
      if (theMessage.request.path.endsWith('/') && theMessage.request.path !== '/') {
        evt.res.resReturned = true;
        const newPath = theMessage.request.path.slice(0, -1);
        response.writeHead(301, {Location: newPath});
        response.end();
        return;
      } else if (theMessage.request.path === '') {
        evt.res.resReturned = true;
        response.writeHead(301, {Location: '/'});
        response.end();
        return;
      }
      o.log('debug',
          'HTTP Interface > [10] Trailing Slash Fixed If Present',
          {interface: myInterface.name}, 'HTTP_REQ_TRAILING_SLASH_FIXED');
      observer.next(evt);
    }, source);
  };

  /**
   * Request Stream Method [11]: Lookup Request Route
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.lookupRequestRoute
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This pipeline method calls the route() method on the Router Module - which searches
   * the local app directory (that was filled at Application Server startup) for the
   * hostname + path of the request. If it identifies that there is a controller that
   * has been configured to respond to that hostname + path combination, then it primes
   * subsequent pipeline methods. Similarly, it primes other methods if no controller is
   * found.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.lookupRequestRoute,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.lookupRequestRoute = function HTTPLookupRequestRoute(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPLookupRequestRouteOp(observer, evt) {
      const request = evt.req; const {theMessage} = request;
      o.log('debug',
          'HTTP Interface > [11] Searching For Request Route',
          {interface: myInterface.name}, 'HTTP_REQ_SEARCHING_FOR_ROUTE');
      request.router.route(theMessage.request.host, theMessage.request.path,
          function HTTPRouterRouteCallback(route) {
            if (route && route.match && route.match.app) {
              const basePath = core.module('app-engine').app(route.match.app).cfg().basePath;
              if (theMessage.request.path === basePath) {
                theMessage.request.path += '/';
              }
            }
            theMessage.request.params = route.param;
            let htmlPath;
            if (route && route.match && route.match.app) {
              const app = core.module('app-engine').app(route.match.app);
              let base;
              if (app.cfg().basePath) base = app.cfg().basePath;
              else base = '';
              if (theMessage.request.path.startsWith(base)) htmlPath = theMessage.request.path.slice(base.length);
              else htmlPath = theMessage.request.path;
            } else htmlPath = theMessage.request.path;
            evt.req.route = route;
            evt.req.htmlPath = htmlPath;
            observer.next(evt);
          });
    }, source);
  };

  /**
   * Request Stream Method [12]: Pipe (HTML) Filesystem Files
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.pipeFilesystemFiles
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * This pipeline method checks the filesystem (under 'html' within the app folder) to
   * see if there are any static files that exist within the same path location as is
   * described within the request message. If it identifies such a file, it pipes
   * that file directly through to the client which effectively results in this request
   * message exiting the remaining pipeline.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.pipeFilesystemFiles,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.pipeFilesystemFiles = function HTTPPipeFilesystemFiles(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPPipeFilesystemFilesOp(observer, evt) {
      const fs = require('fs'); const request = evt.req; const response = evt.res;
      const {route, htmlPath, theMessage} = request;
      if (!route || !route.match) {
        observer.next(evt);
        return;
      }
      const appPath = core.module('app-engine').app(route.match.app).cfg().appPath;
      let stats;
      try {
        stats = fs.lstatSync(appPath + '/html' + htmlPath);
      } catch (e) {
        stats = false;
      }
      let directPath = true;
      if (!stats || stats.isDirectory()) {
        try {
          stats = fs.lstatSync(appPath + '/html' + htmlPath + '/index.html');
          directPath = false;
        } catch (e) {
          stats = false;
        }
      }
      let pathToRead; let filename; let mimeType;
      if (stats && stats.isFile()) {
        if (directPath === true) {
          pathToRead = appPath + '/html' + htmlPath;
          filename = theMessage.request.path.split('/');
          mimeType = o.utils.checkMIMEType(filename[filename.length - 1].split('.').pop());
        } else {
          pathToRead = appPath + '/html' + htmlPath + '/index.html';
          filename = [];
          filename[0] = 'index.html';
          mimeType = o.utils.checkMIMEType(filename[filename.length - 1].split('.').pop());
        }
        if (!mimeType) mimeType = 'application/octet-stream';
        response.writeHead(200, {'Content-Type': mimeType});
        fs.createReadStream(pathToRead).pipe(response);
        evt.res.resReturned = true;
        o.log('debug',
            'HTTP Interface > [12] Filesystem file piped directly through',
            {interface: myInterface.name, file: theMessage.request.path,
              filename: filename, msgId: theMessage.msgId}, 'HTTP_REQ_FILE_PIPED');
      } else {
        o.log('debug',
            'HTTP Interface > [12] Requested File is Not a Filesystem File (would have piped if so)',
            {interface: myInterface.name}, 'HTTP_REQ_NO_FILE_PIPED');
        observer.next(evt);
      }
    }, source);
  };

  /**
   * Request Stream Method [13]: Route Request via Router
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processRequestStream.routeRequest
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Assuming that a route was found in the lookupRequestRoute() pipeline method, and that no file
   * was discovered with the same hostname and path in the pipeFilesystemFiles() method, this
   * method will - (i) Setup a Response Listener; and then (ii) Send the request message through
   * to the Router Module to be routed on to the relevant controller. A timeout is set so that
   * if the response does not arrive within the period of time defined within
   * 'config.interfaces.http[instance].requestTimeout' in the Application Server config, then a
   * 504 Timeout response will be sent to the client, and the Response Listener will be closed.
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processRequestStream.routeRequest,
   *   ...
   * ).subscribe();
   */
  pipelines.processRequestStream.routeRequest = function HTTPRouteRequest(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPRouteRequestOp(observer, evt) {
      const request = evt.req; let resReturned = false;
      const responseListener = function HTTPRouteReqResListener(msg) {
        o.instances[request.interface].removeListener('outgoing.' + msg.msgId, responseListener);
        resReturned = true;
        pipelines.processResponseStream({'req': evt.req, 'res': evt.res, 'msg': msg});
      };
      o.instances[request.interface].on('outgoing.' + request.theMessage.msgId, responseListener);
      const timeout = o.config.interfaces.http[request.interface].requestTimeout; let timer = 0;
      const interval = setInterval(function HTTPRouteReqTimeoutHandler() {
        if (!resReturned && timer < timeout) {
          timer += 10;
        } else if (!resReturned && timer >= timeout) {
          evt.res.statusCode = 504;
          evt.res.setHeader('Content-Type', 'application/json');
          evt.res.end(JSON.stringify({'error': 'Request timed out'}));
          resReturned = true;
          clearInterval(interval);
          o.instances[request.interface].removeListener('outgoing.' + evt.req.theMessage.msgId, responseListener);
        } else if (resReturned) {
          clearInterval(interval);
        }
      }, 10);
      o.log('debug',
          'HTTP Interface > [13] Sending incoming message ' + request.theMessage.msgId +
          ' to router', {interface: myInterface.name, request: request.theMessage}, 'HTTP_REQ_SEND_TO_ROUTER');
      request.router.incoming(request.theMessage);
      observer.next(evt);
    }, source);
  };


  /**
   * Response Stream Processing Methods:
   */

  /**
   * Response Stream Method [1]: Prevent Duplicate Responses
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.preventDuplicateResponses
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.preventDuplicateResponses,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.preventDuplicateResponses = function HTTPPreventDuplicateRes(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPPreventDuplicateResOp(observer, evt) {
     o.log('debug',
          'HTTP Interface > [1] Received response from router, Mitigating Against Duplicate Responses',
          {interface: myInterface.name, msgId: evt.msg.msgId}, 'HTTP_RES_STOP_DUP_RES');
      if (!evt.msg.interface || evt.res.resReturned) return;
      evt.res.resReturned = true;
      observer.next(evt);
    }, source);
  };

  /**
   * Response Stream Method [2]: Set Status Code, Cookies & Headers
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.setStatusCookiesAndHeaders
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.setStatusCookiesAndHeaders,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.setStatusCookiesAndHeaders = function HTTPSetStatusCookiesAndHeaders(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPSetStatusCookiesAndHeadersOp(observer, evt) {
      evt.res.statusCode = evt.msg.response.statusCode;
      if (evt.msg.response.cookies) {
        // eslint-disable-next-line guard-for-in
        for (const name in evt.msg.response.cookies) {
          // noinspection JSUnfilteredForInLoop
          evt.res.setHeader('Set-Cookie', name + '=' + evt.msg.response.cookies[name].value + '; path=/;');
        }
      }
      if (evt.msg.response.clearCookies) {
        // eslint-disable-next-line guard-for-in
        for (const name in evt.msg.response.clearCookies) {
          evt.res.setHeader('Set-Cookie', name + '=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT');
        }
      }
      if (evt.msg.response.headers && typeof evt.msg.response.headers === 'object') {
        // eslint-disable-next-line guard-for-in
        for (const header in evt.msg.response.headers) {
          // noinspection JSUnfilteredForInLoop
          evt.res.setHeader(header, evt.msg.response.headers[header]);
        }
      }
      o.log('debug',
          'HTTP Interface > [2] Status, Headers & Cookies Set Against the Response Message',
          {interface: myInterface.name}, 'HTTP_RES_STATUS_COOKIES_HEADERS_SET');
    observer.next(evt);
  }, source);
  };

  /**
   * Response Stream Method [3]: Check & Finalise Location Request
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.checkAndFinaliseLocationRequest
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.checkAndFinaliseLocationRequest,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.checkAndFinaliseLocationRequest = function HTTPCheckAndEndLocationReq(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPCheckAndEndLocationReqOp(observer, evt) {
      if (evt.msg.response.location) {
        evt.res.setHeader('Location', evt.msg.response.location);
        evt.res.end();
        o.log('debug',
            'HTTP Interface > [3] Checked If Redirect & Redirected Request',
            {interface: myInterface.name, location: evt.msg.response.location}, 'HTTP_RES_EXEC_REDIRECT');
      } else {
        o.log('debug', 'HTTP Interface > [3] Checked If Redirect & It Was Not',
            {interface: myInterface.name}, 'HTTP_RES_NOT_REDIRECT');
        observer.next(evt);
      }
    }, source);
  };

  /**
   * Response Stream Method [4]: Check & Finalise JSON Response Without View
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.checkAndFinaliseResponseWithoutView
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.checkAndFinaliseResponseWithoutView,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.checkAndFinaliseResponseWithoutView = function HTTPCheckAndEndResNoView(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPCheckAndEndResNoViewOp(observer, evt) {
      if (!evt.msg.response.view && evt.msg.response.body) {
        evt.res.setHeader('Content-Type', 'application/json');
        evt.res.end(JSON.stringify(evt.msg.response.body));
        o.log('debug',
            'HTTP Interface > [4] Checked If No View & Finalised Response',
            {interface: myInterface.name}, 'HTTP_RES_SENT_JSON_RES');
      } else {
        o.log('debug', 'HTTP Interface > [4] Checked If No View But There Was One',
            {interface: myInterface.name}, 'HTTP_RES_FOUND_VIEW');
        observer.next(evt);
      }
    }, source);
  };

  /**
   * Response Stream Method [5]: Check & Finalise File Response
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.checkAndFinaliseFileResponse
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.checkAndFinaliseFileResponse,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.checkAndFinaliseFileResponse = function HTTPCheckAndEndFileRes(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPCheckAndEndFileResOp(observer, evt) {
      if (evt.msg.response.file) {
        const fs = require('fs');
        let stats;
        try {
          stats = fs.lstatSync(evt.msg.response.file);
        } catch (e) {
          stats = false;
        }
        if (stats && stats.isFile()) {
          const pathToRead = evt.msg.response.file;
          o.log('debug',
              'HTTP Interface > [5] Found File - Sending to client ',
              {interface: myInterface.name, file: pathToRead, msgId: evt.msg.msgId}, 'HTTP_RES_SENT_FILE_TO_CLIENT');
          const filename = pathToRead.split('/');
          let mimeType = o.utils.checkMIMEType(filename[filename.length - 1].split('.').pop());
          if (!mimeType) {
            mimeType = 'application/octet-stream';
          }
          evt.res.writeHead(200, {'Content-Type': mimeType});
          const stream = fs.createReadStream(pathToRead); let hadError = false;
          stream.pipe(evt.res);
          stream.on('error', function HTTPCheckAndFinaliseFileResponseErrHandler(err) {
            hadError = true; if (evt.msg.response.cb) {
              evt.msg.response.cb(err, null);
            }
          });
          stream.on('close', function HTTPCheckAndFinaliseFileResponseCloseHandler() {
            if (!hadError && evt.msg.response.cb) {
              evt.msg.response.cb(null,
                  {success: true, code: 'FILE_DOWNLOADED', message: 'File Successfully Downloaded'});
            }
          });
        } else {
          o.log('debug',
              'HTTP Interface > [5] Could Not Find File - Responding With Error',
              {interface: myInterface.name, file: evt.msg.response.file}, 'HTTP_RES_CANNOT_FIND_FILE');
          evt.res.setHeader('Content-Type', 'application/json');
          evt.res.end(JSON.stringify({error: 'Cannot Find File'}));
        }
      } else {
        o.log('debug',
            'HTTP Interface > [5] Checked If This Was a File Response But It Was Not',
            {interface: myInterface.name}, 'HTTP_RES_NOT_FILE_RES');
        observer.next(evt);
      }
    }, source);
  };

  /**
   * Response Stream Method [6]: Check & Set MIME Type
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.checkAndSetMIMEType
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.checkAndSetMIMEType,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.checkAndSetMIMEType = function HTTPCheckAndSetMIMEType(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPCheckAndSetMIMETypeOp(observer, evt) {
      const urlSplit = evt.req.url.split('/');
      const filename = urlSplit[urlSplit.length - 1];
      const fileType = filename.split('.')[1];
      let mimeType = o.utils.checkMIMEType(fileType);
      if (!mimeType) mimeType = 'text/html';
      evt.res.setHeader('Content-Type', mimeType);
      o.log('debug',
          'HTTP Interface > [6] Checked & Set MIME Type for This Response',
          {interface: myInterface.name, mimeType: mimeType}, 'HTTP_RES_SET_MIME');
      observer.next(evt);
    }, source);
  };

  /**
   * Response Stream Method [7]: Detect View Type
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.detectViewType
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.detectViewType,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.detectViewType = function HTTPDetectViewType(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPDetectViewTypeOp(observer, evt) {
      if (typeof evt.msg.response.view === 'object' && evt.msg.response.view !== null) evt.msg.viewType = 'object';
      else evt.msg.viewType = 'file';
      o.log('debug',
          'HTTP Interface > [7] View Type Detected',
          {interface: myInterface.name, viewType: evt.msg.viewType}, 'HTTP_RES_VIEW_TYPE_DETECTED');
      observer.next(evt);
    }, source);
  };

  /**
   * Response Stream Method [8]: Process Object View Response
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.processObjectViewResponse
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.processObjectViewResponse,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.processObjectViewResponse = function HTTPProcessObjectViewRes(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPDetectViewTypeOp(observer, evt) {
      if (evt && evt.msg.viewType === 'object') {
        const fs = require('fs');
        const appPath = core.module('app-engine').app(evt.msg.app).cfg().appPath;
        if (typeof evt.msg.response.view === 'object' && evt.msg.response.view !== null) {
          if (evt.msg.response.view.file) {
            try {
              fs.readFile(appPath + '/views/' + evt.msg.response.view.file, 'utf8',
                  function HTTPProcessObjectViewResponseReadFileCallback(err, htmlData) {
                    if (err) {
                      o.log('error',
                          'HTTP Interface > [8] ' + appPath + '/views/' +
                          evt.msg.response.view.file + ' view does not exist.',
                          {interface: myInterface.name, message: evt.msg, error: err}, 'HTTP_RES_ERR_VIEW_NOT_EXIST');
                      evt.res.setHeader('Content-Type', 'application/json');
                      evt.res.end(JSON.stringify(evt.msg.response.body));
                      evt.err = err;
                      observer.next(evt);
                      return;
                    }
                    o.log('debug',
                        'HTTP Interface > [8] Rendering Object-Type HTML View...',
                        {interface: myInterface.name}, 'HTTP_RES_RENDERING_OBJ_VIEW');
                    o.utils.renderView(evt.msg, evt.res, htmlData);
                  });
            } catch (err) {
              o.log('error',
                  'HTTP Interface > [8] View does not exist',
                  {
                    interface: myInterface.name,
                    view: evt.msg.response.view,
                    error: err
                  }, 'HTTP_RES_ERR_VIEW_NOT_EXIST');
              evt.res.setHeader('Content-Type', 'application/json');
              evt.res.end(JSON.stringify(evt.msg.response.body));
              evt.err = err;
              observer.next(evt);
            }
          } else if (evt.msg.response.view.html) {
            const htmlData = evt.msg.response.view.html;
            o.log('debug',
                'HTTP Interface > [8] Rendering Object-Type HTML View...',
                {interface: myInterface.name}, 'HTTP_RES_RENDERING_OBJ_VIEW');
            o.utils.renderView(evt.msg, evt.res, htmlData);
          } else {
            o.log('error',
                'HTTP Interface > [8] Error Loading View - Unknown Type.',
                {interface: myInterface.name}, 'HTTP_RES_ERR_LOADING_VIEW_UNKNOWN');
            evt.res.setHeader('Content-Type', 'application/json');
            evt.res.end(JSON.stringify(evt.msg.response.body));
            // eslint-disable-next-line prefer-promise-reject-errors
            evt.err = 'Error Loading View - Unknown Type';
            observer.next(evt);
          }
        } else {
          o.log('error',
              'HTTP Interface > [8] Skipped Method to Process Object View Response',
              {interface: myInterface.name}, 'HTTP_RES_NOT_OBJ_VIEW');
          observer.next(evt);
        }
      } else {
        observer.next(evt);
      }
    }, source);
  };

  /**
   * Response Stream Method [9]: Process File View Response
   *
   * @private
   * @memberof Server.Interfaces.HTTP
   * @name pipelines.processResponseStream.processFileViewResponse
   * @function
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Yet to be defined...
   *
   * @example
   * core.lib.rxPipeline({}).pipe(
   *   ...
   *   pipelines.processResponseStream.processFileViewResponse,
   *   ...
   * ).subscribe();
   */
  pipelines.processResponseStream.processFileViewResponse = function HTTPProcessFileViewRes(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function HTTPDetectViewTypeOp(observer, evt) {
      if (evt && evt.msg.viewType !== 'object') {
        const fs = require('fs');
        const appPath = core.module('app-engine').app(evt.msg.app).cfg().appPath;
        const viewPath = appPath + '/views/' + evt.msg.response.view;
        evt.msg.appPath = appPath;
        if (o.viewCache[viewPath] && o.viewCache[viewPath].content) {
          o.log('debug', 'HTTP Interface > [9] Rendering File-Type View...',
              {interface: myInterface.name}, 'HTTP_RES_RENDERING_FILE_VIEW');
          o.utils.renderView(evt.msg, evt.res, o.viewCache[viewPath].content);
          observer.next(evt);
        } else {
          try {
            fs.readFile(viewPath, 'utf8',
                function HTTPProcFileViewResReadFileCb(err, htmlData) {
                  if (err) {
                    o.log('error',
                        'HTTP Interface > [9] View does not exist',
                        {interface: myInterface.name, view: evt.msg.response.view, error: err},
                        'HTTP_RES_ERR_FILE_VIEW_NOT_EXIST');
                    evt.res.setHeader('Content-Type', 'application/json');
                    evt.res.end(JSON.stringify(evt.msg.response.body));
                    return;
                  }
                  o.viewCache[viewPath] = {
                    content: htmlData,
                    expiry: 'TBC',
                  };
                  o.log('debug',
                      'HTTP Interface > [9] Rendering File-Type View...',
                      {interface: myInterface.name}, 'HTTP_RES_RENDERING_FILE_VIEW');
                  o.utils.renderView(evt.msg, evt.res, htmlData);
                });
          } catch (err) {
            o.log('error',
                'HTTP Interface > [9] View does not exist...',
                {
                  interface: myInterface.name, view: evt.msg.response.view,
                  error: err
                }, 'HTTP_RES_ERR_FILE_VIEW_NOT_EXIST');
            evt.res.setHeader('Content-Type', 'application/json');
            evt.res.end(JSON.stringify(evt.msg.response.body));
            observer.next(err);
          }
          observer.next(evt);
        }
      } else {
        observer.next(evt);
      }
    }, source);
  };


  /**
   * @private
   * @class Server.Interfaces.HTTP.Utils
   * @hideconstructor
   * @ignore
   *
   * @description
   * This is the Utilities class. It does not need to be instantiated, so you can safely ignore
   * the constructor. You can access methods on it in this way - "interface.utils.method".
   *
   * @example
   * const utils = o.utils;
   */
  o.utils = function() {};

  /**
   * Render View
   *
   * @private
   * @memberof Server.Interfaces.HTTP.Utils
   * @name renderView
   * @function
   * @ignore
   * @param {object} msg - Message Object
   * @param {object} response - Response Object
   * @param {string} htmlData - HTML Data Context
   *
   * @description
   * This is a Utility Function within the HTTP Module that takes the context object passed
   * through from the controller, and the view template that was specified within the res.render()
   * function within the controller and used the Mustache templating engine to inject the
   * context in to the view template. It also detects any partial view templates that are
   * referenced from within the primary view template, loads them and injects them back into
   * the primary view template, before finally sending the result as the response to the client.
   *
   * @example
   * o.utils.renderView(msg, response, htmlData);
   */
  o.utils.renderView = function HTTPRenderView(msg, response, htmlData) {
    const partials = {}; const regex = /{{>(.+)}}+/g; const found = htmlData.match(regex);
    if (found) {
      for (let i = 0; i < found.length; i++) {
        const frontRemoved = found[i].substring(4).slice(0, -2);
        try {
          partials[frontRemoved] = require('fs').readFileSync(msg.appPath +
            '/views/includes/' + frontRemoved + '.mustache', 'utf8');
        } catch (err) {
          o.log('error', 'HTTP Interface > Failed To Render View Whilst Processing Template Partials',
              {interface: myInterface.name, error: err}, 'VIEW_RENDER_FAILED_PARTIALS_ERR');
        }
      }
    }
    const result = o.mustache.render(htmlData, msg.response.body, partials);
    o.executeHookFns(msg.interface, 'onOutgoingResponsePostRender', result).then(function(output) {
      response.end(output);
      o.log('debug', 'HTTP Interface > [10] View Rendered Successfully',
          {interface: myInterface.name}, 'HTTP_RES_VIEW_RENDERED');
    }).catch(function(err) {
      response.end(result);
      o.log('debug', 'HTTP Interface > [10] View Rendered Successfully, Error Executing Hooks',
          {interface: myInterface.name, error: err}, 'HTTP_RES_VIEW_RENDERED');
    });
  };

  /**
   * Check If Port Is Taken
   *
   * @private
   * @memberof Server.Interfaces.HTTP.Utils
   * @name isPortTaken
   * @function
   * @ignore
   * @param {number} port - The port number to check
   * @param {function} cb - Callback function
   *
   * @description
   * This is a Utility Function that is used during initialisation of HTTP Module Instances,
   * in order to ensure that the port that they're about to begin listening on has not
   * been taken by another instance, interface or process.
   *
   * @example
   * o.utils.isPortTaken(80, function(err,res) {
   *     if(res === true) console.log('Port In Use');
   * });
   */
  o.utils.isPortTaken = function HTTPIsPortTaken(port, cb) {
    const tester = require('net').createServer()
        .once('error', function HTTPIsPortTakenErrHandler(err) {
          if (err.code !== 'EADDRINUSE') {
            return cb(err);
          } cb(null, true);
        })
        .once('listening', function HTTPIsPortTakenListeningHandler() {
          tester.once('close', function() {
            cb(null, false);
          }).close();
        })
        .listen(port);
  };

  /**
   * Get File MIME Type
   *
   * @private
   * @memberof Server.Interfaces.HTTP.Utils
   * @name checkMIMEType
   * @function
   * @ignore
   * @param {string} fileType - File Type
   * @return {string} mimeType - MIME Type
   *
   * @description
   * This is a Utility function that is used within the HTTP Module's Response Pipeline to
   * determine the MIME (Multipurpose Internet Mail Extensions) type of a file based
   * on it's file extension.
   *
   * @example
   * const mimeType = o.utils.checkMIMEType('png');
   * console.log('The MIME Type Of This File Is - ' + mimeType);
   * // Output: "The MIME Type Of This File Is - image/png"
   */
  o.utils.checkMIMEType = function HTTPCheckMIMEType(fileType) {
    const mimeTypes = {
      'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif',
      'html': 'text/html', 'js': 'text/javascript', 'css': 'text/css', 'csv': 'text/csv',
      'pdf': 'application/pdf', 'md': 'text/plain', 'txt': 'text/plain',
    };
    return mimeTypes[fileType];
  };


  /**
   * @public
   * @class Server.Interfaces.HTTP.Client
   * @hideconstructor
   *
   * @description
   * This is the HTTP Client class. It does not need to be instantiated, so you can safely ignore
   * the constructor. You can access methods on it in this way - "interface.client.method". This
   * class contains methods to make outgoing HTTP requests and receive responses from these requests.
   *
   * @example
   * const httpClient = req.core.module('http', 'interface').client;
   */
  o.client = function() {};

  /**
   * Make HTTP Request
   *
   * @public
   * @memberof Server.Interfaces.HTTP.Client
   * @name request
   * @param {object} req - Request Object
   * @param {function} cb - Callback Function
   * @function
   *
   * @description
   * This method allows you to make an HTTP/S request to an endpoint, setting the verb to whatever
   * you would like, in addition to providing the URL to send the request to, an object containing
   * headers to accompany the request, a request body and setting the encoding type of the request.
   * All of this information describing the request that you wish to send is passed within a
   * Request Object to this method.
   *
   * @example
   * req.core.module('http', 'interface').client.request({
   *   "url": "https://www.google.com:8080/path1/path2?test=now",
   *   "headers": {
   *      "Content-Type": "application/json",
   *      "Content-Length": data.length
   *   },
   *   "method": "POST",
   *   "data": {"test": "value"},
   *   "encoding": "utf8"
   * }, function(err, res) {
   *    console.log(res);
   * });
   *
   */
  o.client.request = function HTTPClientRequest(req, cb) {
    if (!req.url) {
      cb({success: false, code: 1, message: 'URL Not Defined'}, null);
      return;
    }
    if (!req.method) {
      cb({success: false, code: 2, message: 'Method Not Defined'}, null);
      return;
    }
    let protocol;
    if (req.url.split('/')[0] === 'http:') protocol = 'http';
    else if (req.url.split('/')[0] === 'https:') protocol = 'https';
    else {
      cb({success: false, code: 3, message: 'Unknown Protocol'}, null);
      return;
    }
    const urlPieces = req.url.split('/');
    const httpLib = require(protocol);
    const hostname = urlPieces[2].split(':')[0];
    const port = urlPieces[2].split(':')[1];
    urlPieces.shift(); urlPieces.shift(); urlPieces.shift();
    const path = '/' + urlPieces.join('/').split('?')[0];
    const options = {'hostname': hostname, 'method': req.method};
    if (port) options.port = port;
    if (req.headers) options.headers = req.headers;
    else options.headers = {};
    if (path) options.path = path;
    if (req.data && core.module('utilities').isJSON(req.data) === 'json_object') {
      if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
      req.data = JSON.stringify(req.data);
    } else if (req.data && (typeof req.data === 'string' || req.data instanceof String) &&
      core.module('utilities').isJSON(req.data) === 'json_string') {
      if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
    } else if (req.data && (typeof req.data === 'string' || req.data instanceof String) &&
      req.data.indexOf('<') !== -1 && req.data.indexOf('>') !== -1) {
      if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/xml';
    } else if (req.data && (typeof req.data === 'string' || req.data instanceof String)) {
      if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'text/plain';
    } else {
      if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'text/plain';
    }
    if (req.data && !options.headers['Content-Length']) {
      options.headers['Content-Length'] = Buffer.byteLength(req.data);
    }
    const makeRequest = function(theOptions) {
      const reqObj = httpLib.request(theOptions, function HTTPClientReqCallback(res) {
        let responseData = '';
        if (req.encoding) res.setEncoding(req.encoding);
        else res.setEncoding('utf8');
        res.on('data', (chunk) => {
          responseData += chunk;
        });
        res.on('end', () => {
          if (responseData && core.module('utilities').isJSON(responseData) === 'json_string') {
            responseData = JSON.parse(responseData);
          } else if (responseData && responseData.indexOf('<') === -1 &&
            responseData.indexOf('>') === -1 && responseData.indexOf('=') !== -1) {
            const responseDataSplit = responseData.split('&'); const responseDataNew = {};
            for (let i = 0; i < responseDataSplit.length; i++) {
              const valueSplit = responseDataSplit[i].split('=');
              responseDataNew[decodeURIComponent(valueSplit[0])] = decodeURIComponent(valueSplit[1]);
            }
            responseData = responseDataNew;
          } else responseData = decodeURIComponent(responseData);
          const resObj = {'statusCode': res.statusCode, 'data': responseData};
          o.log('debug',
              'HTTP Interface > Received Incoming HTTP Response.',
              {interface: myInterface.name, response: resObj}, 'HTTP_RECEIVED_RESPONSE');
          o.executeHookFns('http', 'onIncomingResponse', resObj).then(function(myResOutput) {
            cb(null, {
              success: true, code: 4, message: 'Response Received Successfully',
              statusCode: myResOutput.statusCode, data: myResOutput.data,
            });
          }).catch(function(err) {
            cb(null, {
              success: true, code: 4, message: 'Response Received Successfully',
              statusCode: res.statusCode, data: responseData, err: err,
            });
          });
        });
      });
      reqObj.on('error', (error) => {
        cb({success: false, code: 5, message: 'Request Error', error: error}, null);
      });
      if (req.data) reqObj.write(req.data);
      reqObj.end();
    };
    o.executeHookFns('http', 'onOutgoingRequest', options).then(function(theOptions) {
      o.log('debug',
          'HTTP Interface > Making Outgoing HTTP Request.',
          {interface: myInterface.name, options: theOptions}, 'HTTP_SENDING_REQUEST');
      makeRequest(theOptions);
    }).catch(function(err) {
      o.log('debug',
          'HTTP Interface > Error Making Outgoing HTTP Request.',
          {interface: myInterface.name, error: err}, 'HTTP_ERR_SENDING_REQUEST');
      makeRequest(options);
    });
  };

  /**
   * Make GET HTTP Request
   *
   * @public
   * @memberof Server.Interfaces.HTTP.Client
   * @name get
   * @function
   * @param {string} url - Request URL
   * @param {function} cb - Callback Function
   *
   * @description
   * This method allows you to make an HTTP/S request to an endpoint, with the verb of the
   * request set to GET. Such requests are commonly used to instruct a server to fetch one or
   * more objects from the server. You are also able to attach a request body and/or headers
   * to the request.
   *
   * @example
   * const httpClient = req.core.module('http', 'interface').client;
   * httpClient.get('https://www.restfulnights.com/api/v1/users', function(err, res) {
   *     if(res.statusCode == 200) {
   *         console.log('User List Retrieved Successfully');
   *     }
   * });
   */
  o.client.get = function HTTPClientGetRequest(url, cb) {
    myInterface.client.request({'url': url, 'headers': {}, 'method': 'GET', 'encoding': 'utf8'}, cb);
  };

  /**
   * Make POST HTTP Request
   *
   * @public
   * @memberof Server.Interfaces.HTTP.Client
   * @name post
   * @function
   * @param {string} url - Request URL
   * @param {object} data - Request Body Data
   * @param {object} options - Options Object
   * @param {function} cb - Callback Function
   *
   * @description
   * This method allows you to make an HTTP/S request to an endpoint, with the verb of the
   * request set to POST. Such requests are commonly used to instruct a server to create
   * a new object on the server. You are also able to attach a request body and/or headers
   * to the request.
   *
   * @example
   * const httpClient = req.core.module('http', 'interface').client;
   * httpClient.post('https://www.restfulnights.com/api/v1/users', null, {email: '[email protected]'}, function(err, res) {
   *     if(res.statusCode == 201) {
   *         console.log('User Created Successfully');
   *     }
   * });
   */
  o.client.post = function HTTPClientPostReq(url, data, options, cb) {
    const reqObj = {'url': url, 'headers': {}, 'method': 'POST', 'encoding': 'utf8'};
    if (data) reqObj.data = data;
    if (options && options.headers) reqObj.headers = options.headers;
    myInterface.client.request(reqObj, cb);
  };

  /**
   * Make PUT HTTP Request
   *
   * @public
   * @memberof Server.Interfaces.HTTP.Client
   * @name put
   * @function
   * @param {string} url - Request URL
   * @param {object} data - Request Body Data
   * @param {object} options - Options Object
   * @param {function} cb - Callback Function
   *
   * @description
   * This method allows you to make an HTTP/S request to an endpoint, with the verb of the
   * request set to PUT. Such requests are commonly used to instruct a server to update
   * one or more properties on an object on the server. You are also able to attach a request
   * body and/or headers to the request.
   *
   * @example
   * const httpClient = req.core.module('http', 'interface').client;
   * httpClient.put('https://www.restfulnights.com/api/v1/users/101', {country: 'Australia'}, {}, function(err, res) {
   *     if(res.statusCode == 200) {
   *         console.log('User Updated Successfully');
   *     }
   * });
   */
  o.client.put = function HTTPClientPutReq(url, data, options, cb) {
    const reqObj = {'url': url, 'headers': {}, 'method': 'PUT', 'encoding': 'utf8'};
    if (data) reqObj.data = data;
    if (options && options.headers) reqObj.headers = options.headers;
    myInterface.client.request(reqObj, cb);
  };

  /**
   * Make DELETE HTTP Request
   *
   * @public
   * @memberof Server.Interfaces.HTTP.Client
   * @name delete
   * @function
   * @param {string} url - Request URL
   * @param {object} data - Request Body Data
   * @param {object} options - Options Object
   * @param {function} cb - Callback Function
   *
   * @description
   * This method allows you to make an HTTP/S request to an endpoint, with the verb of the
   * request set to DELETE. Such requests are commonly used to instruct a server to delete
   * an object from the server. You are also able to attach a request body and/or headers
   * to the request.
   *
   * @example
   * const httpClient = req.core.module('http', 'interface').client;
   * httpClient.delete('https://www.restfulnights.com/api/v1/users/101', null, {}, function(err, res) {
   *     if(res.statusCode == 200) {
   *         console.log('User Deleted Successfully');
   *     }
   * });
   */
  o.client.delete = function HTTPClientDeleteReq(url, data, options, cb) {
    const reqObj = {'url': url, 'headers': {}, 'method': 'DELETE', 'encoding': 'utf8'};
    if (data) reqObj.data = data;
    if (options && options.headers) reqObj.headers = options.headers;
    myInterface.client.request(reqObj, cb);
  };
}();