1
1
import * as http from 'http' ;
2
2
import * as path from 'path' ;
3
- import * as util from 'util' ;
4
3
import * as fs from 'fs' ;
4
+ import { promisify } from 'util' ;
5
5
import * as marked from 'marked' ;
6
- import serve = require( 'serve-handler' ) ;
6
+ import * as mime from 'mime' ;
7
+ import escape = require( 'escape-html' ) ;
7
8
import enableDestroy = require( 'server-destroy' ) ;
8
9
9
- const readFile = util . promisify ( fs . readFile ) ;
10
+ const readFile = promisify ( fs . readFile ) ;
11
+ const stat = promisify ( fs . stat ) ;
12
+ const readdir = promisify ( fs . readdir ) ;
10
13
11
14
export interface WebServerOptions {
12
15
// The local path that should be mounted as a static web server
@@ -25,30 +28,89 @@ export interface WebServerOptions {
25
28
* @returns Promise that resolves with the instance of the HTTP server
26
29
*/
27
30
export async function startWebServer ( options : WebServerOptions ) {
31
+ const root = path . resolve ( options . root ) ;
28
32
return new Promise < http . Server > ( ( resolve , reject ) => {
29
33
const server = http
30
- . createServer ( async ( req , res ) => {
31
- const pathParts = req . url ! . split ( '/' ) . filter ( x => ! ! x ) ;
32
- if ( pathParts . length > 0 ) {
33
- const ext = path . extname ( pathParts [ pathParts . length - 1 ] ) ;
34
- if ( options . markdown && ext . toLowerCase ( ) === '.md' ) {
35
- const filePath = path . join ( path . resolve ( options . root ) , req . url ! ) ;
36
- const data = await readFile ( filePath , { encoding : 'utf-8' } ) ;
37
- const result = marked ( data , { gfm : true } ) ;
38
- res . writeHead ( 200 , {
39
- 'content-type' : 'text/html' ,
40
- } ) ;
41
- res . end ( result ) ;
42
- return ;
43
- }
44
- }
45
- return serve ( req , res , {
46
- public : options . root ,
47
- directoryListing : options . directoryListing ,
48
- } ) ;
49
- } )
34
+ . createServer ( ( req , res ) => handleRequest ( req , res , root , options ) )
50
35
. listen ( options . port , ( ) => resolve ( server ) )
51
36
. on ( 'error' , reject ) ;
52
37
enableDestroy ( server ) ;
53
38
} ) ;
54
39
}
40
+
41
+ async function handleRequest (
42
+ req : http . IncomingMessage ,
43
+ res : http . ServerResponse ,
44
+ root : string ,
45
+ options : WebServerOptions
46
+ ) {
47
+ const pathParts = req . url ?. split ( '/' ) || [ ] ;
48
+ const originalPath = path . join ( root , ...pathParts ) ;
49
+ if ( req . url ?. endsWith ( '/' ) ) {
50
+ pathParts . push ( 'index.html' ) ;
51
+ }
52
+ const localPath = path . join ( root , ...pathParts ) ;
53
+ if ( ! localPath . startsWith ( root ) ) {
54
+ res . writeHead ( 500 ) ;
55
+ res . end ( ) ;
56
+ return ;
57
+ }
58
+ const maybeListing =
59
+ options . directoryListing && localPath . endsWith ( `${ path . sep } index.html` ) ;
60
+
61
+ try {
62
+ const stats = await stat ( localPath ) ;
63
+ const isDirectory = stats . isDirectory ( ) ;
64
+ if ( isDirectory ) {
65
+ // this means we got a path with no / at the end!
66
+ const doc = "<html><body>Redirectin'</body></html>" ;
67
+ res . statusCode = 301 ;
68
+ res . setHeader ( 'Content-Type' , 'text/html; charset=UTF-8' ) ;
69
+ res . setHeader ( 'Content-Length' , Buffer . byteLength ( doc ) ) ;
70
+ res . setHeader ( 'Location' , req . url + '/' ) ;
71
+ res . end ( doc ) ;
72
+ return ;
73
+ }
74
+ } catch ( err ) {
75
+ if ( ! maybeListing ) {
76
+ return return404 ( res , err ) ;
77
+ }
78
+ }
79
+
80
+ try {
81
+ let data = await readFile ( localPath , { encoding : 'utf8' } ) ;
82
+ let mimeType = mime . getType ( localPath ) ;
83
+ const isMarkdown = req . url ?. toLocaleLowerCase ( ) . endsWith ( '.md' ) ;
84
+ if ( isMarkdown && options . markdown ) {
85
+ data = marked ( data , { gfm : true } ) ;
86
+ mimeType = 'text/html; charset=UTF-8' ;
87
+ }
88
+ res . setHeader ( 'Content-Type' , mimeType ! ) ;
89
+ res . setHeader ( 'Content-Length' , Buffer . byteLength ( data ) ) ;
90
+ res . writeHead ( 200 ) ;
91
+ res . end ( data ) ;
92
+ } catch ( err ) {
93
+ if ( maybeListing ) {
94
+ try {
95
+ const files = await readdir ( originalPath ) ;
96
+ const fileList = files
97
+ . filter ( f => escape ( f ) )
98
+ . map ( f => `<li><a href="${ f } ">${ f } </a></li>` )
99
+ . join ( '\r\n' ) ;
100
+ const data = `<html><body><ul>${ fileList } </ul></body></html>` ;
101
+ res . writeHead ( 200 ) ;
102
+ res . end ( data ) ;
103
+ return ;
104
+ } catch ( err ) {
105
+ return return404 ( res , err ) ;
106
+ }
107
+ } else {
108
+ return return404 ( res , err ) ;
109
+ }
110
+ }
111
+ }
112
+
113
+ function return404 ( res : http . ServerResponse , err : Error ) {
114
+ res . writeHead ( 404 ) ;
115
+ res . end ( JSON . stringify ( err ) ) ;
116
+ }
0 commit comments