UNPKG

web-service

Version:

Instantiates web services: REST Api, file upload, etc

682 lines (545 loc) 19.6 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _promise = require('babel-runtime/core-js/promise'); var _promise2 = _interopRequireDefault(_promise); var _keys = require('babel-runtime/core-js/object/keys'); var _keys2 = _interopRequireDefault(_keys); var _getIterator2 = require('babel-runtime/core-js/get-iterator'); var _getIterator3 = _interopRequireDefault(_getIterator2); var _regenerator = require('babel-runtime/regenerator'); var _regenerator2 = _interopRequireDefault(_regenerator); var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); exports.default = web_service; var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs); var _http = require('http'); var _http2 = _interopRequireDefault(_http); var _https = require('https'); var _https2 = _interopRequireDefault(_https); var _koa = require('koa'); var _koa2 = _interopRequireDefault(_koa); var _koaBodyparser = require('koa-bodyparser'); var _koaBodyparser2 = _interopRequireDefault(_koaBodyparser); var _koaMount = require('koa-mount'); var _koaMount2 = _interopRequireDefault(_koaMount); var _koaBunyan = require('koa-bunyan'); var _koaBunyan2 = _interopRequireDefault(_koaBunyan); var _koaCompress = require('koa-compress'); var _koaCompress2 = _interopRequireDefault(_koaCompress); var _koaSend = require('koa-send'); var _koaSend2 = _interopRequireDefault(_koaSend); var _koaLocale = require('koa-locale'); var _koaLocale2 = _interopRequireDefault(_koaLocale); var _errors = require('./errors'); var _errors2 = _interopRequireDefault(_errors); var _promisify = require('./promisify'); var _promisify2 = _interopRequireDefault(_promisify); var _helpers = require('./helpers'); var _errorHandler = require('./middleware/error handler'); var _errorHandler2 = _interopRequireDefault(_errorHandler); var _authentication = require('./middleware/authentication'); var _authentication2 = _interopRequireDefault(_authentication); var _proxy = require('./middleware/proxy'); var _proxy2 = _interopRequireDefault(_proxy); var _fileUpload = require('./middleware/file upload'); var _fileUpload2 = _interopRequireDefault(_fileUpload); var _acl = require('./middleware/acl'); var _acl2 = _interopRequireDefault(_acl); var _session = require('./middleware/session'); var _session2 = _interopRequireDefault(_session); var _routing2 = require('./middleware/routing'); var _routing3 = _interopRequireDefault(_routing2); var _redirect = require('./middleware/redirect'); var _redirect2 = _interopRequireDefault(_redirect); var _rewrite = require('./middleware/rewrite'); var _rewrite2 = _interopRequireDefault(_rewrite); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // Sets up a Web Server instance (based on Koa) // // options: // // compress - enables tar/gz compression of Http response data // // detect_locale - extracts locale from Http Request headers // and places it into ctx.locale // // session - tracks user session (ctx.session) // // authentication - uses a JWT token as a means of user authentication // (should be a function transforming token payload into user info) // // parseBody - parse Http Post requests body (default: false; true when using routing) // // routing - enables Rest Http routing // (usage: web.get('/path', parameters => return 'Echo')) // // log - bunyan log instance // // returns an object with properties: // // shutdown() - gracefully shuts down the server (not tested) // // upload() - enables file upload functionality // // parameters: // // path - the URL path to mount this middleware at (defaults to /) // // upload_folder - where to write the files // // multiple_files - set this flag to true in case of multiple file upload // // files() - enables serving static files // // parameters: // // url_path - the URL path to mount this middleware at // // filesystem_path - the corresponding filesystem path where the static files reside // // listen() - starts listening for requests // // parameters: // // port - the TCP port to listen on // host - the TCP host to listen on (defaults to 0.0.0.0) // // returns: a Promise // // mount() - mounts a Koa middleware at a path // // parameters: // // path - the URL path to mount the middleware at // middleware - the middleware to mount // // use() - standard Koa .use() method // // redirect() - HTTP redirect helper // // parameters: // // from - the base URL path from which to redirect // // options: // // to - the base URL (or path) to which the redirect will be performed // // exact - redirect to `to` only in case of exact URL path match (`url.path === from`) // // match - custom URL matching function match({ url, path, querystring, query }); // should return a URL (or a path) to which the redirect will be performed; // if it returns nothing then the redirect won't be performed. // // status - HTTP redirection status (defaults to 301 (Moved Permanently)) // (e.g. can be set to 302 (Moved Temporarily)) // // rewrite() - Rewrites HTTP request URL (for further matching) // // parameters: // // from - the base URL path on which to rewrite // // options: // // to - the base URL (or path) to which to rewrite the HTTP request URL // // exact - rewrite to `to` only in case of exact URL path match (`url.path === from`) // // match - custom URL matching function match({ url, path, querystring, query }); // should return a URL (or a path) to which to rewrite the HTTP request URL; // if it returns nothing then the URL won't be rewritten. // // proxy() - proxies all requests for this path to another web server // // parameters: // // path - the URL path to mount the requests for // destination - where to proxy these requests to // function web_service() { var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; // In development mode errors are printed as HTML var development = process.env.NODE_ENV !== 'production'; // This object will be returned var result = {}; // Closers for proxies and running web servers var closers = []; // Create a Koa web application var web = new _koa2.default(); // Always trusts `X-Forwarded-For` HTTP header. // https://en.wikipedia.org/wiki/X-Forwarded-For // This means that a proxy **must** be set up // which is gonna **replace** `X-Forwarded-For` // with the real IP address of a client // (otherwise a hacker could forge any IP address). // The reason is that microservices are distributed // randomly in a cloud in a VPN, therefore simple // `requiest.getRemoteAddr()` call becomes useless for // determining the original HTTP Request IP address. web.proxy = options.xForwardedFor === false ? false : true; // Compresses HTTP response with GZIP // (better delegate this task to NginX or HAProxy in production) if (options.compress) { // хз, нужно ли сжатие в node.js: мб лучше поставить впереди nginx'ы, // и ими сжимать, чтобы не нагружать процесс node.js web.use((0, _koaCompress2.default)()); } // Dummy log in case no `log` supplied var log = options.log || { debug: console.info.bind(console), info: console.info.bind(console), warn: console.warn.bind(console), error: console.error.bind(console) }; // Is used in `api.js` result.log = log; // Handle all subsequent errors web.use((0, _errorHandler2.default)({ development: development, log: log, markup_settings: options.error_html })); // If an Access Control List is set, // then allow only IPs from the list of subnets. // (this is a "poor man"'s ACL, better use a real firewall) if (options.access_list) { web.use((0, _acl2.default)(options.access_list)); } // Outputs Apache-style logs for incoming HTTP requests. // E.g. "GET /users?page=2 200 466ms 4.66kb" if (options.debug) { web.use((0, _koaBunyan2.default)(log, { // which level you want to use for logging. // default is info level: 'debug', // this is optional. Here you can provide request time in ms, // and all requests longer than specified time will have level 'warn' timeLimit: 100 })); } if (options.detect_locale) { // Gets locale from HTTP request // (the second parameter is the HTTP GET query parameter name // and also the cookie name) (0, _koaLocale2.default)(web, 'locale'); // Sets `ctx.locale` variable for reference web.use(function () { var _ref = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee(ctx) { return _regenerator2.default.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: ctx.locale = ctx.getLocaleFromQuery() || ctx.getLocaleFromCookie() || ctx.getLocaleFromHeader(); case 1: case 'end': return _context.stop(); } } }, _callee, this); })); return function (_x2) { return _ref.apply(this, arguments); }; }()); } // Secret keys (used for JWT token signing, for example) web.keys = options.keys; // Enable JWT authentication if (options.authentication) { web.use((0, _authentication2.default)({ options: options.authentication, keys: options.keys, log: log })); } // Sessions aren't currently used if (options.session) { web.use((0, _session2.default)(options.redis)); } // Checks if `parseBody` needs to be set to `true` // (that's the case for routing) if (options.parseBody !== false && options.routing === true) { options.parseBody = true; } // Enables HTTP POST body parsing if (options.parseBody) { // Set up http post request handling. // Usage: ctx.request.body web.use((0, _koaBodyparser2.default)({ formLimit: '100mb' })); } // Enables REST routing if (options.routing) { var _routing = (0, _routing3.default)({ keys: options.keys, routing: options.routing, parseBody: options.parseBody }); var extensions = _routing.extensions; var middleware = _routing.middleware; // Injects REST routing methods to `result` object. var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(extensions)), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var key = _step.value; result[key] = extensions[key]; } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = (0, _getIterator3.default)(middleware), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var use = _step2.value; web.use(use); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } // // HTTP server shutdown flag // let shutting_down = false // // In case of server shutdown, stop accepting new HTTP connections. // // (this code wasn't tested) // web.use(async function (ctx, next) // { // if (shutting_down) // { // ctx.status = 503 // ctx.message = 'The server is shutting down for maintenance' // } // else // { // await next() // } // }) // Shuts down the HTTP server. // Returns a Promise. result.close = function () { // shutting_down = true // Shut down http proxies and running web servers. // Stops the server from accepting new connections and keeps existing connections. return _promise2.default.all(closers.map(function (closer) { return closer(); })); }; // Legacy method name (0.4.x) result.shut_down = result.close; // Returns the number of currently present HTTP connections. // (this method wasn't tested) result.connections = function () { // http_server.getConnections() return (0, _promisify2.default)(web.getConnections, web)(); }; // Enables handling file uploads. // Takes an object with parameters. result.file_upload = function () { // Check for misconfiguration if (options.parseBody) { throw new Error('.file_upload() was enabled but also "parseBody" wasn\'t set to false, therefore Http POST request bodies are parsed which creates a conflict. Set "parseBody" parameter to false.'); } // Enable file uploading middleware web.use(_fileUpload2.default.apply(this, arguments)); }; // Shorter alias for file uploads result.upload = result.file_upload; // Serves static files // (better do it with NginX or HAProxy in production) result.files = function (url_path, filesystem_path) { var _this = this; var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; // Cache them in the web browser for 1 year by default var maxAge = options.maxAge || 365 * 24 * 60 * 60; web.use((0, _koaMount2.default)(url_path, function () { var _ref2 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee2(ctx, next) { var not_found; return _regenerator2.default.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: if (ctx.method !== 'HEAD' && ctx.method !== 'GET') { // 405 Method Not Allowed ctx.throw(405, 'Only HEAD and GET HTTP methods are allowed for requesting static files'); } not_found = void 0; _context2.prev = 2; _context2.next = 5; return (0, _koaSend2.default)(ctx, ctx.path, { maxAge: maxAge, root: _path2.default.resolve(filesystem_path) }); case 5: not_found = !_context2.sent; _context2.next = 15; break; case 8: _context2.prev = 8; _context2.t0 = _context2['catch'](2); if (!(_context2.t0.statusCode === 404)) { _context2.next = 14; break; } not_found = true; _context2.next = 15; break; case 14: throw _context2.t0; case 15: if (not_found) { ctx.throw(404, 'File not found'); } case 16: case 'end': return _context2.stop(); } } }, _callee2, _this, [[2, 8]]); })); return function (_x4, _x5) { return _ref2.apply(this, arguments); }; }())); }; // (deprecated) // Legacy alias for static files serving. // This should be removed in some future "major" version. result.serve_static_files = result.files; // Mounts Koa middleware at path result.mount = function (path, handler) { web.use((0, _koaMount2.default)(path, handler)); }; // exposes Koa .use() function for custom middleware result.use = web.use.bind(web); // Proxies all URLs starting with 'from_path' to another server // (make sure you proxy only to your own servers // so that you don't leak cookies or JWT tokens to the 3rd party) result.proxy = function (from_path, to, proxy_options) { var _proxier = (0, _proxy2.default)(from_path, to, proxy_options); var proxy = _proxier.proxy; var middleware = _proxier.middleware; // This closer is for standalone `.listen()` proxies // which is not used here. // closers.push(() => return promisify(proxy.close, proxy)()) web.use(middleware); }; // Redirection helper result.redirect = function (from, options) { web.use((0, _redirect2.default)(from, options)); }; // URL rewrite result.rewrite = function (from, options) { web.use((0, _rewrite2.default)(from, options)); }; // Returns a "callback" which can be used to start Node.js servers: // const http = require('http') // http.createServer(result.callback()).listen(3000) // http.createServer(result.callback()).listen(3001) result.callback = function () { return web.callback(); }; // Runs http server. // Returns a Promise resolving to an instance of HTTP server. result.listen = function (port, host, options) { if ((0, _helpers.is_object)(port)) { options = host; host = port.host; port = port.port; } host = host || '0.0.0.0'; options = options || {}; // The last route - throws "Not found" error web.use(function () { var _ref3 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee3(ctx) { return _regenerator2.default.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: ctx.status = 404; ctx.message = 'The requested resource not found: ' + ctx.method + ' ' + ctx.url; // Reduces noise in the `log` in case of errors // (web browsers query '/favicon.ico' automatically) if (!(0, _helpers.ends_with)(ctx.path, '/favicon.ico')) { log.error(ctx.message, 'Web server error: Not found'); } case 3: case 'end': return _context3.stop(); } } }, _callee3, this); })); return function (_x6) { return _ref3.apply(this, arguments); }; }()); // Create HTTP server var web_server = options.https ? _https2.default.createServer((0, _helpers.is_object)(options.https) ? options.https : undefined) : _http2.default.createServer(); // // Enable Koa for handling HTTP requests // web_server.on('request', web.callback()) // Copy-pasted from // https://github.com/koajs/koala/blob/master/lib/app.js // // "Expect: 100-continue" is something related to http request body parsing // http://crypto.pp.ua/2011/02/mexanizm-expectcontinue/ // var koa_callback = web.callback(); web_server.on('request', koa_callback); web_server.on('checkContinue', function (request, response) { // Requests with `Expect: 100-continue` request.checkContinue = true; koa_callback(request, response); }); // Starts HTTP server var web_server_start_promise = web_server_listen(web_server, port, host); closers.push(function () { return web_server_start_promise.then(function () { return (0, _promisify2.default)(web_server.close, web_server)(); }); }); return web_server_start_promise; // .on('connection', () => connections++) // .on('close', () => connections--) }; // done return result; } function web_server_listen(web_server, port, host) { return new _promise2.default(function (resolve, reject) { // Starts HTTP server web_server.listen(port, host, function (error) { if (error) { return reject(error); } resolve(web_server); }); }); } module.exports = exports['default']; //# sourceMappingURL=web service.js.map