-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmicrobi.js
More file actions
396 lines (323 loc) · 10.7 KB
/
microbi.js
File metadata and controls
396 lines (323 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
///////////////////////////////////////////////////////////
//
// microbi.js
//
///////////////////////////////////////////////////////////
//
// Api server and http server for Node.js
//
//
var fs = require( 'fs' )
var http = require( 'http' )
var https = require( 'https' )
var path = require( 'path' )
var url = require( 'url' )
// supported mime types.
// add more if needed.
const mime = {
'css': 'text/css',
'gif': 'image/gif',
'htm': 'text/html',
'html': 'text/html',
'ico': 'image/x-icon',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'js': 'application/javascript',
'json': 'application/json',
'mpeg': 'video/mpeg',
'png': 'image/png',
'pdf': 'application/pdf',
'rar': 'application/x-rar-compressed',
'rtf': 'application/rtf',
'svg': 'image/svg+xml',
'ttf': 'font/ttf',
'txt': 'text/plain',
'wav': 'audio/x-wav',
'weba': 'audio/webm',
'webm': 'video/webm',
'webp': 'image/webp',
'woff': 'font/woff',
'woff2': 'font/woff2',
'xhtml': 'application/xhtml+xml',
'xml': 'application/xml',
'zip': 'application/zip',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2'
}
// The server object. Additional servers can be created with:
// var another_server = Object.create( microbi )
var microbi = {
// api routes are stored here
api: null,
// global mime type for api ops. Individual ops can override it.
apiContentType: mime.txt,
// if the static server is enabled
staticServer: true,
// starts the server on port and ip
start: function( port, ip ) {
port = port || process.argv[ 2 ] || 8080
ip = ip || process.argv[ 3 ] || '127.0.0.1'
var that = this
this.server = http.createServer( function( request, response ) {
onRequest( request, response, that.api, that.staticServer, that.apiContentType )
}).listen( port, ip );
},
// starts https server on port and ip.
// see node documentation for info on options object parameter.
startHttps: function( options, port, ip ) {
port = port || process.argv[ 2 ] || 8080
ip = ip || process.argv[ 3 ] || '127.0.0.1'
var that = this
this.server = https.createServer( function( request, response ) {
onRequest( request, response, that.api, that.staticServer, that.apiContentType )
}).listen( port, ip );
},
// Sets global mime type for api ops, from a extension name
// Example: microbi.setMime("txt")
setMime: function( ext ) {
this.apiContentType = mime[ext]
},
}
/**
* Request responder function. This function is called on each
* request to the server
*
* This is the function that handles incoming requests to the server.
* It does the next things:
* - parses the url.
* - if there is an api op for the url, executes it.
* - if there is no api op, looks to serve a static file at the given url.
*
* @param request Object instance of node http.IncomingMessage.
* @param response Object instance of node http.ServerResponse.
* @param api Object container of api ops.
* @param staticEnabled boolean flag is the static server is enabled.
* @param apiContentType String global mime type for api ops.
*/
var onRequest = function( request, response, api, staticEnabled, apiContentType ) {
// get request method (GET, POST, PUT, etc) and url
var urlObj = url.parse( request.url, true )
// collect request data into an object
requestInfo = {
method: request.method, // "GET", "POST", etc
pathname: urlObj.pathname, // request pathname (i.e: "/stuff/item")
queryParams: urlObj.query, // url params as key:values in an object
body: '', // the request body, empty for now
pathParams: null,
}
if ( ! validatePath( requestInfo.pathname ) ) {
respond404( response )
return
}
if ( api ) {
// check if there is an api op
var apiOp = router.getOp( requestInfo, api )
// streaming api ops are handled here
if ( apiOp && apiOp.stream ) {
response.writeHead( 200, {
'Content-Type': apiOp.mime ? mime[apiOp.mime] : apiContentType
})
apiOp.fn( request, response )
return // request processing completed, exit now.
// normal (non streaming) api ops are handled here
} else if ( apiOp ) {
request.setEncoding( 'utf8' )
requestInfo.pathParams = apiOp.params
// collect the whole body before answering
request.on( 'error', function( e ) {
console.log('Request Error:', e)
respond400( response )
})
request.on( 'data', function( data ) {
requestInfo.body += data
})
request.on( 'end', function() {
response.writeHead( 200, {
'Content-Type': apiOp.mime ? mime[apiOp.mime] : apiContentType
})
response.end( apiOp.fn( requestInfo ) )
})
return // request processing completed, exit now.
}
}
// If the static server has been disabled, don't look for files to
// serve. just exit now with a 404 response.
if ( ! staticEnabled ) {
respond404( response )
return
}
// If the responder function reaches to here, it means that there is no
// api method to server. What is left is to check if there is a file
// to serve at the given path. The static file server only allows for
// GET request. If the request method is not GET, respond 405 and exit.
if ( requestInfo.method != 'GET' ) {
respond405( response )
return
}
serveFile( requestInfo.pathname, request, response )
}
/**
* Serve a static file
* @param pathname String The path for the file to serve.
* @param request Object instance of node http.IncomingMessage.
* @param response Object instance of node http.ServerResponse.
*/
var serveFile = function( pathname, request, response ) {
// If the requested path ends in "/", add "index.html"
if ( pathname[ pathname.length - 1 ] == '/' )
pathname += 'index.html'
var fileToServe = '.' + pathname
var ext = path.extname( fileToServe ).replace( '.', '' )
let mimeExt = mime[ext]
// if there is no mime type, check if it could be a directory
if ( !mimeExt ) {
let file = fs.openSync( fileToServe, 'r' )
let is_dir = fs.fstatSync( file )
if ( is_dir ) {
redirect301( response, pathname + '/' )
return
}
}
// serve file or respond 404 if there is no file
var readStream = fs.createReadStream( fileToServe )
readStream.on( 'error', function() {
respond404( response )
return
})
readStream.once( 'readable', function() {
response.writeHead( 200, {
'Content-Type': mimeExt }
)
// connect the file read stream to the response stream, to serve the file
readStream.pipe( response )
readStream.on( 'end', function() {
response.end()
})
})
}
// The paths requested to the server must match this regex.
// It will only allow letters, numbers underscore, minus
// sign and dots.
var VALID_PATH_REGEX = /^[\./_\-\d\w]*$/
// The path isn't allowed to contain ".." or "/." or "//"
var DISALLOWED_PATH_REGEX = /(\.\.)|(\/\.)|(\/\/)/
/**
* Validate the path
*
* Returns true if the path is valid. False otherwise.
* @param path String path to test.
* @return boolean
*/
var validatePath = function( path ) {
if ( DISALLOWED_PATH_REGEX.test( path ) ) return false
return VALID_PATH_REGEX.test( path )
}
/**
* Emit a 301 response: moved permanently
* @param response Object instance of node http.ServerResponse.
* @param location string redirect url.
*/
var redirect301 = function( response, location ) {
response.writeHead( 301, { 'Location': location } )
response.end()
}
/**
* Emit a 404 response
* @param response Object instance of node http.ServerResponse.
*/
var respond404 = function( response ) {
response.writeHead( 404, { 'Content-Type': mime.txt } )
response.end( '404 Not found.' )
}
/**
* Emit a 405 response: method not allowed
* @param response Object instance of node http.ServerResponse.
*/
var respond405 = function( response ) {
response.writeHead( 405, { 'Content-Type': mime.txt } )
response.end( '405 Method not allowed.' )
}
/**
* Emit a 400 response: Bad request
* @param response Object instance of node http.ServerResponse.
*/
var respond400 = function( response ) {
response.writeHead( 400, { 'Content-Type': mime.txt } )
response.end( '400 Bad request.' )
}
module.exports = microbi
// if it is not being run from node, launch the server
if ( ! module.parent ) {
microbi.start( 8080 )
}
//
// Router functions
//
const router = {}
/**
* Search the api object for a defined api op, for a server request.
*
* @param requestInfo Object describes the incoming request.
* @param api Object Where api ops are stored.
* @return Object An object containing the api op and data.
*/
router.getOp = ( requestInfo, api ) => {
var routeParts = getParts( requestInfo.pathname )
if ( ! routeParts.length ) return null
var pathNode = getPathNode( routeParts, api )
if ( ! pathNode.node ) return null
var apiFunction = pathNode.node[ requestInfo.method ]
if ( ! apiFunction ) return null
var streamFlag = pathNode.node[ requestInfo.method + ':stream' ]
var mimeAlt = pathNode.node[ requestInfo.method + ':mime' ]
// return an object with api op data
return {
fn: apiFunction, // function, or null if there was no api op found
stream: streamFlag, // boolean flag, indicating streaming ops.
params: pathNode.params, // array, with path parameters if any
mime: mimeAlt // alternative mime type, if the op had one defined.
}
}
/**
* Splits a path and discards empty elements
*
* For example for the path:
* stuff/items
* creates the array:
* [ 'stuff', 'items' ]
* @param path String path, example: "/some/path"
* @return Array path pieces (strings)
*/
router.getParts = ( path ) => {
return path.split( '/' ).filter(function(el) { return el })
}
/**
* Takes an array of path pieces, and an api object.
* finds if there is a tree of properties in the api object
* that correspond to the path pieces.
* For example, for the array path:
* [ 'stuff', 'items' ]
* it searches the api object tree as follows:
* api.stuff.items
* return the value of the last object in the tree,
* or null if the required properties are not defined.
* Properties of the form "$x", (if any) will match any path piece,
* and will be returned as path parameters, in an array.
*
* @param paths array the path pieces.
* @param api Object were the api functions are stored.
* @return Object with api node and an array of path params.
*/
router.getPathNode = ( paths, api ) => {
var apiNode = api, paramNode, urlParams = []
for (var i in paths) {
paramNode = apiNode.$x
apiNode = apiNode[ paths[i] ]
if ( ! apiNode && paramNode ) {
apiNode = paramNode
urlParams.push( paths[i] )
}
if ( ! apiNode ) return { node: null, params: null }
}
return { node: apiNode, params: urlParams }
}