node-http-server – Diff between revs 6 and 7

Subversion Repositories:
Rev:
Only display areas with differencesIgnore whitespace
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