UNPKG

19.3 kBJavaScriptView Raw
1'use strict';
2
3const http = require('http'),
4 https = require('https'),
5 url = require('url'),
6 path = require('path'),
7 fs = require('fs'),
8 querystring=require('querystring'),
9 Config = require(`${__dirname}/Config.js`);
10
11//
12// # [SERVER CLASS](https://github.com/RIAEvangelist/node-http-server#server-class)
13// ---------------
14//
15// You can pass a ` userConfig ` object to shallow merge and/or decorate the ` server.config `
16// when instatiating if you desire.
17// ```javascript
18//
19// const server=require('node-http-server');
20// // server is a Server instation already so to modify or decorate its config, you
21// // must call the server.deploy method.
22//
23// server.deploy({verbose:true})
24//
25//
26// const Server=server.Server;
27// // Server is a refrence to the Server class so you can extend or instatiate it.
28//
29// const myServer=new Server({verbose:true});
30// // this is the instatiated server which now contains your desired defaults.
31// // like the first example, it too can still be modified on deployment
32//
33// myServer.deploy({port:8888},(server)=>{console.log(server)});
34//
35// ```
36
37class Server{
38 // ` server.config ` is where the servers configuration will reside.
39 // It is a new instance of the [Config class](http://riaevangelist.github.io/node-http-server/server/Config.js.html) which will be shallow merged and/or decorated by with
40 // the passed ` userConfig ` if one is passed upon construction of the Server class, or to the ` server.deploy ` method.
41 //
42 // [Detailed info on the server.config or userConfig](https://github.com/RIAEvangelist/node-http-server#custom-configuration)
43 //
44 constructor(userConfig){
45 this.config=new this.Config(userConfig);
46 }
47
48 // #### deploy
49 //
50 // ` server.deploy ` starts the server.
51 //
52 // ` server.deploy(userConfig,readyCallback) `
53 //
54 // |method | returns |
55 // |--------|---------|
56 // | deploy | void |
57 //
58 // | parameter | required | description |
59 // |---------------|----------|-------------|
60 // | userConfig | no | if a ` userConfig ` object is passed it will decorate the [Config class](http://riaevangelist.github.io/node-http-server/Config.js.html) |
61 // | readyCallback | no | called once the server is started |
62 //
63 // ```javascript
64 //
65 // const server=require('node-http-server');
66 //
67 // server.deploy(
68 // {
69 // port:8000,
70 // root:'~/myApp/'
71 // },
72 // serverReady
73 // );
74 //
75 // server.deploy(
76 // {
77 // port:8888,
78 // root:'~/myOtherApp/'
79 // },
80 // serverReady
81 // );
82 //
83 // function serverReady(server){
84 // console.log( `Server on port ${server.config.port} is now up`);
85 // }
86 //
87 // ```
88 //
89 // See the example folder for more detailed examples, or check the node-http-server readme for some [Quickstart Examples for deploying a node server](https://github.com/RIAEvangelist/node-http-server#examples)
90 //
91 get deploy(){
92 return deploy;
93 }
94
95 // #### onRawRequest
96 //
97 // ` server.onRawRequest `
98 //
99 // ` server.onRawRequest(request,response,serve) `
100 //
101 // |method | should return |
102 // |--------|---------|
103 // | onRawRequest | bool/void |
104 //
105 // | parameter | description |
106 // |------------|-------------|
107 // | request | http(s) request obj |
108 // | response | http(s) response obj |
109 // | serve | ref to ` server.serve ` |
110 //
111 //
112 // ```javascript
113 //
114 // const server=require('node-http-server');
115 // const config=new server.Config;
116 //
117 // config.port=8000;
118 //
119 // server.onRawRequest=gotRequest;
120 //
121 // server.deploy(config);
122 //
123 //
124 // function gotRequest(request,response,serve){
125 // console.log(request.uri,request.headers);
126 //
127 // serve(
128 // request,
129 // response,
130 // JSON.stringify(
131 // {
132 // uri:request.uri,
133 // headers:request.headers
134 // }
135 // )
136 // );
137 //
138 // return true;
139 // }
140 //
141 // ```
142 //
143 onRawRequest(request,response,serve){
144
145 }
146
147 // #### onRawRequest
148 //
149 // ` server.onRequest `
150 //
151 // ` server.onRequest(request,response,serve) `
152 //
153 // |method | should return |
154 // |--------|--------------|
155 // | onRequest | bool/void |
156 //
157 // | parameter | description |
158 // |------------|-------------|
159 // | request | raw http(s) request obj |
160 // | response | http(s) response obj |
161 // | serve | ref to ` server.serve ` |
162 //
163 //
164 // ```javascript
165 //
166 // const server=require('node-http-server');
167 // const config=new server.Config;
168 //
169 // config.port=8099;
170 // config.verbose=true;
171 //
172 // server.onRequest=gotRequest;
173 //
174 // server.deploy(config);
175 //
176 //
177 // function gotRequest(request,response,serve){
178 // //at this point the request is decorated with helper members lets take a look at the query params if there are any.
179 // console.log(request.query,request.uri,request.headers);
180 //
181 // //lets only let the requests with a query param of hello go through
182 // if(request.query.hello){
183 // // remember returning false means do not inturrupt the response lifecycle
184 // // and that you will not be manually serving
185 // return false;
186 // }
187 //
188 // serve(
189 // request,
190 // response,
191 // JSON.stringify(
192 // {
193 // success:false,
194 // message:'you must have a query param of hello to access the server i.e. /index.html?hello'
195 // uri:request.uri,
196 // query:request.query
197 // }
198 // )
199 // );
200 //
201 // //now we let the server know we want it to kill the normal request lifecycle
202 // //because we just completed it by serving above. we could also handle it async style
203 // //and request a meme or something from the web and put that on the page (or something...)
204 // return true;
205 // }
206 //
207 // ```
208 //
209 onRequest(request,response,serve){
210
211 }
212
213 // #### beforeServe
214 //
215 // ` server.beforeServe `
216 //
217 // ` server.beforeServe(request,response,body,encoding,serve) `
218 //
219 // |method | should return |
220 // |--------|---------|
221 // | beforeServe | bool/void |
222 //
223 // | parameter | description |
224 // |------------|-------------|
225 // | request | decorated http(s) request obj |
226 // | response | http(s) response obj |
227 // | body | response content body RefString |
228 // | encoding | response body encoding RefString |
229 // | serve | ref to ` server.serve ` |
230 //
231 //
232 // `type RefString`
233 //
234 // |type |keys |description|
235 // |---------|---------|-----------|
236 // |RefString| `value` |a way to allow modifying a string by refrence.|
237 //
238 // ```javascript
239 //
240 // const server=require('node-http-server');
241 //
242 // server.beforeServe=beforeServe;
243 //
244 // function beforeServe(request,response,body,encoding){
245 // //only parsing html files for this example
246 // if(response.getHeader('Content-Type')!=server.config.contentType.html){
247 // //return void||false to allow response lifecycle to continue as normal
248 // return;
249 // }
250 //
251 // const someVariable='this is some variable value';
252 //
253 // body.value=body.value.replace('{{someVariable}}',someVariable);
254 //
255 // //return void||false to allow response lifecycle to continue as normal
256 // //with modified body content
257 // return;
258 // }
259 //
260 // server.deploy(
261 // {
262 // port:8000,
263 // root:`${__dirname}/appRoot/`
264 // }
265 // );
266 //
267 // ```
268 //
269 beforeServe(request,response,body,encoding,serve){
270
271 }
272
273 // #### afterServe
274 //
275 // ` server.afterServe `
276 //
277 // ` server.afterServe(request) `
278 //
279 // |method | should return |
280 // |------------|---------|
281 // | afterServe | n/a |
282 //
283 // | parameter | description |
284 // |------------|-------------|
285 // | request | decorated http(s) request obj |
286 //
287 //
288 // ```javascript
289 //
290 // const server=require('node-http-server');
291 //
292 // server.afterServe=afterServe;
293 //
294 // function afterServe(request){
295 // console.log(`just served ${request.uri}`);
296 // }
297 //
298 // server.deploy(
299 // {
300 // port:8075,
301 // root:`${__dirname}/appRoot/`
302 // }
303 // );
304 //
305 // ```
306 //
307 afterServe(request){
308
309 }
310
311 get serve(){
312 return serve;
313 }
314
315 get serveFile(){
316 return serveFile;
317 }
318
319 get Config(){
320 return Config
321 }
322
323 get Server(){
324 return Server
325 }
326}
327
328function deploy(userConfig, readyCallback=function(){}){
329 Object.defineProperty(
330 this,
331 'server',
332 {
333 value:http.createServer(
334 requestRecieved.bind(this)
335 ),
336 writable:false,
337 enumerable:true
338 }
339 );
340
341 if(userConfig){
342 Object.assign(this.config,userConfig);
343 }
344
345 if(this.config.https && this.config.https.privateKey && this.config.https.certificate){
346 if(!this.config.https.port){
347 this.config.https.port=443;
348 }
349 this.config.httpsOptions = {
350 key: fs.readFileSync(this.config.https.privateKey),
351 cert: fs.readFileSync(this.config.https.certificate),
352 passphrase: this.config.https.passphrase
353 };
354
355 if(this.config.https.ca){
356 this.config.httpsOptions.ca=fs.readFileSync(this.config.https.ca);
357 }
358
359 Object.defineProperty(
360 this,
361 'secureServer',
362 {
363 value:https.createServer(
364 this.config.httpsOptions,
365 requestRecieved.bind(this)
366 ),
367 writable:false,
368 enumerable:true
369 }
370 );
371 }else{
372 this.config.https={
373 only:false
374 };
375 }
376
377 this.config.logID=`### ${this.config.domain} server`;
378
379 if(this.config.verbose){
380 console.log(
381 `${this.config.logID} configured with ###\n\n`,this.config);
382 }
383
384 this.server.timeout=this.config.server.timeout;
385
386 if(!this.config.https.only){
387 this.server.listen(
388 this.config.port,
389 function() {
390 if(this.config.verbose){
391 console.log(`${this.config.logID} listening on port ${this.config.port} ###\n\n`);
392 }
393
394 //The ` readyCallback ` is passed the full server instance
395 // so that you may use the same callback to handle multiple
396 // server instances in the same code instead of writing an inline
397 // callback... think about it ;)
398 //
399 readyCallback(this);
400
401 }.bind(this)
402 );
403 }
404
405 if(this.config.httpsOptions){
406 this.secureServer.listen(
407 this.config.https.port,
408 function() {
409 if(this.config.verbose){
410 console.log(`HTTPS: ${this.config.logID} listening on port ${this.config.https.port} ###\n\n`);
411 }
412 // for example the same ready callback could handle both the
413 // https and http servers with a simple test for the servers port
414 //
415 readyCallback(this);
416 }.bind(this)
417 );
418 }
419}
420
421function setHeaders(response,headers){
422 const keys=Object.keys(headers);
423 for(const i in keys){
424 response.setHeader(
425 keys[i],
426 headers[keys[i]]
427 );
428 }
429}
430
431function serveFile(filename,exists,request,response) {
432 if(!exists) {
433 if(this.config.verbose){
434 console.log(`${this.config.logID} 404 ###\n\n`);
435 }
436
437 if(!response){
438 return false;
439 }
440
441 response.statusCode=404;
442 setHeaders(response, this.config.errors.headers);
443
444 this.serve(
445 request,
446 response,
447 this.config.errors['404']
448 );
449 return;
450 }
451
452 const contentType = path.extname(filename).slice(1);
453
454 if(!this.config.contentType[contentType]){
455 if(this.config.verbose){
456 console.log(`${this.config.logID} 415 ###\n\n`);
457 }
458
459 response.statusCode=415;
460 setHeaders(response, this.config.errors.headers);
461
462 this.serve(
463 request,
464 response,
465 this.config.errors['415']
466 );
467 return;
468 }
469
470 if (
471 fs.statSync(filename).isDirectory()
472 ){
473 filename+=`/${this.config.server.index}`;
474 }
475
476 if (
477 this.config.restrictedType[contentType]
478 ){
479 if(this.config.verbose){
480 console.log(`${this.config.logID} 403 ###\n\n`);
481 }
482
483 response.statusCode=403;
484 setHeaders(response, this.config.errors.headers);
485
486 this.serve(
487 request,
488 response,
489 this.config.errors['403']
490 );
491 return;
492 }
493
494 fs.readFile(
495 filename,
496 'binary',
497 function(err, file) {
498 if(err) {
499 if(this.config.verbose){
500 console.log(`${this.config.logID} 500 ###\n\n`,err,'\n\n');
501 }
502
503 response.statusCode=500;
504 setHeaders(response, this.config.errors.headers);
505
506 this.serve(
507 request,
508 response,
509 this.config.errors['500'].replace(/\{\{err\}\}/g,err)
510 );
511 return;
512 }
513
514 response.setHeader(
515 'Content-Type',
516 this.config.contentType[contentType]
517 );
518
519 if(this.config.server.noCache){
520 response.setHeader(
521 'Cache-Control',
522 'no-cache, no-store, must-revalidate'
523 );
524 }
525
526 response.statusCode=200;
527
528 this.serve(
529 request,
530 response,
531 file,
532 'binary'
533 );
534
535 if(this.config.verbose){
536 console.log(`${this.config.logID} 200 ###\n\n`);
537 }
538
539 return;
540 }.bind(this)
541 );
542}
543
544function serve(request,response,body,encoding){
545 if(!response.statusCode){
546 response.statusCode=200;
547 }
548
549 if(!response.getHeader('Content-Type')){
550 response.setHeader(
551 'Content-Type',
552 'text/plain'
553 );
554
555 if(this.config.verbose){
556 console.log(`${this.config.logID} response content-type header not specified ###\n\nContent-Type set to: text/plain\n\n`);
557 }
558 }
559
560 if(!encoding){
561 encoding='utf8';
562
563 if(this.config.verbose){
564 console.log(`${this.config.logID} encoding not specified ###\nencoding set to:\n`,encoding,'\n\n');
565 }
566 }
567
568 const refBody=new RefString;
569 const refEncoding=new RefString;
570
571 refBody.value=body;
572 refEncoding.value=encoding;
573
574 //return any value to force or specify delayed or manual serving
575 if(
576 this.beforeServe(
577 request,
578 response,
579 refBody,
580 refEncoding,
581 completeServing.bind(this)
582 )
583 ){
584 return;
585 };
586
587 completeServing.bind(this)(request,response,refBody,encoding);
588
589 return;
590}
591
592function completeServing(request,response,refBody,refEncoding){
593 if(!(refBody instanceof RefString)){
594 refBody=new RefString(refBody);
595 }
596
597 if(!(refEncoding instanceof RefString)){
598 refEncoding=new RefString(refEncoding||'binary');
599 }
600
601 if(response.finished){
602 this.afterServe(request);
603 return;
604 }
605
606 response.end(
607 refBody.value,
608 refEncoding.value,
609 this.afterServe.bind(this,request)
610 );
611}
612
613class RefString{
614 constructor(value){
615 if(value){
616 this._string=value;
617 }
618 }
619
620 get value(){
621 return this._string;
622 }
623
624 set value(value){
625 this._string=value;
626 return this._string;
627 }
628}
629
630function requestRecieved(request,response){
631 if(this.config.log){
632 const logData={
633 method : request.method,
634 url : request.url,
635 headers : request.headers
636 };
637
638 this.config.logFunction(
639 logData
640 );
641 }
642
643 let uri = url.parse(request.url);
644 uri.protocol='http';
645 uri.host=uri.hostname=request.headers.host;
646 uri.port=80;
647 uri.query=querystring.parse(uri.query);
648
649 if(request.connection.encrypted){
650 uri.protocol='https';
651 uri.port=443;
652 }
653
654 (
655 function(){
656 if(!uri.host){
657 return;
658 }
659 const host=uri.host.split(':');
660
661 if(!host[1]){
662 return;
663 }
664 uri.host=uri.hostname=host[0];
665 uri.port=host[1];
666 }
667 )();
668
669 for(let key in uri){
670 if(uri[key]!==null){
671 continue;
672 }
673 uri[key]='';
674 }
675
676 request.uri=uri;
677
678 if(
679 this.onRawRequest(
680 request,
681 response,
682 completeServing.bind(this)
683 )
684 ){
685 return;
686 };
687
688 uri=uri.pathname;
689
690 if (uri=='/'){
691 uri=`/${this.config.server.index}`;
692 }
693
694 let hostname= [];
695
696 if (request.headers.host !== undefined){
697 hostname = request.headers.host.split(':');
698 }
699
700 let root = this.config.root;
701
702 if(this.config.verbose){
703 console.log(`${this.config.logID} REQUEST ###\n\n`,
704 request.headers,'\n',
705 uri,'\n\n',
706 hostname,'\n\n'
707 );
708 }
709
710 if(this.config.domain!='0.0.0.0' && hostname.length > 0 && hostname[0]!=this.config.domain){
711 if(!this.config.domains[hostname[0]]){
712 if(this.config.verbose){
713 console.log(`${this.config.logID} INVALID HOST ###\n\n`);
714 }
715 this.serveFile(hostname[0],false,response);
716 return;
717 }
718 root=this.config.domains[hostname[0]];
719 }
720
721
722 if(this.config.verbose){
723 console.log(`${this.config.logID} USING ROOT : ${root}###\n\n`);
724 }
725
726 if(uri.slice(-1)=='/'){
727 uri+=this.config.server.index;
728 }
729
730 request.url=uri;
731 request.serverRoot=root;
732
733 //return any value to force or specify delayed or manual serving
734 if(
735 this.onRequest(
736 request,
737 response,
738 completeServing.bind(this)
739 )
740 ){
741 return;
742 };
743
744 const filename = path.join(
745 request.serverRoot,
746 request.url
747 );
748
749 fs.exists(
750 filename,
751 function fileExists(exists){
752 this.serveFile(filename,exists,request,response);
753 }.bind(this)
754 );
755}
756
757module.exports=new Server;