UNPKG

actionhero

Version:

actionhero.js is a multi-transport API Server with integrated cluster capabilities and delayed tasks

471 lines (412 loc) 17.1 kB
var url = require('url'); var fs = require('fs'); var path = require('path'); var util = require('util'); var formidable = require('formidable'); var browser_fingerprint = require('browser_fingerprint'); var Mime = require('mime'); var uuid = require('node-uuid'); var initialize = function(api, options, next){ ////////// // INIT // ////////// var type = 'web' var attributes = { canChat: false, logConnections: false, logExits: false, sendWelcomeMessage: false, verbs: [ // no verbs for connections of this type, as they are to be very short-lived ] } var server = new api.genericServer(type, options, attributes); if(['api', 'file'].indexOf(api.config.servers.web.rootEndpointType) < 0){ throw new Error('api.config.servers.web.rootEndpointType can only be \'api\' or \'file\''); } ////////////////////// // REQUIRED METHODS // ////////////////////// server.start = function(next){ if(options.secure === false){ var http = require('http'); server.server = http.createServer(function(req, res){ handleRequest(req, res); }); } else { var https = require('https'); server.server = https.createServer(api.config.servers.web.serverOptions, function(req, res){ handleRequest(req, res); }); } var bootAttempts = 0 server.server.on('error', function(e){ bootAttempts++; if(bootAttempts < api.config.servers.web.bootAttempts){ server.log('cannot boot web server; trying again [' + String(e) + ']', 'error'); if(bootAttempts === 1){ cleanSocket(options.bindIP, options.port); } setTimeout(function(){ server.log('attempting to boot again..') server.server.listen(options.port, options.bindIP); }, 1000) }else{ return next(new Error('cannot start web server @ ' + options.bindIP + ':' + options.port + ' => ' + e.message)); } }); server.server.listen(options.port, options.bindIP, function(){ chmodSocket(options.bindIP, options.port); next(); }); } server.stop = function(next){ server.server.close(); process.nextTick(function(){ next(); }); } server.sendMessage = function(connection, message){ var stringResponse = ''; if(connection.rawConnection.method !== 'HEAD'){ stringResponse = String(message); } connection.rawConnection.responseHeaders.push(['Content-Length', Buffer.byteLength(stringResponse, 'utf8')]); cleanHeaders(connection); var headers = connection.rawConnection.responseHeaders; var responseHttpCode = parseInt(connection.rawConnection.responseHttpCode); connection.rawConnection.res.writeHead(responseHttpCode, headers); connection.rawConnection.res.end(stringResponse); connection.destroy(); } server.sendFile = function(connection, error, fileStream, mime, length){ var foundExpires = false; var foundCacheControl = false; connection.rawConnection.responseHeaders.forEach(function(pair){ if( pair[0].toLowerCase() === 'expires' ) { foundExpires = true; } if( pair[0].toLowerCase() === 'cache-control' ){ foundCacheControl = true; } }) connection.rawConnection.responseHeaders.push(['Content-Type', mime]); connection.rawConnection.responseHeaders.push(['Content-Length', length]); if(foundExpires === false) { connection.rawConnection.responseHeaders.push(['Expires', new Date(new Date().getTime() + api.config.servers.web.flatFileCacheDuration * 1000).toUTCString()]); } if(foundCacheControl === false) { connection.rawConnection.responseHeaders.push(['Cache-Control', 'max-age=' + api.config.servers.web.flatFileCacheDuration + ', must-revalidate, public']); } cleanHeaders(connection); var headers = connection.rawConnection.responseHeaders; if(error){ connection.rawConnection.responseHttpCode = 404 } var responseHttpCode = parseInt(connection.rawConnection.responseHttpCode); connection.rawConnection.res.writeHead(responseHttpCode, headers); if(error){ connection.rawConnection.res.end(String(error)); connection.destroy(); } else { fileStream.pipe(connection.rawConnection.res); fileStream.on('end', function(){ connection.destroy(); }); } }; server.goodbye = function(connection){ // disconnect handlers } //////////// // EVENTS // //////////// server.on('connection', function(connection){ determineRequestParams(connection, function(requestMode){ if(requestMode === 'api'){ server.processAction(connection); } else if(requestMode === 'file'){ server.processFile(connection); } else if(requestMode === 'options'){ respondToOptions(connection); } else if(requestMode === 'trace'){ respondToTrace(connection); } }); }); server.on('actionComplete', function(data){ completeResponse(data); }); ///////////// // HELPERS // ///////////// var handleRequest = function(req, res){ browser_fingerprint.fingerprint(req, api.config.servers.web.fingerprintOptions, function(fingerprint, elementHash, cookieHash){ var responseHeaders = [] var cookies = api.utils.parseCookies(req); var responseHttpCode = 200; var method = req.method.toUpperCase(); var parsedURL = url.parse(req.url, true); var i; for(i in cookieHash){ responseHeaders.push([i, cookieHash[i]]); } // https://github.com/evantahler/actionhero/issues/189 responseHeaders.push(['Content-Type', 'application/json; charset=utf-8']); for(i in api.config.servers.web.httpHeaders){ responseHeaders.push([i, api.config.servers.web.httpHeaders[i]]); } var remoteIP = req.connection.remoteAddress; var remotePort = req.connection.remotePort; // helpers for unix socket bindings with no forward if(!remoteIP && !remotePort){ remoteIP = '0.0.0.0'; remotePort = '0'; } if(req.headers['x-forwarded-for']){ var parts; var forwardedIp = req.headers['x-forwarded-for'].split(',')[0]; if(forwardedIp.indexOf('.') >= 0 || (forwardedIp.indexOf('.') < 0 && forwardedIp.indexOf(':') < 0)){ // IPv4 forwardedIp = forwardedIp.replace('::ffff:',''); // remove any IPv6 information, ie: '::ffff:127.0.0.1' parts = forwardedIp.split(':'); if(parts[0]){ remoteIP = parts[0]; } if(parts[1]){ remotePort = parts[1]; } }else{ // IPv6 parts = api.utils.parseIPv6URI(forwardedIp); if(parts.host){ remoteIP = parts.host; } if(parts.port){ remotePort = parts.port; } } if(req.headers['x-forwarded-port']){ remotePort = req.headers['x-forwarded-port']; } } server.buildConnection({ // will emit 'connection' rawConnection: { req: req, res: res, params: {}, method: method, cookies: cookies, responseHeaders: responseHeaders, responseHttpCode: responseHttpCode, parsedURL: parsedURL }, id: fingerprint + '-' + uuid.v4(), fingerprint: fingerprint, remoteAddress: remoteIP, remotePort: remotePort }); }); } var completeResponse = function(data){ if(data.toRender === true){ if(api.config.servers.web.metadataOptions.serverInformation){ var stopTime = new Date().getTime(); data.response.serverInformation = { serverName: api.config.general.serverName, apiVersion: api.config.general.apiVersion, requestDuration: (stopTime - data.connection.connectedAt), currentTime: stopTime }; } if(api.config.servers.web.metadataOptions.requesterInformation){ data.response.requesterInformation = buildRequesterInformation(data.connection); } if(data.response.error){ if(api.config.servers.web.returnErrorCodes === true && data.connection.rawConnection.responseHttpCode === 200){ if(data.actionStatus === 'unknown_action'){ data.connection.rawConnection.responseHttpCode = 404; }else if(data.actionStatus === 'missing_params'){ data.connection.rawConnection.responseHttpCode = 422; }else if(data.actionStatus === 'server_error'){ data.connection.rawConnection.responseHttpCode = 500; }else{ data.connection.rawConnection.responseHttpCode = 400; } } } if( !data.response.error && data.action && data.params.apiVersion && api.actions.actions[data.params.action][data.params.apiVersion].matchExtensionMimeType === true && data.connection.extension ){ data.connection.rawConnection.responseHeaders.push(['Content-Type', Mime.lookup(data.connection.extension)]); } if(data.response.error && util.isError(data.response.error)){ data.response.error = String( data.response.error.message ); } var stringResponse = ''; if( extractHeader(data.connection, 'Content-Type').match(/json/) ){ stringResponse = JSON.stringify(data.response, null, api.config.servers.web.padding); if(data.params.callback){ data.connection.rawConnection.responseHeaders.push(['Content-Type', 'application/javascript']); stringResponse = data.connection.params.callback + '(' + stringResponse + ');'; } }else{ stringResponse = data.response; } server.sendMessage(data.connection, stringResponse); } } var extractHeader = function(connection, match){ var i = connection.rawConnection.responseHeaders.length - 1 while(i >= 0){ if(connection.rawConnection.responseHeaders[i][0].toLowerCase() === match.toLowerCase()){ return connection.rawConnection.responseHeaders[i][1]; } i--; } return null; } var respondToOptions = function(connection){ if(!api.config.servers.web.httpHeaders['Access-Control-Allow-Methods'] && !extractHeader(connection, 'Access-Control-Allow-Methods')){ var methods = 'HEAD, GET, POST, PUT, DELETE, OPTIONS, TRACE'; connection.rawConnection.responseHeaders.push(['Access-Control-Allow-Methods', methods]); } if(!api.config.servers.web.httpHeaders['Access-Control-Allow-Origin'] && !extractHeader(connection, 'Access-Control-Allow-Origin')){ var origin = '*'; connection.rawConnection.responseHeaders.push(['Access-Control-Allow-Origin', origin]); } server.sendMessage(connection, ''); } var respondToTrace= function(connection){ var data = buildRequesterInformation(connection); var stringResponse = JSON.stringify(data, null, api.config.servers.web.padding); server.sendMessage(connection, stringResponse); } var determineRequestParams = function(connection, callback){ // determine file or api request var requestMode = api.config.servers.web.rootEndpointType; var pathname = connection.rawConnection.parsedURL.pathname var pathParts = pathname.split('/'); var matcherLength, i; while(pathParts[0] === ''){ pathParts.shift(); } if(pathParts[pathParts.length - 1] === ''){ pathParts.pop(); } if(pathParts[0] && pathParts[0] === api.config.servers.web.urlPathForActions){ requestMode = 'api'; pathParts.shift(); }else if(pathParts[0] && pathParts[0] === api.config.servers.web.urlPathForFiles){ requestMode = 'file' pathParts.shift(); }else if(pathParts[0] && pathname.indexOf(api.config.servers.web.urlPathForActions) === 0 ){ requestMode = 'api'; matcherLength = api.config.servers.web.urlPathForActions.split('/').length; for(i = 0; i < (matcherLength - 1); i++){ pathParts.shift(); } }else if(pathParts[0] && pathname.indexOf(api.config.servers.web.urlPathForFiles) === 0 ){ requestMode = 'file' matcherLength = api.config.servers.web.urlPathForFiles.split('/').length; for(i = 0; i < (matcherLength - 1); i++){ pathParts.shift(); } } var extensionParts = connection.rawConnection.parsedURL.pathname.split('.'); if (extensionParts.length > 1){ connection.extension = extensionParts[(extensionParts.length - 1)]; } // OPTIONS if(connection.rawConnection.method === 'OPTIONS'){ requestMode = 'options'; callback(requestMode); } // API else if(requestMode === 'api'){ if(connection.rawConnection.method === 'TRACE'){ requestMode = 'trace'; } fillParamsFromWebRequest(connection, connection.rawConnection.parsedURL.query); connection.rawConnection.params.query = connection.rawConnection.parsedURL.query; if( connection.rawConnection.method !== 'GET' && connection.rawConnection.method !== 'HEAD' && ( connection.rawConnection.req.headers['content-type'] || connection.rawConnection.req.headers['Content-Type'] ) ){ connection.rawConnection.form = new formidable.IncomingForm(); for(i in api.config.servers.web.formOptions){ connection.rawConnection.form[i] = api.config.servers.web.formOptions[i]; } connection.rawConnection.form.parse(connection.rawConnection.req, function(err, fields, files) { if(err){ server.log('error processing form: ' + String(err), 'error'); connection.error = new Error('There was an error processing this form.'); } else { connection.rawConnection.params.body = fields; connection.rawConnection.params.files = files; fillParamsFromWebRequest(connection, files); fillParamsFromWebRequest(connection, fields); } if(api.config.servers.web.queryRouting !== true){ connection.params.action = null; } api.routes.processRoute(connection, pathParts); callback(requestMode); }); }else{ if(api.config.servers.web.queryRouting !== true){ connection.params.action = null; } api.routes.processRoute(connection, pathParts); callback(requestMode); } } // FILE else if(requestMode === 'file'){ if(!connection.params.file){ connection.params.file = pathParts.join(path.sep); } if(connection.params.file === '' || connection.params.file[connection.params.file.length - 1] === '/'){ connection.params.file = connection.params.file + api.config.general.directoryFileType; } callback(requestMode); } } var fillParamsFromWebRequest = function(connection, varsHash){ // helper for JSON posts var collapsedVarsHash = api.utils.collapseObjectToArray(varsHash); if(collapsedVarsHash !== false){ varsHash = {payload: collapsedVarsHash} // post was an array, lets call it "payload" } for(var v in varsHash){ connection.params[v] = varsHash[v]; } } var buildRequesterInformation = function(connection){ var requesterInformation = { id: connection.id, fingerprint: connection.fingerprint, remoteIP: connection.remoteIP, receivedParams: {} }; for(var p in connection.params){ if(api.config.general.disableParamScrubbing === true || api.params.postVariables.indexOf(p) >= 0){ requesterInformation.receivedParams[p] = connection.params[p]; } } return requesterInformation; } var cleanHeaders = function(connection){ var originalHeaders = connection.rawConnection.responseHeaders.reverse(); var foundHeaders = []; var cleanedHeaders = []; for(var i in originalHeaders){ var key = originalHeaders[i][0]; var value = originalHeaders[i][1]; if(foundHeaders.indexOf(key.toLowerCase()) >= 0 && key.toLowerCase().indexOf('set-cookie') < 0 ){ // ignore, it's a duplicate } else if(connection.rawConnection.method === 'HEAD' && key === 'Transfer-Encoding'){ // ignore, we can't send this header for HEAD requests } else { foundHeaders.push(key.toLowerCase()); cleanedHeaders.push([key, value]); } } connection.rawConnection.responseHeaders = cleanedHeaders; } var cleanSocket = function(bindIP, port){ if(!bindIP && port.indexOf('/') >= 0){ fs.unlink(port, function(err){ if(err){ server.log('cannot remove stale socket @' + port + ' : ' + err); }else{ server.log('removed stale unix socket @ ' + port); } }); } } var chmodSocket = function(bindIP, port){ if(!options.bindIP && options.port.indexOf('/') >= 0){ fs.chmodSync(port, 0777); } } next(server); } ///////////////////////////////////////////////////////////////////// // exports exports.initialize = initialize;