node-http-server – Blame information for rev 7
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | #!/usr/bin/env node |
7 | office | 2 | |
3 | /////////////////////////////////////////////////////////////////////////// |
||
1 | office | 4 | // Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 // |
5 | /////////////////////////////////////////////////////////////////////////// |
||
6 | |||
7 | // Import packages. |
||
8 | const auth = require("http-auth"); |
||
9 | const https = require('https'); |
||
7 | office | 10 | const http = require('http'); |
1 | office | 11 | const path = require('path'); |
12 | const fs = require('fs'); |
||
13 | const mime = require('mime'); |
||
14 | const url = require('url'); |
||
15 | const moment = require('moment'); |
||
16 | const winston = require('winston'); |
||
17 | const yargs = require('yargs'); |
||
6 | office | 18 | const forge = require('node-forge'); |
7 | office | 19 | const dns = require('dns'); |
1 | office | 20 | |
21 | // Get command-line arguments. |
||
22 | const argv = yargs |
||
23 | .version() |
||
24 | .option('root', { |
||
25 | alias: 'd', |
||
26 | describe: 'Path to the document root', |
||
27 | demandOption: true |
||
28 | }) |
||
29 | .help() |
||
30 | .argv |
||
31 | |||
32 | // Configuration file. |
||
7 | office | 33 | const config = require( |
34 | path |
||
35 | .resolve(__dirname, 'config') |
||
36 | ); |
||
1 | office | 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 | |||
7 | office | 46 | // Generate certificates on the fly using incremental serials. |
47 | function generateCertificates(name, domain, size) { |
||
6 | office | 48 | // Generate 1024-bit key-pair. |
7 | office | 49 | const keys = forge |
50 | .pki |
||
51 | .rsa |
||
52 | .generateKeyPair(size); |
||
6 | office | 53 | // Create self-signed certificate. |
7 | office | 54 | const cert = forge |
55 | .pki |
||
56 | .createCertificate(); |
||
57 | cert.serialNumber = moment().format('x'); |
||
6 | office | 58 | cert.publicKey = keys.publicKey; |
7 | office | 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 | }]); |
||
6 | office | 85 | |
86 | // Self-sign certificate. |
||
7 | office | 87 | cert.sign( |
88 | keys.privateKey, |
||
89 | forge |
||
90 | .md |
||
91 | .sha256 |
||
92 | .create() |
||
93 | ); |
||
6 | office | 94 | |
95 | // Return PEM-format keys and certificates. |
||
96 | return { |
||
7 | office | 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) |
||
6 | office | 112 | }; |
113 | } |
||
114 | |||
1 | office | 115 | // Create various logging mechanisms. |
116 | const log = new winston.Logger({ |
||
117 | transports: [ |
||
118 | new winston.transports.File({ |
||
119 | level: 'info', |
||
7 | office | 120 | filename: path.resolve(__dirname, config.log.file), |
1 | office | 121 | handleExceptions: true, |
122 | json: false, |
||
123 | maxsize: 1048576, // 1MiB. |
||
124 | maxFiles: 10, // Ten rotations. |
||
125 | colorize: false, |
||
126 | timestamp: () => moment().format('YYYYMMDDTHHmmss') |
||
127 | }), |
||
128 | new winston.transports.Console({ |
||
129 | level: 'info', |
||
130 | handleExceptions: true, |
||
131 | json: false, |
||
132 | colorize: true, |
||
133 | timestamp: () => moment().format('YYYYMMDDTHHmmss') |
||
134 | }) |
||
135 | ], |
||
136 | exitOnError: false |
||
137 | }); |
||
138 | |||
7 | office | 139 | function handleClient(request, response, documentRoot) { |
140 | const requestAddress = request.socket.address(); |
||
141 | const requestedURL = url.parse(request.url, true); |
||
1 | office | 142 | |
7 | office | 143 | log.info('Client: ' + |
144 | requestAddress.address + ':' + |
||
145 | requestAddress.port + |
||
146 | ' accessing: ' + |
||
147 | requestedURL.pathname |
||
148 | ); |
||
1 | office | 149 | |
7 | office | 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); |
||
1 | office | 158 | |
7 | office | 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 | } |
||
1 | office | 170 | |
7 | office | 171 | fs.stat(filesystemPath, (error, stats) => { |
172 | // Document does not exist. |
||
173 | if (error) { |
||
174 | response.statusCode = 404; |
||
1 | office | 175 | response.end(); |
176 | return; |
||
177 | } |
||
178 | |||
7 | office | 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) => { |
||
1 | office | 185 | if (error) { |
7 | office | 186 | log.warn('Could not list directory: ' + filesystemPath); |
187 | response.statusCode = 500; |
||
1 | office | 188 | response.end(); |
189 | return; |
||
190 | } |
||
7 | office | 191 | log.info('Directory listing requested for: ' + filesystemPath); |
192 | response.statusCode = 200; |
||
193 | response.write(JSON.stringify(paths)); |
||
194 | response.end(); |
||
195 | }); |
||
1 | office | 196 | |
7 | office | 197 | return; |
198 | } |
||
1 | office | 199 | |
200 | fs.access(filesystemPath, fs.constants.R_OK, (error) => { |
||
201 | if (error) { |
||
7 | office | 202 | log.warn('The server was unable to access the filesystem path: ' + filesystemPath); |
1 | office | 203 | response.statusCode = 403; |
204 | response.end(); |
||
205 | return; |
||
206 | } |
||
207 | |||
7 | office | 208 | // Set MIME content type. |
209 | response.setHeader('Content-Type', mime.lookup(documentRoot)); |
||
1 | office | 210 | |
7 | office | 211 | var readStream = fs.createReadStream(documentRoot) |
1 | office | 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 | }); |
||
7 | office | 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 | |||
252 | fs.realpath(argv.root, (error, documentRoot) => { |
||
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, |
||
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 | ); |
||
1 | office | 305 | }); |
7 | office | 306 | })(); |
1 | office | 307 | |
308 | }); |