1 | var 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 |
|
11 | var MAX_REQUEST_BYTES = 16 * 1024 * 1024
|
12 |
|
13 | var 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 |
|
20 | module.exports = dynalite
|
21 |
|
22 | function 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 |
|
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 |
|
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 |
|
52 | validOperations.forEach(function(action) {
|
53 | action = validations.toLowerFirst(action)
|
54 | actions[action] = require('./actions/' + action)
|
55 | actionValidations[action] = require('./validations/' + action)
|
56 | })
|
57 |
|
58 | function rand52CharId() {
|
59 |
|
60 | var bytes = crypto.randomBytes(39)
|
61 |
|
62 | return bytes.toString('base64').toUpperCase().replace(/\+|\//g, '0')
|
63 | }
|
64 |
|
65 | function 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 |
|
73 |
|
74 |
|
75 | res.end(body)
|
76 | }
|
77 |
|
78 | function 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 |
|
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 |
|
135 | res.contentType = contentType != 'application/x-amz-json-1.0' ? 'application/json' : contentType
|
136 |
|
137 |
|
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 |
|
173 |
|
174 |
|
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 |
|
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 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
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 |
|
269 | if (require.main === module) dynalite().listen(4567)
|