1 | ;
|
2 |
|
3 | const 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 |
|
37 | class 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 |
|
328 | function 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 |
|
421 | function 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 |
|
431 | function 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 |
|
544 | function 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 |
|
592 | function 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 |
|
613 | class 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 |
|
630 | function 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 |
|
757 | module.exports=new Server;
|