ubjs/packages/http-proxy/HttpProxy.js

/**
 * Reverse proxy with UnityBase based authentication.
 *
 * Use it to authorize a requests using UB and when bypass it to other services
 *
 * Example below will check validity of UB authentication header (if not - return 401)
 * and proxy all requests for `cms` endpoint to the `http://localhost:3030`.
 *
 * For requests what start from `/ubcms` authentication not checked
 *
 * I.e. GET /cms/some/path&p1=true will be proxied to GET http://localhost:3030/some/path&p1=true
 *

     const HttpProxy = require('@unitybase/http-proxy')
     new HttpProxy({
        endpoint: 'cms',
        targetURL: 'http://localhost:3030'
        nonAuthorizedURLs: [/\/ubcms/]
     })

 * @module @unitybase/http-proxy
 */

const http = require('http')
const EventEmitter = require('events').EventEmitter
const UBA_COMMON = require('@unitybase/base').uba_common

/**
 * @class
 */
class HttpProxy extends EventEmitter {
  /**
   * @param {Object} config
   * @param {string} config.endpoint Proxy endpoint name
   * @param {string} config.targetURL A URL of target server we reverse requests to
   * @param {boolean} [config.compressionEnable=false] Use gzip compression of request body
   * @param {Number} [config.sendTimeout=30000] Send timeout in ms.
   * @param {Number} [config.receiveTimeout=30000] Receive timeout in ms.
   * @param {Number} [config.connectTimeout=30000] Connect timeout in ms.
   * @param {Array<RegExp>} [config.nonAuthorizedURLs] Array of regular expression for URL what not require a authentication
   * @param {Array<RegExp>} [config.authorizedURLs] Array of regular expression for URL what require a authentication. If set this parameter will be ignored parameter nonAuthorizedURLs
   * @param {string|Array<string>} [config.authorizedRole] Authorize only user with role(s)
   */
  constructor (config) {
    super()
    this.endpoint = config.endpoint

    let requestParams = {
      URL: config.targetURL,
      keepAlive: true,
      compressionEnable: config.compressionEnable
    }
    if (config.connectTimeout >= 0) {
      requestParams.connectTimeout = config.connectTimeout
    }
    if (config.receiveTimeout >= 0) {
      requestParams.receiveTimeout = config.receiveTimeout
    }
    if (config.sendTimeout >= 0) {
      requestParams.sendTimeout = config.sendTimeout
    }

    this.authorizedRole = config.authorizedRole
    this.nonAuthorizedURLs = config.nonAuthorizedURLs || []
    this.authorizedURLs = config.authorizedURLs || []
    if (config.authorizedURLs) {
      this.nonAuthorizedURLs = null
    }

    this.reverseRequest = http.request(requestParams)
    if (this.reverseRequest.options.path && this.reverseRequest.options.path !== '/') {
      this.basePath = this.reverseRequest.options.path
    }

    App.registerEndpoint(this.endpoint, (req, resp) => {
      this.processRequest(req, resp)
    }, false)
  }

  userHasRoles () {
    if (!this.authorizedRole) return true
    let roleNames = Session.userRoleNames.split(',')
    if (Array.isArray(this.authorizedRole)) {
      return !this.authorizedRole.every(role => !roleNames.includes(role))
    }
    return roleNames.includes(this.authorizedRole)
  }

  checkRequestIsAuthorized (path, resp) {
    if (this.nonAuthorizedURLs) {
      for (let urlRegexp of this.nonAuthorizedURLs) {
        if (urlRegexp.test(path)) return true
      }
    } else {
      let needAuth = false
      for (let urlRegexp of this.authorizedURLs) {
        if (urlRegexp.test(path)) {
          needAuth = true
          break
        }
      }
      if (!needAuth) return true
    }

    if (!App.authFromRequest() || (Session.userID === UBA_COMMON.USERS.ANONYMOUS.ID) ||
      !this.userHasRoles()) {
      resp.statusCode = 401
      resp.writeEnd('')
      return false
    } else {
      return true
    }
  }

  /**
   * Handle a  proxy request
   * @param {THTTPRequest} req
   * @param {THTTPResponse} resp
   */
  processRequest (req, resp) {
    this.reverseRequest.setMethod(req.method)
    this.reverseRequest.setHeadersAsString(req.headers)

    let path = (this.basePath ? this.basePath + (req.uri ? '/' : '') : '') + (req.uri || '/') + (req.parameters ? '?' + req.parameters : '')
    if (!this.checkRequestIsAuthorized(path, resp)) {
      return
    }
    if (path) {
      this.reverseRequest.setPath(path)
    }
    let response
    try {
      response = this.reverseRequest.end(req.read('bin'), 'bin')
    } catch (e) {
      console.error(e.message)
      resp.statusCode = 500
      resp.writeHead('Content-Type: text/plain')
      resp.writeEnd(e.message, 'utf-8')
      return
    }

    let responseHeaders = response.headers
    // TODO - do we need handle a redirect here?
    // switch (response.statusCode) {
    //   case 301:
    //   case 302:
    //     if (responseHeaders.location) {
    //       let parsedURL = url.parse(responseHeaders.location)
    //       if (parsedURL.protocol === me.reverseRequest.options.protocol &&
    //         parsedURL.hostName === me.reverseRequest.options.hostName &&
    //         parsedURL.port === me.reverseRequest.options.port) {
    //         responseHeaders.location = App.serverURL + '/' + this.endpoint + '/' + parsedURL.path + parsedURL.hash
    //       }
    //     }
    //     break
    // }
    resp.statusCode = response.statusCode
    if (responseHeaders['content-type']) {
      resp.writeHead('Content-Type: ' + responseHeaders['content-type'])
    }

    if (responseHeaders['cache-control']) resp.writeHead('Cache-Control: ' + responseHeaders['cache-control'])
    if (responseHeaders['date']) resp.writeHead('Date: ' + responseHeaders['date'])
    if (responseHeaders['etag']) resp.writeHead('ETag: ' + responseHeaders['etag'])
    if (responseHeaders['last-modified']) resp.writeHead('Last-Modified: ' + responseHeaders['last-modified'])

    resp.writeEnd(response.read('bin'), 'bin')
  }
}

module.exports = HttpProxy