node-http-server – Diff between revs 1 and 6
?pathlinks?
Rev 1 | Rev 6 | |||
---|---|---|---|---|
1 | #!/usr/bin/env node |
1 | #!/usr/bin/env node |
|
2 | /////////////////////////////////////////////////////////////////////////// |
2 | /////////////////////////////////////////////////////////////////////////// |
|
3 | // Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 // |
3 | // Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 // |
|
4 | /////////////////////////////////////////////////////////////////////////// |
4 | /////////////////////////////////////////////////////////////////////////// |
|
5 | |
5 | |
|
6 | // Import packages. |
6 | // Import packages. |
|
7 | const auth = require("http-auth"); |
7 | const auth = require("http-auth"); |
|
8 | const https = require('https'); |
8 | const https = require('https'); |
|
9 | const path = require('path'); |
9 | const path = require('path'); |
|
10 | const fs = require('fs'); |
10 | const fs = require('fs'); |
|
11 | const mime = require('mime'); |
11 | const mime = require('mime'); |
|
12 | const url = require('url'); |
12 | const url = require('url'); |
|
13 | const moment = require('moment'); |
13 | const moment = require('moment'); |
|
14 | const winston = require('winston'); |
14 | const winston = require('winston'); |
|
15 | const yargs = require('yargs'); |
15 | const yargs = require('yargs'); |
|
- | 16 | const forge = require('node-forge'); |
||
16 | |
17 | |
|
17 | // Get command-line arguments. |
18 | // Get command-line arguments. |
|
18 | const argv = yargs |
19 | const argv = yargs |
|
19 | .version() |
20 | .version() |
|
20 | .option('root', { |
21 | .option('root', { |
|
21 | alias: 'd', |
22 | alias: 'd', |
|
22 | describe: 'Path to the document root', |
23 | describe: 'Path to the document root', |
|
23 | demandOption: true |
24 | demandOption: true |
|
24 | }) |
25 | }) |
|
25 | .help() |
26 | .help() |
|
26 | .argv |
27 | .argv |
|
27 | |
28 | |
|
28 | // Configuration file. |
29 | // Configuration file. |
|
29 | const config = require(path.resolve(__dirname, 'config')); |
30 | const config = require(path.resolve(__dirname, 'config')); |
|
30 | |
31 | |
|
31 | // Check for path traversal. |
32 | // Check for path traversal. |
|
32 | function isRooted(userPath, rootPath, separator) { |
33 | function isRooted(userPath, rootPath, separator) { |
|
33 | userPath = userPath.split(separator).filter(Boolean); |
34 | userPath = userPath.split(separator).filter(Boolean); |
|
34 | rootPath = rootPath.split(separator).filter(Boolean); |
35 | rootPath = rootPath.split(separator).filter(Boolean); |
|
35 | return userPath.length >= rootPath.length && |
36 | return userPath.length >= rootPath.length && |
|
36 | rootPath.every((e, i) => e === userPath[i]); |
37 | rootPath.every((e, i) => e === userPath[i]); |
|
37 | } |
38 | } |
|
- | 39 | |
||
- | 40 | function generateCertificates(name, domain) { |
||
- | 41 | // Generate 1024-bit key-pair. |
||
- | 42 | var keys = forge.pki.rsa.generateKeyPair(1024); |
||
- | 43 | // Create self-signed certificate. |
||
- | 44 | var cert = forge.pki.createCertificate(); |
||
- | 45 | cert.publicKey = keys.publicKey; |
||
- | 46 | cert.validity.notBefore = new Date(); |
||
- | 47 | cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); |
||
- | 48 | cert.setSubject([ |
||
- | 49 | { |
||
- | 50 | name: 'commonName', |
||
- | 51 | value: domain |
||
- | 52 | }, |
||
- | 53 | { |
||
- | 54 | name: 'organizationName', |
||
- | 55 | value: name |
||
- | 56 | } |
||
- | 57 | ]); |
||
- | 58 | cert.setIssuer([ |
||
- | 59 | { |
||
- | 60 | name: 'commonName', |
||
- | 61 | value: name |
||
- | 62 | }, |
||
- | 63 | { |
||
- | 64 | name: 'organizationName', |
||
- | 65 | value: name |
||
- | 66 | } |
||
- | 67 | ]); |
||
- | 68 | |
||
- | 69 | // Self-sign certificate. |
||
- | 70 | cert.sign(keys.privateKey); |
||
- | 71 | |
||
- | 72 | // Return PEM-format keys and certificates. |
||
- | 73 | return { |
||
- | 74 | privateKey: forge.pki.privateKeyToPem(keys.privateKey), |
||
- | 75 | publicKey: forge.pki.publicKeyToPem(keys.publicKey), |
||
- | 76 | certificate: forge.pki.certificateToPem(cert) |
||
- | 77 | }; |
||
- | 78 | } |
||
38 | |
79 | |
|
39 | // Create various logging mechanisms. |
80 | // Create various logging mechanisms. |
|
40 | const log = new winston.Logger({ |
81 | const log = new winston.Logger({ |
|
41 | transports: [ |
82 | transports: [ |
|
42 | new winston.transports.File({ |
83 | new winston.transports.File({ |
|
43 | level: 'info', |
84 | level: 'info', |
|
44 | filename: path.resolve(__dirname, config.server_log), |
85 | filename: path.resolve(__dirname, config.server_log), |
|
45 | handleExceptions: true, |
86 | handleExceptions: true, |
|
46 | json: false, |
87 | json: false, |
|
47 | maxsize: 1048576, // 1MiB. |
88 | maxsize: 1048576, // 1MiB. |
|
48 | maxFiles: 10, // Ten rotations. |
89 | maxFiles: 10, // Ten rotations. |
|
49 | colorize: false, |
90 | colorize: false, |
|
50 | timestamp: () => moment().format('YYYYMMDDTHHmmss') |
91 | timestamp: () => moment().format('YYYYMMDDTHHmmss') |
|
51 | }), |
92 | }), |
|
52 | new winston.transports.Console({ |
93 | new winston.transports.Console({ |
|
53 | level: 'info', |
94 | level: 'info', |
|
54 | handleExceptions: true, |
95 | handleExceptions: true, |
|
55 | json: false, |
96 | json: false, |
|
56 | colorize: true, |
97 | colorize: true, |
|
57 | timestamp: () => moment().format('YYYYMMDDTHHmmss') |
98 | timestamp: () => moment().format('YYYYMMDDTHHmmss') |
|
58 | }) |
99 | }) |
|
59 | ], |
100 | ], |
|
60 | exitOnError: false |
101 | exitOnError: false |
|
61 | }); |
102 | }); |
|
62 | |
103 | |
|
63 | fs.realpath(argv.root, (error, documentRoot) => { |
104 | fs.realpath(argv.root, (error, documentRoot) => { |
|
64 | if (error) { |
105 | if (error) { |
|
65 | log.error('Could not find document root: ' + argv.root); |
106 | log.error('Could not find document root: ' + argv.root); |
|
66 | process.exit(1); |
107 | process.exit(1); |
|
67 | } |
108 | } |
|
68 | |
109 | |
|
69 | var authentication = auth.digest({ |
110 | var authentication = auth.digest({ |
|
70 | realm: "was", |
111 | realm: "was", |
|
71 | file: path.resolve(__dirname, config.password_file) |
112 | file: path.resolve(__dirname, config.password_file) |
|
72 | }); |
113 | }); |
|
- | 114 | |
||
- | 115 | const certs = generateCertificates("was", 'localhost'); |
||
73 | |
116 | |
|
74 | // HTTPs server using digest authentication. |
117 | // HTTPs server using digest authentication. |
|
75 | https.createServer(authentication, { |
118 | https.createServer(authentication, { |
|
76 | key: fs.readFileSync(path.resolve(__dirname, config.key)), |
119 | key: certs.privateKey, |
|
77 | cert: fs.readFileSync(path.resolve(__dirname, config.certificate)), |
120 | cert: certs.certificate, |
|
78 | ca: fs.readFileSync(path.resolve(__dirname, config.ca)), |
- | ||
79 | }, (request, response) => { |
121 | }, (request, response) => { |
|
80 | const requestAddress = request.socket.address(); |
122 | const requestAddress = request.socket.address(); |
|
81 | const requestedURL = url.parse(request.url, true); |
123 | const requestedURL = url.parse(request.url, true); |
|
82 | |
124 | |
|
83 | log.info('Client: ' + requestAddress.address + ':' + requestAddress.port + ' accessing: ' + requestedURL.pathname); |
125 | log.info('Client: ' + requestAddress.address + ':' + requestAddress.port + ' accessing: ' + requestedURL.pathname); |
|
84 | |
126 | |
|
85 | const trimmedPath = requestedURL.pathname.split('/').filter(Boolean).join('/'); |
127 | const trimmedPath = requestedURL.pathname.split('/').filter(Boolean).join('/'); |
|
86 | const filesystemPath = trimmedPath === '/' ? |
128 | const filesystemPath = trimmedPath === '/' ? |
|
87 | path.join(documentRoot, trimmedPath) : |
129 | path.join(documentRoot, trimmedPath) : |
|
88 | path.resolve(documentRoot, trimmedPath); |
130 | path.resolve(documentRoot, trimmedPath); |
|
89 | |
131 | |
|
90 | if (!isRooted(filesystemPath, documentRoot, path.sep)) { |
132 | if (!isRooted(filesystemPath, documentRoot, path.sep)) { |
|
91 | log.warn('Attempted path traversal: ' + requestAddress.address + ':' + requestAddress.port + ' requesting: ' + requestedURL.pathname); |
133 | log.warn('Attempted path traversal: ' + requestAddress.address + ':' + requestAddress.port + ' requesting: ' + requestedURL.pathname); |
|
92 | response.statusCode = 403; |
134 | response.statusCode = 403; |
|
93 | response.end(); |
135 | response.end(); |
|
94 | return; |
136 | return; |
|
95 | } |
137 | } |
|
96 | |
138 | |
|
97 | fs.stat(filesystemPath, (error, stats) => { |
139 | fs.stat(filesystemPath, (error, stats) => { |
|
98 | // Document does not exist. |
140 | // Document does not exist. |
|
99 | if (error) { |
141 | if (error) { |
|
100 | response.statusCode = 404; |
142 | response.statusCode = 404; |
|
101 | response.end(); |
143 | response.end(); |
|
102 | return; |
144 | return; |
|
103 | } |
145 | } |
|
104 | |
146 | |
|
105 | switch (stats.isDirectory()) { |
147 | switch (stats.isDirectory()) { |
|
106 | case true: // Browser requesting directory. |
148 | case true: // Browser requesting directory. |
|
107 | const documentRoot = path.resolve(filesystemPath, config.default_document); |
149 | const documentRoot = path.resolve(filesystemPath, config.default_document); |
|
108 | fs.stat(documentRoot, (error, stats) => { |
150 | fs.stat(documentRoot, (error, stats) => { |
|
109 | if (error) { |
151 | if (error) { |
|
110 | fs.readdir(filesystemPath, (error, paths) => { |
152 | fs.readdir(filesystemPath, (error, paths) => { |
|
111 | if (error) { |
153 | if (error) { |
|
112 | log.warn('Could not list directory: ' + filesystemPath); |
154 | log.warn('Could not list directory: ' + filesystemPath); |
|
113 | response.statusCode = 500; |
155 | response.statusCode = 500; |
|
114 | response.end(); |
156 | response.end(); |
|
115 | return; |
157 | return; |
|
116 | } |
158 | } |
|
117 | log.info('Directory listing requested for: ' + filesystemPath); |
159 | log.info('Directory listing requested for: ' + filesystemPath); |
|
118 | response.statusCode = 200; |
160 | response.statusCode = 200; |
|
119 | response.write(JSON.stringify(paths)); |
161 | response.write(JSON.stringify(paths)); |
|
120 | response.end(); |
162 | response.end(); |
|
121 | }); |
163 | }); |
|
122 | |
164 | |
|
123 | return; |
165 | return; |
|
124 | } |
166 | } |
|
125 | |
167 | |
|
126 | fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
168 | fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
|
127 | if (error) { |
169 | if (error) { |
|
128 | log.warn('The server was unable to access the filesystem path: ' + filesystemPath); |
170 | log.warn('The server was unable to access the filesystem path: ' + filesystemPath); |
|
129 | response.statusCode = 403; |
171 | response.statusCode = 403; |
|
130 | response.end(); |
172 | response.end(); |
|
131 | return; |
173 | return; |
|
132 | } |
174 | } |
|
133 | |
175 | |
|
134 | // Set MIME content type. |
176 | // Set MIME content type. |
|
135 | response.setHeader('Content-Type', mime.lookup(documentRoot)); |
177 | response.setHeader('Content-Type', mime.lookup(documentRoot)); |
|
136 | |
178 | |
|
137 | var readStream = fs.createReadStream(documentRoot) |
179 | var readStream = fs.createReadStream(documentRoot) |
|
138 | .on('open', () => { |
180 | .on('open', () => { |
|
139 | response.statusCode = 200; |
181 | response.statusCode = 200; |
|
140 | readStream.pipe(response); |
182 | readStream.pipe(response); |
|
141 | }) |
183 | }) |
|
142 | .on('error', () => { |
184 | .on('error', () => { |
|
143 | response.statusCode = 500; |
185 | response.statusCode = 500; |
|
144 | response.end(); |
186 | response.end(); |
|
145 | }); |
187 | }); |
|
146 | |
188 | |
|
147 | }); |
189 | }); |
|
148 | |
190 | |
|
149 | }); |
191 | }); |
|
150 | break; |
192 | break; |
|
151 | default: // Browser requesting file. |
193 | default: // Browser requesting file. |
|
152 | // Check if the file is accessible. |
194 | // Check if the file is accessible. |
|
153 | fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
195 | fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
|
154 | if (error) { |
196 | if (error) { |
|
155 | response.statusCode = 403; |
197 | response.statusCode = 403; |
|
156 | response.end(); |
198 | response.end(); |
|
157 | return; |
199 | return; |
|
158 | } |
200 | } |
|
159 | |
201 | |
|
160 | response.setHeader('Content-Type', mime.lookup(filesystemPath)); |
202 | response.setHeader('Content-Type', mime.lookup(filesystemPath)); |
|
161 | |
203 | |
|
162 | var readStream = fs.createReadStream(filesystemPath) |
204 | var readStream = fs.createReadStream(filesystemPath) |
|
163 | .on('open', () => { |
205 | .on('open', () => { |
|
164 | response.statusCode = 200; |
206 | response.statusCode = 200; |
|
165 | readStream.pipe(response); |
207 | readStream.pipe(response); |
|
166 | }) |
208 | }) |
|
167 | .on('error', () => { |
209 | .on('error', () => { |
|
168 | response.statusCode = 500; |
210 | response.statusCode = 500; |
|
169 | response.end(); |
211 | response.end(); |
|
170 | }); |
212 | }); |
|
171 | |
213 | |
|
172 | }); |
214 | }); |
|
173 | break; |
215 | break; |
|
174 | } |
216 | } |
|
175 | }); |
217 | }); |
|
176 | |
218 | |
|
177 | }).listen(config.port, config.address, () => { |
219 | }).listen(config.port, config.address, () => { |
|
178 | log.info('Server is listening on: ' + config.address + ' and port: ' + config.port + ' whilst serving files from: ' + documentRoot); |
220 | log.info('Server is listening on: ' + config.address + ' and port: ' + config.port + ' whilst serving files from: ' + documentRoot); |
|
179 | }); |
221 | }); |
|
180 | }); |
222 | }); |
|
181 | |
223 | |