UNPKG

kinesalite

Version:

An implementation of Amazon's Kinesis built on LevelDB

263 lines (216 loc) 9.65 kB
var https = require('https'), http = require('http'), fs = require('fs'), path = require('path'), url = require('url'), crypto = require('crypto'), cbor = require('cbor'), uuid = require('uuid'), validations = require('./validations'), db = require('./db') var MAX_REQUEST_BYTES = 7 * 1024 * 1024 var AMZ_JSON = 'application/x-amz-json-1.1' var AMZ_CBOR = 'application/x-amz-cbor-1.1' var validApis = ['Kinesis_20131202'], validOperations = ['AddTagsToStream', 'CreateStream', 'DeleteStream', 'DescribeStream', 'DescribeStreamSummary', 'GetRecords', 'GetShardIterator', 'ListShards', 'ListStreams', 'ListTagsForStream', 'MergeShards', 'PutRecord', 'PutRecords', 'RemoveTagsFromStream', 'SplitShard', 'IncreaseStreamRetentionPeriod', 'DecreaseStreamRetentionPeriod'], actions = {}, actionValidations = {} module.exports = kinesalite function kinesalite(options) { options = options || {} var server, store = db.create(options), requestHandler = httpHandler.bind(null, store) if (options.ssl) { options.key = options.key || fs.readFileSync(path.join(__dirname, 'ssl', 'server-key.pem')) options.cert = options.cert || fs.readFileSync(path.join(__dirname, 'ssl', 'server-crt.pem')) options.ca = options.ca || fs.readFileSync(path.join(__dirname, 'ssl', 'ca-crt.pem')) server = https.createServer(options, requestHandler) } else { server = http.createServer(requestHandler) } // Ensure we close DB when we're closing the server too var httpServerClose = server.close, httpServerListen = server.listen server.close = function(cb) { store.db.close(function(err) { if (err) return cb(err) // Recreate the store if the user wants to listen again server.listen = function() { store.recreate() httpServerListen.apply(server, arguments) } httpServerClose.call(server, cb) }) } return server } validOperations.forEach(function(action) { action = validations.toLowerFirst(action) actions[action] = require('./actions/' + action) actionValidations[action] = require('./validations/' + action) }) function sendRaw(req, res, body, statusCode) { req.removeAllListeners() res.statusCode = statusCode || 200 if (body != null) { res.setHeader('Content-Length', Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body, 'utf8')) } // AWS doesn't send a 'Connection' header but seems to use keep-alive behaviour // res.setHeader('Connection', '') // res.shouldKeepAlive = false res.end(body) } function sendJson(req, res, data, statusCode) { var body = data != null ? JSON.stringify(data) : '' res.setHeader('Content-Type', res.contentType) sendRaw(req, res, body, statusCode) } function sendCbor(req, res, data, statusCode) { var body = data != null ? cbor.Encoder.encodeOne(data) : '' res.setHeader('Content-Type', res.contentType) sendRaw(req, res, body, statusCode) } function sendResponse(req, res, data, statusCode) { return res.contentType == AMZ_CBOR ? sendCbor(req, res, data, statusCode) : sendJson(req, res, data, statusCode) } function sendError(req, res, contentValid, type, msg) { return contentValid ? sendResponse(req, res, {__type: type, message: msg}, 400) : typeof msg == 'number' ? sendRaw(req, res, '<' + type + '/>\n', msg) : sendRaw(req, res, '<' + type + '>\n <Message>' + msg + '</Message>\n</' + type + '>\n', 403) } function httpHandler(store, req, res) { var body req.on('error', function(err) { throw err }) req.on('data', function(data) { var newLength = data.length + (body ? body.length : 0) if (newLength > MAX_REQUEST_BYTES) { res.setHeader('Transfer-Encoding', 'chunked') return sendRaw(req, res, null, 413) } body = body ? Buffer.concat([body, data], newLength) : data }) req.on('end', function() { // All responses after this point have a RequestId res.setHeader('x-amzn-RequestId', uuid.v1()) if (req.method != 'OPTIONS' || !req.headers.origin) res.setHeader('x-amz-id-2', crypto.randomBytes(72).toString('base64')) // FIRST check if we've got an origin header: if (req.headers.origin) { res.setHeader('Access-Control-Allow-Origin', '*') // If it's a valid OPTIONS call, return here if (req.method == 'OPTIONS') { if (req.headers['access-control-request-headers']) res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers']) if (req.headers['access-control-request-method']) res.setHeader('Access-Control-Allow-Methods', req.headers['access-control-request-method']) res.setHeader('Access-Control-Max-Age', 172800) return sendRaw(req, res, '') } res.setHeader('Access-Control-Expose-Headers', 'x-amzn-RequestId,x-amzn-ErrorType,x-amz-request-id,x-amz-id-2,x-amzn-ErrorMessage,Date') } if (req.method != 'POST') { return sendError(req, res, false, 'AccessDeniedException', 'Unable to determine service/operation name to be authorized') } var contentType = (req.headers['content-type'] || '').split(';')[0].trim() var contentValid = ~[AMZ_JSON, AMZ_CBOR, 'application/json'].indexOf(contentType) var target = (req.headers['x-amz-target'] || '').split('.') var service = target[0] var operation = target[1] var serviceValid = service && ~validApis.indexOf(service) var operationValid = operation && ~validOperations.indexOf(operation) // AWS doesn't seem to care about the HTTP path, so no checking needed for that // THEN if the method and content-type are ok, see if the JSON parses: if (!body) { res.contentType = contentType == AMZ_JSON ? AMZ_JSON : AMZ_CBOR return sendResponse(req, res, {__type: serviceValid && operationValid ? 'SerializationException' : 'UnknownOperationException'}, 400) } if (!contentValid) { if (!service || !operation) { return sendError(req, res, false, 'AccessDeniedException', 'Unable to determine service/operation name to be authorized') } return sendError(req, res, false, 'UnknownOperationException', 404) } res.contentType = contentType var data if (contentType == AMZ_CBOR) { try { data = cbor.Decoder.decodeFirstSync(body) } catch (e) { } } else { try { data = JSON.parse(body.toString()) } catch (e) { } } if (typeof data != 'object' || data == null) { if (contentType == 'application/json') { return sendJson(req, res, { Output: {__type: 'com.amazon.coral.service#SerializationException'}, Version: '1.0', }, 400) } return sendResponse(req, res, {__type: 'SerializationException'}, 400) } // After this point, application/json doesn't seem to progress any further if (contentType == 'application/json') { return sendJson(req, res, { Output: {__type: 'com.amazon.coral.service#UnknownOperationException'}, Version: '1.0', }, 404) } if (!serviceValid || !operationValid) { return sendResponse(req, res, {__type: 'UnknownOperationException'}, 400) } // THEN check auth: var authHeader = req.headers.authorization var query = ~req.url.indexOf('?') ? url.parse(req.url, true).query : {} var authQuery = 'X-Amz-Algorithm' in query var msg = '', params if (authHeader && authQuery) { return sendError(req, res, contentValid, 'InvalidSignatureException', 'Found both \'X-Amz-Algorithm\' as a query-string param and \'Authorization\' as HTTP header.') } if (!authHeader && !authQuery) { return sendError(req, res, contentValid, 'MissingAuthenticationTokenException', 'Missing Authentication Token') } if (authHeader) { params = ['Credential', 'Signature', 'SignedHeaders'] var authParams = authHeader.split(/,| /).slice(1).filter(Boolean).reduce(function(obj, x) { var keyVal = x.trim().split('=') obj[keyVal[0]] = keyVal[1] return obj }, {}) params.forEach(function(param) { if (!authParams[param]) msg += 'Authorization header requires \'' + param + '\' parameter. ' }) if (!req.headers['x-amz-date'] && !req.headers.date) msg += 'Authorization header requires existence of either a \'X-Amz-Date\' or a \'Date\' header. ' if (msg) msg += 'Authorization=' + authHeader } else { params = ['X-Amz-Algorithm', 'X-Amz-Credential', 'X-Amz-Signature', 'X-Amz-SignedHeaders', 'X-Amz-Date'] params.forEach(function(param) { if (!query[param]) msg += 'AWS query-string parameters must include \'' + param + '\'. ' }) if (msg) msg += 'Re-examine the query-string parameters.' } if (msg) { return sendError(req, res, contentValid, 'IncompleteSignatureException', msg) } // If we've reached here, we're good to go: var action = validations.toLowerFirst(operation) var actionValidation = actionValidations[action] try { data = validations.checkTypes(data, actionValidation.types) validations.checkValidations(data, actionValidation.types, actionValidation.custom, operation) } catch (e) { if (e.statusCode) return sendResponse(req, res, e.body, e.statusCode) throw e } actions[action](store, data, function(err, data) { if (err && err.statusCode) return sendResponse(req, res, err.body, err.statusCode) if (err) throw err sendResponse(req, res, data) }) }) } if (require.main === module) kinesalite().listen(4567)