node-http-server – Blame information for rev 7

Subversion Repositories:
Rev:
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 });