node-http-server – Diff between revs 7 and 8
?pathlinks?
Rev 7 | Rev 8 | |||
---|---|---|---|---|
Line 1... | Line 1... | |||
1 | #!/usr/bin/env node |
1 | #!/usr/bin/env node |
|
2 | |
- | ||
3 | /////////////////////////////////////////////////////////////////////////// |
2 | /////////////////////////////////////////////////////////////////////////// |
|
4 | // Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 // |
3 | // Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 // |
|
5 | /////////////////////////////////////////////////////////////////////////// |
4 | /////////////////////////////////////////////////////////////////////////// |
|
Line 6... | Line 5... | |||
6 | |
5 | |
|
7 | // Import packages. |
6 | // Import packages. |
|
8 | const auth = require("http-auth"); |
7 | const auth = require("http-auth"); |
|
9 | const https = require('https'); |
8 | const https = require('https'); |
|
10 | const http = require('http'); |
9 | const http = require('http'); |
|
11 | const path = require('path'); |
10 | const path = require('path'); |
|
12 | const fs = require('fs'); |
- | ||
13 | const mime = require('mime'); |
11 | const fs = require('fs'); |
|
14 | const url = require('url'); |
12 | const url = require('url'); |
|
15 | const moment = require('moment'); |
13 | const moment = require('moment'); |
|
16 | const winston = require('winston'); |
14 | const winston = require('winston'); |
|
17 | const yargs = require('yargs'); |
- | ||
18 | const forge = require('node-forge'); |
15 | const yargs = require('yargs'); |
|
Line -... | Line 16... | |||
- | 16 | const dns = require('dns'); |
||
- | 17 | |
||
- | 18 | // Local imports. |
||
- | 19 | const handler = require( |
||
- | 20 | path |
||
- | 21 | .resolve(__dirname, 'src', 'handler') |
||
- | 22 | ); |
||
- | 23 | const certs = require( |
||
- | 24 | path |
||
- | 25 | .resolve(__dirname, 'src', 'certs') |
||
- | 26 | ); |
||
- | 27 | |
||
- | 28 | // Configuration file. |
||
- | 29 | const config = require( |
||
- | 30 | path |
||
- | 31 | .resolve(__dirname, 'config') |
||
19 | const dns = require('dns'); |
32 | ); |
|
20 | |
33 | |
|
21 | // Get command-line arguments. |
34 | // Get command-line arguments. |
|
22 | const argv = yargs |
35 | const argv = yargs |
|
23 | .version() |
36 | .version() |
|
Line 27... | Line 40... | |||
27 | demandOption: true |
40 | demandOption: true |
|
28 | }) |
41 | }) |
|
29 | .help() |
42 | .help() |
|
30 | .argv |
43 | .argv |
|
Line 31... | Line -... | |||
31 | |
- | ||
32 | // Configuration file. |
- | ||
33 | const config = require( |
- | ||
34 | path |
- | ||
35 | .resolve(__dirname, 'config') |
- | ||
36 | ); |
- | ||
37 | |
- | ||
38 | // Check for path traversal. |
- | ||
39 | function isRooted(userPath, rootPath, separator) { |
- | ||
40 | userPath = userPath.split(separator).filter(Boolean); |
- | ||
41 | rootPath = rootPath.split(separator).filter(Boolean); |
- | ||
42 | return userPath.length >= rootPath.length && |
- | ||
43 | rootPath.every((e, i) => e === userPath[i]); |
- | ||
44 | } |
- | ||
45 | |
- | ||
46 | // Generate certificates on the fly using incremental serials. |
- | ||
47 | function generateCertificates(name, domain, size) { |
- | ||
48 | // Generate 1024-bit key-pair. |
- | ||
49 | const keys = forge |
- | ||
50 | .pki |
- | ||
51 | .rsa |
- | ||
52 | .generateKeyPair(size); |
- | ||
53 | // Create self-signed certificate. |
- | ||
54 | const cert = forge |
- | ||
55 | .pki |
- | ||
56 | .createCertificate(); |
- | ||
57 | cert.serialNumber = moment().format('x'); |
- | ||
58 | cert.publicKey = keys.publicKey; |
- | ||
59 | cert |
- | ||
60 | .validity |
- | ||
61 | .notBefore = moment().toDate(); |
- | ||
62 | cert |
- | ||
63 | .validity |
- | ||
64 | .notAfter |
- | ||
65 | .setFullYear( |
- | ||
66 | cert |
- | ||
67 | .validity |
- | ||
68 | .notBefore |
- | ||
69 | .getFullYear() + 1 |
- | ||
70 | ); |
- | ||
71 | cert.setSubject([{ |
- | ||
72 | name: 'commonName', |
- | ||
73 | value: domain |
- | ||
74 | }, { |
- | ||
75 | name: 'organizationName', |
- | ||
76 | value: name |
- | ||
77 | }]); |
- | ||
78 | cert.setIssuer([{ |
- | ||
79 | name: 'commonName', |
- | ||
80 | value: domain |
- | ||
81 | }, { |
- | ||
82 | name: 'organizationName', |
- | ||
83 | value: name |
- | ||
84 | }]); |
- | ||
85 | |
- | ||
86 | // Self-sign certificate. |
- | ||
87 | cert.sign( |
- | ||
88 | keys.privateKey, |
- | ||
89 | forge |
- | ||
90 | .md |
- | ||
91 | .sha256 |
- | ||
92 | .create() |
- | ||
93 | ); |
- | ||
94 | |
- | ||
95 | // Return PEM-format keys and certificates. |
- | ||
96 | return { |
- | ||
97 | privateKey: forge |
- | ||
98 | .pki |
- | ||
99 | .privateKeyToPem( |
- | ||
100 | keys |
- | ||
101 | .privateKey |
- | ||
102 | ), |
- | ||
103 | publicKey: forge |
- | ||
104 | .pki |
- | ||
105 | .publicKeyToPem( |
- | ||
106 | keys |
- | ||
107 | .publicKey |
- | ||
108 | ), |
- | ||
109 | certificate: forge |
- | ||
110 | .pki |
- | ||
111 | .certificateToPem(cert) |
- | ||
112 | }; |
- | ||
113 | } |
- | ||
114 | |
44 | |
|
115 | // Create various logging mechanisms. |
45 | // Create various logging mechanisms. |
|
116 | const log = new winston.Logger({ |
46 | const log = new winston.Logger({ |
|
117 | transports: [ |
47 | transports: [ |
|
118 | new winston.transports.File({ |
48 | new winston.transports.File({ |
|
Line 134... | Line 64... | |||
134 | }) |
64 | }) |
|
135 | ], |
65 | ], |
|
136 | exitOnError: false |
66 | exitOnError: false |
|
137 | }); |
67 | }); |
|
Line 138... | Line -... | |||
138 | |
- | ||
139 | function handleClient(request, response, documentRoot) { |
- | ||
140 | const requestAddress = request.socket.address(); |
- | ||
141 | const requestedURL = url.parse(request.url, true); |
- | ||
142 | |
- | ||
143 | log.info('Client: ' + |
- | ||
144 | requestAddress.address + ':' + |
- | ||
145 | requestAddress.port + |
- | ||
146 | ' accessing: ' + |
- | ||
147 | requestedURL.pathname |
- | ||
148 | ); |
- | ||
149 | |
- | ||
150 | const trimmedPath = requestedURL |
- | ||
151 | .pathname |
- | ||
152 | .split('/') |
- | ||
153 | .filter(Boolean) |
- | ||
154 | .join('/'); |
- | ||
155 | const filesystemPath = trimmedPath === '/' ? |
- | ||
156 | path.join(documentRoot, trimmedPath) : |
- | ||
157 | path.resolve(documentRoot, trimmedPath); |
- | ||
158 | |
- | ||
159 | if (!isRooted(filesystemPath, documentRoot, path.sep)) { |
- | ||
160 | log.warn('Attempted path traversal: ' + |
- | ||
161 | requestAddress.address + ':' + |
- | ||
162 | requestAddress.port + |
- | ||
163 | ' requesting: ' + |
- | ||
164 | requestedURL.pathname |
- | ||
165 | ); |
- | ||
166 | response.statusCode = 403; |
- | ||
167 | response.end(); |
- | ||
168 | return; |
- | ||
169 | } |
- | ||
170 | |
- | ||
171 | fs.stat(filesystemPath, (error, stats) => { |
- | ||
172 | // Document does not exist. |
- | ||
173 | if (error) { |
- | ||
174 | response.statusCode = 404; |
- | ||
175 | response.end(); |
- | ||
176 | return; |
- | ||
177 | } |
- | ||
178 | |
- | ||
179 | switch (stats.isDirectory()) { |
- | ||
180 | case true: // Directory is requested so provide directory indexes. |
- | ||
181 | const documentRoot = path.resolve(filesystemPath, config.site.index); |
- | ||
182 | fs.stat(documentRoot, (error, stats) => { |
- | ||
183 | if (error) { |
- | ||
184 | fs.readdir(filesystemPath, (error, paths) => { |
- | ||
185 | if (error) { |
- | ||
186 | log.warn('Could not list directory: ' + filesystemPath); |
- | ||
187 | response.statusCode = 500; |
- | ||
188 | response.end(); |
- | ||
189 | return; |
- | ||
190 | } |
- | ||
191 | log.info('Directory listing requested for: ' + filesystemPath); |
- | ||
192 | response.statusCode = 200; |
- | ||
193 | response.write(JSON.stringify(paths)); |
- | ||
194 | response.end(); |
- | ||
195 | }); |
- | ||
196 | |
- | ||
197 | return; |
- | ||
198 | } |
- | ||
199 | |
- | ||
200 | fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
- | ||
201 | if (error) { |
- | ||
202 | log.warn('The server was unable to access the filesystem path: ' + filesystemPath); |
- | ||
203 | response.statusCode = 403; |
- | ||
204 | response.end(); |
- | ||
205 | return; |
- | ||
206 | } |
- | ||
207 | |
- | ||
208 | // Set MIME content type. |
- | ||
209 | response.setHeader('Content-Type', mime.lookup(documentRoot)); |
- | ||
210 | |
- | ||
211 | var readStream = fs.createReadStream(documentRoot) |
- | ||
212 | .on('open', () => { |
- | ||
213 | response.statusCode = 200; |
- | ||
214 | readStream.pipe(response); |
- | ||
215 | }) |
- | ||
216 | .on('error', () => { |
- | ||
217 | response.statusCode = 500; |
- | ||
218 | response.end(); |
- | ||
219 | }); |
- | ||
220 | |
- | ||
221 | }); |
- | ||
222 | |
- | ||
223 | }); |
- | ||
224 | break; |
- | ||
225 | default: // Browser requesting file. |
- | ||
226 | // Check if the file is accessible. |
- | ||
227 | fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
- | ||
228 | if (error) { |
- | ||
229 | response.statusCode = 403; |
- | ||
230 | response.end(); |
- | ||
231 | return; |
- | ||
232 | } |
- | ||
233 | |
- | ||
234 | response.setHeader('Content-Type', mime.lookup(filesystemPath)); |
- | ||
235 | |
- | ||
236 | var readStream = fs.createReadStream(filesystemPath) |
- | ||
237 | .on('open', () => { |
- | ||
238 | response.statusCode = 200; |
- | ||
239 | readStream.pipe(response); |
- | ||
240 | }) |
- | ||
241 | .on('error', () => { |
- | ||
242 | response.statusCode = 500; |
- | ||
243 | response.end(); |
- | ||
244 | }); |
- | ||
245 | |
- | ||
246 | }); |
- | ||
247 | break; |
- | ||
248 | } |
- | ||
249 | }); |
- | ||
250 | } |
- | ||
251 | |
68 | |
|
252 | fs.realpath(argv.root, (error, documentRoot) => { |
69 | fs.realpath(argv.root, (error, root) => { |
|
253 | if (error) { |
70 | if (error) { |
|
254 | log.error('Could not find document root: ' + argv.root); |
71 | log.error('Could not find document root: ' + argv.root); |
|
255 | process.exit(1); |
72 | process.exit(1); |
|
Line 263... | Line 80... | |||
263 | |
80 | |
|
264 | // Start HTTP server. |
81 | // Start HTTP server. |
|
265 | http.createServer( |
82 | http.createServer( |
|
266 | // authentication, |
83 | // authentication, |
|
267 | (request, response) => |
84 | (request, response) => |
|
- | 85 | handler.handleClient(config, request, response, root, (error, level) => { |
||
- | 86 | switch (level) { |
||
- | 87 | case handler.error.level.INFO: |
||
- | 88 | log.info(error); |
||
- | 89 | break; |
||
- | 90 | case handler.error.level.WARN: |
||
- | 91 | log.warn(error); |
||
- | 92 | break; |
||
- | 93 | case handler.error.level.ERROR: |
||
- | 94 | log.error(error); |
||
- | 95 | break; |
||
- | 96 | } |
||
268 | handleClient(request, response, documentRoot) |
97 | }) |
|
269 | ).listen(config.net.port, config.net.address, () => { |
98 | ).listen(config.net.port, config.net.address, () => { |
|
270 | log.info('HTTP Server is listening on: ' + |
99 | log.info('HTTP Server is listening on: ' + |
|
271 | config.net.address + |
100 | config.net.address + |
|
272 | ' and port: ' + |
101 | ' and port: ' + |
|
273 | config.net.port + |
102 | config.net.port + |
|
274 | ' whilst serving files from: ' + |
103 | ' whilst serving files from: ' + |
|
275 | documentRoot |
104 | root |
|
276 | ); |
105 | ); |
|
Line 277... | Line 106... | |||
277 | }); |
106 | }); |
|
278 | |
107 | |
|
279 | // Start HTTPs server if enabled. |
108 | // Start HTTPs server if enabled. |
|
280 | config.ssl.enable && (() => { |
109 | if (config.ssl.enable) { |
|
281 | // Generate certificates for HTTPs. |
110 | // Generate certificates for HTTPs. |
|
282 | const certs = generateCertificates( |
111 | certs.generateCertificates( |
|
283 | config.site.name, |
112 | config.site.name, |
|
- | 113 | config.net.address, |
||
- | 114 | config.ssl.privateKeySize, |
||
- | 115 | (certificates) => { |
||
- | 116 | https.createServer( |
||
- | 117 | // authentication, |
||
- | 118 | { |
||
- | 119 | key: certificates.privateKey, |
||
- | 120 | cert: certificates.certificate, |
||
- | 121 | }, |
||
- | 122 | (request, response) => |
||
- | 123 | handler.handleClient(config, request, response, root, (error, level) => { |
||
- | 124 | switch (level) { |
||
- | 125 | case handler.error.level.INFO: |
||
- | 126 | log.info(error); |
||
- | 127 | break; |
||
- | 128 | case handler.error.level.WARN: |
||
- | 129 | log.warn(error); |
||
- | 130 | break; |
||
- | 131 | case handler.error.level.ERROR: |
||
- | 132 | log.error(error); |
||
- | 133 | break; |
||
- | 134 | } |
||
- | 135 | }) |
||
- | 136 | ).listen(config.ssl.port, config.ssl.address, () => { |
||
- | 137 | // Print information on server startup. |
||
- | 138 | log.info('HTTPs Server is listening on: ' + |
||
- | 139 | config.net.address + |
||
- | 140 | ' and port: ' + |
||
- | 141 | config.net.port + |
||
- | 142 | ' whilst serving files from: ' + |
||
- | 143 | root |
||
- | 144 | ); |
||
284 | config.net.address, |
145 | }) |
|
285 | config.ssl.privateKeySize |
- | ||
286 | ); |
- | ||
287 | |
- | ||
288 | https.createServer( |
- | ||
289 | // authentication, |
- | ||
290 | { |
- | ||
291 | key: certs.privateKey, |
- | ||
292 | cert: certs.certificate, |
- | ||
293 | }, |
- | ||
294 | (request, response) => |
- | ||
295 | handleClient(request, response, documentRoot) |
- | ||
296 | ).listen(config.ssl.port, config.ssl.address, () => { |
- | ||
297 | // Print information on server startup. |
- | ||
298 | log.info('HTTPs Server is listening on: ' + |
- | ||
299 | config.net.address + |
- | ||
300 | ' and port: ' + |
- | ||
301 | config.net.port + |
- | ||
302 | ' whilst serving files from: ' + |
- | ||
303 | documentRoot |
- | ||
304 | ); |
146 | } |
|
305 | }); |
- | ||
306 | })(); |
147 | ); |