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