node-http-server – Diff between revs 6 and 7
?pathlinks?
Rev 6 | Rev 7 | |||
---|---|---|---|---|
Line 1... | Line 1... | |||
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 | /////////////////////////////////////////////////////////////////////////// |
|
Line 5... | Line 6... | |||
5 | |
6 | |
|
6 | // Import packages. |
7 | // Import packages. |
|
7 | const auth = require("http-auth"); |
8 | const auth = require("http-auth"); |
|
- | 9 | const https = require('https'); |
||
8 | 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'); |
|
- | 18 | const forge = require('node-forge'); |
||
Line 16... | Line 19... | |||
16 | 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 |
|
Line 25... | Line 28... | |||
25 | }) |
28 | }) |
|
26 | .help() |
29 | .help() |
|
27 | .argv |
30 | .argv |
|
Line 28... | Line 31... | |||
28 | |
31 | |
|
- | 32 | // Configuration file. |
||
- | 33 | const config = require( |
||
29 | // Configuration file. |
34 | path |
|
- | 35 | .resolve(__dirname, 'config') |
||
Line 30... | Line 36... | |||
30 | const config = require(path.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 && |
|
Line -... | Line 43... | |||
- | 43 | rootPath.every((e, i) => e === userPath[i]); |
||
37 | rootPath.every((e, i) => e === userPath[i]); |
44 | } |
|
38 | } |
45 | |
|
- | 46 | // Generate certificates on the fly using incremental serials. |
||
- | 47 | function generateCertificates(name, domain, size) { |
||
- | 48 | // Generate 1024-bit key-pair. |
||
39 | |
49 | const keys = forge |
|
40 | function generateCertificates(name, domain) { |
50 | .pki |
|
- | 51 | .rsa |
||
- | 52 | .generateKeyPair(size); |
||
41 | // Generate 1024-bit key-pair. |
53 | // Create self-signed certificate. |
|
- | 54 | const cert = forge |
||
42 | var keys = forge.pki.rsa.generateKeyPair(1024); |
55 | .pki |
|
- | 56 | .createCertificate(); |
||
- | 57 | cert.serialNumber = moment().format('x'); |
||
43 | // Create self-signed certificate. |
58 | cert.publicKey = keys.publicKey; |
|
- | 59 | cert |
||
- | 60 | .validity |
||
44 | var cert = forge.pki.createCertificate(); |
61 | .notBefore = moment().toDate(); |
|
45 | cert.publicKey = keys.publicKey; |
62 | cert |
|
46 | cert.validity.notBefore = new Date(); |
63 | .validity |
|
- | 64 | .notAfter |
||
47 | cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); |
65 | .setFullYear( |
|
48 | cert.setSubject([ |
66 | cert |
|
49 | { |
67 | .validity |
|
50 | name: 'commonName', |
68 | .notBefore |
|
51 | value: domain |
69 | .getFullYear() + 1 |
|
52 | }, |
70 | ); |
|
53 | { |
71 | cert.setSubject([{ |
|
- | 72 | name: 'commonName', |
||
- | 73 | value: domain |
||
54 | name: 'organizationName', |
74 | }, { |
|
55 | value: name |
75 | name: 'organizationName', |
|
56 | } |
- | ||
57 | ]); |
76 | value: name |
|
58 | cert.setIssuer([ |
77 | }]); |
|
59 | { |
- | ||
60 | name: 'commonName', |
78 | cert.setIssuer([{ |
|
61 | value: name |
79 | name: 'commonName', |
|
62 | }, |
80 | value: domain |
|
63 | { |
- | ||
64 | name: 'organizationName', |
81 | }, { |
|
Line 65... | Line 82... | |||
65 | value: name |
82 | name: 'organizationName', |
|
- | 83 | value: name |
||
66 | } |
84 | }]); |
|
- | 85 | |
||
- | 86 | // Self-sign certificate. |
||
- | 87 | cert.sign( |
||
- | 88 | keys.privateKey, |
||
- | 89 | forge |
||
Line 67... | Line 90... | |||
67 | ]); |
90 | .md |
|
68 | |
91 | .sha256 |
|
- | 92 | .create() |
||
- | 93 | ); |
||
69 | // Self-sign certificate. |
94 | |
|
- | 95 | // Return PEM-format keys and certificates. |
||
- | 96 | return { |
||
- | 97 | privateKey: forge |
||
- | 98 | .pki |
||
- | 99 | .privateKeyToPem( |
||
70 | cert.sign(keys.privateKey); |
100 | keys |
|
- | 101 | .privateKey |
||
- | 102 | ), |
||
- | 103 | publicKey: forge |
||
- | 104 | .pki |
||
- | 105 | .publicKeyToPem( |
||
71 | |
106 | keys |
|
72 | // Return PEM-format keys and certificates. |
107 | .publicKey |
|
73 | return { |
108 | ), |
|
Line 74... | Line 109... | |||
74 | privateKey: forge.pki.privateKeyToPem(keys.privateKey), |
109 | certificate: forge |
|
75 | publicKey: forge.pki.publicKeyToPem(keys.publicKey), |
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', |
|
Line 99... | Line 134... | |||
99 | }) |
134 | }) |
|
100 | ], |
135 | ], |
|
101 | exitOnError: false |
136 | exitOnError: false |
|
102 | }); |
137 | }); |
|
Line 103... | Line 138... | |||
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; |
|
Line 108... | Line -... | |||
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(); |
|
Line 136... | Line -... | |||
136 | return; |
- | ||
137 | } |
- | ||
138 | |
- | ||
139 | fs.stat(filesystemPath, (error, stats) => { |
- | ||
140 | // Document does not exist. |
- | ||
141 | if (error) { |
- | ||
142 | response.statusCode = 404; |
- | ||
143 | response.end(); |
- | ||
144 | return; |
176 | return; |
|
145 | } |
177 | } |
|
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) => { |
- | ||
151 | 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 | } |
182 | fs.stat(documentRoot, (error, stats) => { |
|
167 | |
183 | if (error) { |
|
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(); |
- | ||
173 | return; |
- | ||
174 | } |
188 | response.end(); |
|
175 | |
- | ||
176 | // Set MIME content type. |
- | ||
177 | response.setHeader('Content-Type', mime.lookup(documentRoot)); |
- | ||
178 | |
189 | return; |
|
179 | var readStream = fs.createReadStream(documentRoot) |
190 | } |
|
180 | .on('open', () => { |
- | ||
181 | response.statusCode = 200; |
- | ||
182 | readStream.pipe(response); |
- | ||
183 | }) |
191 | log.info('Directory listing requested for: ' + filesystemPath); |
|
184 | .on('error', () => { |
- | ||
185 | response.statusCode = 500; |
- | ||
186 | response.end(); |
192 | response.statusCode = 200; |
|
Line 187... | Line 193... | |||
187 | }); |
193 | response.write(JSON.stringify(paths)); |
|
188 | |
194 | response.end(); |
|
189 | }); |
- | ||
190 | |
- | ||
- | 195 | }); |
||
191 | }); |
196 | |
|
192 | break; |
197 | return; |
|
- | 198 | } |
||
193 | default: // Browser requesting file. |
199 | |
|
194 | // Check if the file is accessible. |
200 | fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
|
195 | 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); |
|
Line -... | Line 203... | |||
- | 203 | response.statusCode = 403; |
||
197 | response.statusCode = 403; |
204 | response.end(); |
|
Line 198... | Line 205... | |||
198 | response.end(); |
205 | return; |
|
199 | return; |
206 | } |
|
200 | } |
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; |
|
Line 207... | Line 214... | |||
207 | readStream.pipe(response); |
214 | readStream.pipe(response); |
|
208 | }) |
- | ||
209 | .on('error', () => { |
- | ||
210 | response.statusCode = 500; |
- | ||
Line -... | Line 215... | |||
- | 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 | } |
||
211 | response.end(); |
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 | |
||
212 | }); |
246 | }); |
|
- | 247 | break; |
||
- | 248 | } |
||
- | 249 | }); |
||
- | 250 | } |
||
- | 251 | |
||
- | 252 | fs.realpath(argv.root, (error, documentRoot) => { |
||
- | 253 | if (error) { |
||
213 | |
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, |
||
- | 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 + |
||
214 | }); |
300 | ' and port: ' + |