UNPKG

11.8 kBJavaScriptView Raw
1var http = require('http'),
2 https = require('https'),
3 fs = require('fs'),
4 path = require('path'),
5 url = require('url'),
6 crypto = require('crypto'),
7 crc32 = require('buffer-crc32'),
8 validations = require('./validations'),
9 db = require('./db')
10
11var MAX_REQUEST_BYTES = 16 * 1024 * 1024
12
13var validApis = ['DynamoDB_20111205', 'DynamoDB_20120810'],
14 validOperations = ['BatchGetItem', 'BatchWriteItem', 'CreateTable', 'DeleteItem', 'DeleteTable',
15 'DescribeTable', 'DescribeTimeToLive', 'GetItem', 'ListTables', 'PutItem', 'Query', 'Scan', 'TagResource',
16 'UntagResource', 'ListTagsOfResource', 'UpdateItem', 'UpdateTable'],
17 actions = {},
18 actionValidations = {}
19
20module.exports = dynalite
21
22function dynalite(options) {
23 options = options || {}
24 var server, store = db.create(options), requestHandler = httpHandler.bind(null, store)
25
26 if (options.ssl) {
27 options.key = options.key || fs.readFileSync(path.join(__dirname, 'ssl', 'server-key.pem'))
28 options.cert = options.cert || fs.readFileSync(path.join(__dirname, 'ssl', 'server-crt.pem'))
29 options.ca = options.ca || fs.readFileSync(path.join(__dirname, 'ssl', 'ca-crt.pem'))
30 server = https.createServer(options, requestHandler)
31 } else {
32 server = http.createServer(requestHandler)
33 }
34
35 // Ensure we close DB when we're closing the server too
36 var httpServerClose = server.close, httpServerListen = server.listen
37 server.close = function(cb) {
38 store.db.close(function(err) {
39 if (err) return cb(err)
40 // Recreate the store if the user wants to listen again
41 server.listen = function() {
42 store.recreate()
43 httpServerListen.apply(server, arguments)
44 }
45 httpServerClose.call(server, cb)
46 })
47 }
48
49 return server
50}
51
52validOperations.forEach(function(action) {
53 action = validations.toLowerFirst(action)
54 actions[action] = require('./actions/' + action)
55 actionValidations[action] = require('./validations/' + action)
56})
57
58function rand52CharId() {
59 // 39 bytes turns into 52 base64 characters
60 var bytes = crypto.randomBytes(39)
61 // Need to replace + and / so just choose 0, obvs won't be truly random, whatevs
62 return bytes.toString('base64').toUpperCase().replace(/\+|\//g, '0')
63}
64
65function sendData(req, res, data, statusCode) {
66 var body = JSON.stringify(data)
67 req.removeAllListeners()
68 res.statusCode = statusCode || 200
69 res.setHeader('x-amz-crc32', crc32.unsigned(body))
70 res.setHeader('Content-Type', res.contentType)
71 res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
72 // AWS doesn't send a 'Connection' header but seems to use keep-alive behaviour
73 // res.setHeader('Connection', '')
74 // res.shouldKeepAlive = false
75 res.end(body)
76}
77
78function httpHandler(store, req, res) {
79 var body
80 req.on('error', function(err) { throw err })
81 req.on('data', function(data) {
82 var newLength = data.length + (body ? body.length : 0)
83 if (newLength > MAX_REQUEST_BYTES) {
84 req.removeAllListeners()
85 res.statusCode = 413
86 res.setHeader('Transfer-Encoding', 'chunked')
87 return res.end()
88 }
89 body = body ? Buffer.concat([body, data], newLength) : data
90 })
91 req.on('end', function() {
92
93 body = body ? body.toString() : ''
94
95 // All responses after this point have a RequestId
96 res.setHeader('x-amzn-RequestId', rand52CharId())
97
98 if (req.headers.origin) {
99 res.setHeader('Access-Control-Allow-Origin', '*')
100
101 if (req.method == 'OPTIONS') {
102 if (req.headers['access-control-request-headers'])
103 res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'])
104
105 if (req.headers['access-control-request-method'])
106 res.setHeader('Access-Control-Allow-Methods', req.headers['access-control-request-method'])
107
108 res.setHeader('Access-Control-Max-Age', 172800)
109 res.setHeader('Content-Length', 0)
110 req.removeAllListeners()
111 return res.end()
112 }
113 }
114
115 if (req.method == 'GET') {
116 req.removeAllListeners()
117 res.statusCode = 200
118 res.setHeader('x-amz-crc32', 3128867991)
119 res.setHeader('Content-Length', 42)
120 return res.end('healthy: dynamodb.us-east-1.amazonaws.com ')
121 }
122
123 var contentType = (req.headers['content-type'] || '').split(';')[0].trim()
124
125 if (req.method != 'POST' ||
126 (body && contentType != 'application/json' && contentType != 'application/x-amz-json-1.0')) {
127 req.removeAllListeners()
128 res.statusCode = 404
129 res.setHeader('x-amz-crc32', 3552371480)
130 res.setHeader('Content-Length', 29)
131 return res.end('<UnknownOperationException/>\n')
132 }
133
134 // TODO: Perhaps don't do this
135 res.contentType = contentType != 'application/x-amz-json-1.0' ? 'application/json' : contentType
136
137 // THEN check body, see if the JSON parses:
138
139 var data
140 if (body) {
141 try {
142 data = JSON.parse(body)
143 } catch (e) {
144 return sendData(req, res, {__type: 'com.amazon.coral.service#SerializationException'}, 400)
145 }
146 }
147
148 var target = (req.headers['x-amz-target'] || '').split('.')
149
150 if (target.length != 2 || !~validApis.indexOf(target[0]) || !~validOperations.indexOf(target[1]))
151 return sendData(req, res, {__type: 'com.amazon.coral.service#UnknownOperationException'}, 400)
152
153 var authHeader = req.headers.authorization
154 var query = url.parse(req.url, true).query
155 var authQuery = 'X-Amz-Algorithm' in query
156
157 if (authHeader && authQuery)
158 return sendData(req, res, {
159 __type: 'com.amazon.coral.service#InvalidSignatureException',
160 message: 'Found both \'X-Amz-Algorithm\' as a query-string param and \'Authorization\' as HTTP header.',
161 }, 400)
162
163 if ((!authHeader && !authQuery) || (authHeader && (authHeader.trim().slice(0, 5) != 'AWS4-')))
164 return sendData(req, res, {
165 __type: 'com.amazon.coral.service#MissingAuthenticationTokenException',
166 message: 'Request is missing Authentication Token',
167 }, 400)
168
169 var msg = '', params
170
171 if (authHeader) {
172 // TODO: Go through key-vals first
173 // "'Credential' not a valid key=value pair (missing equal-sign) in Authorization header: 'AWS4-HMAC-SHA256 \
174 // Signature=b, Credential, SignedHeaders'."
175 params = ['Credential', 'Signature', 'SignedHeaders']
176 var authParams = authHeader.split(/,| /).slice(1).filter(Boolean).reduce(function(obj, x) {
177 var keyVal = x.trim().split('=')
178 obj[keyVal[0]] = keyVal[1]
179 return obj
180 }, {})
181 params.forEach(function(param) {
182 if (!authParams[param])
183 // TODO: SignedHeaders *is* allowed to be an empty string at this point
184 msg += 'Authorization header requires \'' + param + '\' parameter. '
185 })
186 if (!req.headers['x-amz-date'] && !req.headers.date)
187 msg += 'Authorization header requires existence of either a \'X-Amz-Date\' or a \'Date\' header. '
188 if (msg) msg += 'Authorization=' + authHeader
189
190 } else {
191 params = ['X-Amz-Algorithm', 'X-Amz-Credential', 'X-Amz-Signature', 'X-Amz-SignedHeaders', 'X-Amz-Date']
192 params.forEach(function(param) {
193 if (!query[param])
194 msg += 'AWS query-string parameters must include \'' + param + '\'. '
195 })
196 if (msg) msg += 'Re-examine the query-string parameters.'
197 }
198
199 if (msg) {
200 return sendData(req, res, {
201 __type: 'com.amazon.coral.service#IncompleteSignatureException',
202 message: msg,
203 }, 400)
204 }
205 // THEN check Date format and expiration
206 // {"__type":"com.amazon.coral.service#IncompleteSignatureException","message":"Date must be in ISO-8601 'basic format'. \
207 // Got '201'. See http://en.wikipedia.org/wiki/ISO_8601"}
208 // {"__type":"com.amazon.coral.service#InvalidSignatureException","message":"Signature expired: 20130301T000000Z is \
209 // now earlier than 20130609T094515Z (20130609T100015Z - 15 min.)"}
210 // THEN check Host is in SignedHeaders (not case sensitive)
211 // {"__type":"com.amazon.coral.service#InvalidSignatureException","message":"'Host' must be a 'SignedHeader' in the AWS Authorization."}
212 // THEN check Algorithm
213 // {"__type":"com.amazon.coral.service#IncompleteSignatureException","message":"Unsupported AWS 'algorithm': \
214 // 'AWS4-HMAC-SHA25' (only AWS4-HMAC-SHA256 for now). "}
215 // THEN check Credential (trailing slashes are ignored)
216 // {"__type":"com.amazon.coral.service#IncompleteSignatureException","message":"Credential must have exactly 5 \
217 // slash-delimited elements, e.g. keyid/date/region/service/term, got 'a/b/c/d'"}
218 // THEN check Credential pieces, all must match exact case, keyid checking throws different error below
219 // {"__type":"com.amazon.coral.service#InvalidSignatureException","message":\
220 // "Credential should be scoped to a valid region, not 'c'. \
221 // Credential should be scoped to correct service: 'dynamodb'. \
222 // Credential should be scoped with a valid terminator: 'aws4_request', not 'e'. \
223 // Date in Credential scope does not match YYYYMMDD from ISO-8601 version of date from HTTP: 'b' != '20130609', from '20130609T095204Z'."}
224 // THEN check keyid
225 // {"__type":"com.amazon.coral.service#UnrecognizedClientException","message":"The security token included in the request is invalid."}
226 // THEN check signature (requires body - will need async)
227 // {"__type":"com.amazon.coral.service#InvalidSignatureException","message":"The request signature we calculated \
228 // does not match the signature you provided. Check your AWS Secret Access Key and signing method. \
229 // Consult the service documentation for details.\n\nThe Canonical String for this request should have \
230 // been\n'POST\n/\n\nhost:dynamodb.ap-southeast-2.amazonaws.com\n\nhost\ne3b0c44298fc1c149afbf4c8996fb92427ae41e46\
231 // 49b934ca495991b7852b855'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\n20130609T\
232 // 100759Z\n20130609/ap-southeast-2/dynamodb/aws4_request\n7b8b82a032afd6014771e3375813fc995dd167b7b3a133a0b86e5925cb000ec5'\n"}
233 // THEN check X-Amz-Security-Token if it exists
234 // {"__type":"com.amazon.coral.service#UnrecognizedClientException","message":"The security token included in the request is invalid"}
235
236 // THEN check types (note different capitalization for Message and poor grammar for a/an):
237
238 // THEN validation checks (note different service):
239 // {"__type":"com.amazon.coral.validate#ValidationException","message":"3 validation errors detected: \
240 // Value \'2147483647\' at \'limit\' failed to satisfy constraint: \
241 // Member must have value less than or equal to 100; \
242 // Value \'89hls;;f;d\' at \'exclusiveStartTableName\' failed to satisfy constraint: \
243 // Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+; \
244 // Value \'89hls;;f;d\' at \'exclusiveStartTableName\' failed to satisfy constraint: \
245 // Member must have length less than or equal to 255"}
246
247 // For some reason, the serialization checks seem to be a bit out of sync
248 if (!body)
249 return sendData(req, res, {__type: 'com.amazon.coral.service#SerializationException'}, 400)
250
251 var action = validations.toLowerFirst(target[1])
252 var actionValidation = actionValidations[action]
253 try {
254 data = validations.checkTypes(data, actionValidation.types)
255 validations.checkValidations(data, actionValidation.types, actionValidation.custom, store)
256 } catch (e) {
257 if (e.statusCode) return sendData(req, res, e.body, e.statusCode)
258 throw e
259 }
260
261 actions[action](store, data, function(err, data) {
262 if (err && err.statusCode) return sendData(req, res, err.body, err.statusCode)
263 if (err) throw err
264 sendData(req, res, data)
265 })
266 })
267}
268
269if (require.main === module) dynalite().listen(4567)