node-http-server – Diff between revs 31 and 32

Subversion Repositories:
Rev:
Only display areas with differencesIgnore whitespace
Rev 31 Rev 32
1 #!/usr/bin/env node 1 #!/usr/bin/env node
2   2  
3 /*************************************************************************/ 3 /*************************************************************************/
4 /* Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 */ 4 /* Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 */
5 /*************************************************************************/ 5 /*************************************************************************/
6   6  
7 const url = require('url'); 7 const url = require('url');
8 const path = require('path'); 8 const path = require('path');
9 const fs = require('fs'); 9 const fs = require('fs');
10 const mime = require('mime'); 10 const mime = require('mime');
11 const auth = require("http-auth"); 11 const auth = require("http-auth");
12 const stream = require('stream'); 12 const stream = require('stream');
13 const util = require('util'); 13 const util = require('util');
14 const EventEmitter = require('events').EventEmitter; 14 const EventEmitter = require('events').EventEmitter;
15   15  
16 // Checks whether userPath is a child of rootPath. 16 // Checks whether userPath is a child of rootPath.
17 function isRooted(userPath, rootPath, separator, callback) { 17 function isRooted(userPath, rootPath, separator, callback) {
18 userPath = userPath.split(separator).filter(Boolean); 18 userPath = userPath.split(separator).filter(Boolean);
19 rootPath = rootPath.split(separator).filter(Boolean); 19 rootPath = rootPath.split(separator).filter(Boolean);
20 callback(userPath.length >= rootPath.length && 20 callback(userPath.length >= rootPath.length &&
21 rootPath.every((e, i) => e === userPath[i])); 21 rootPath.every((e, i) => e === userPath[i]));
22 } 22 }
23   23  
24 // Serves files. 24 // Serves files.
25 function files(self, config, file, client) { 25 function files(self, config, file, client) {
26 // Check if the file is accessible. 26 // Check if the file is accessible.
27 fs.access(file, fs.constants.R_OK, (error) => { 27 fs.access(file, fs.constants.R_OK, (error) => {
28 if (error) { 28 if (error) {
29 self.emit('log', { 29 self.emit('log', {
30 message: 'Client: ' + 30 message: 'Client: ' +
31 client.address + ':' + 31 client.address + ':' +
32 client.port + 32 client.port +
33 ' requesting inaccessible path: ' + 33 ' requesting inaccessible path: ' +
34 file, 34 file,
35 severity: 'warning' 35 severity: 'warning'
36 }); 36 });
37 self.emit('data', { 37 self.emit('data', {
38 status: 403, 38 status: 403,
39 data: new stream.Readable({ 39 data: new stream.Readable({
40 read(size) { 40 read(size) {
41 this.push(null); 41 this.push(null);
42 } 42 }
43 }), 43 }),
44 type: 'text/plain' 44 type: 'text/plain'
45 }); 45 });
46 return; 46 return;
47 } 47 }
48 self.emit('log', { 48 self.emit('log', {
49 message: 'Client: ' + 49 message: 'Client: ' +
50 client.address + ':' + 50 client.address + ':' +
51 client.port + 51 client.port +
52 ' sent file: ' + 52 ' sent file: ' +
53 file, 53 file,
54 severity: 'info' 54 severity: 'info'
55 }); 55 });
56 self.emit('data', { 56 self.emit('data', {
57 status: 200, 57 status: 200,
58 data: fs 58 data: fs
59 .createReadStream(file), 59 .createReadStream(file),
60 type: mime 60 type: mime
61 .lookup(file) 61 .lookup(file)
62 }); 62 });
63 }); 63 });
64 } 64 }
65   65  
66 // Serves a directory listing or the document index in case it exists. 66 // Serves a directory listing or the document index in case it exists.
67 function index(self, config, directory, href, client) { 67 function index(self, config, directory, href, client) {
68 const root = path.resolve(directory, config.site.index); 68 const root = path.resolve(directory, config.site.index);
69 fs.stat(root, (error, stats) => { 69 fs.stat(root, (error, stats) => {
70 if (error) { 70 if (error) {
71 if (config.site.indexing 71 if (config.site.indexing
72 .some((directory) => 72 .some((directory) =>
73 directory.toUpperCase() === href.toUpperCase())) { 73 directory.toUpperCase() === href.toUpperCase())) {
74 fs.readdir(directory, (error, paths) => { 74 fs.readdir(directory, (error, paths) => {
75 if (error) { 75 if (error) {
76 self.emit('log', { 76 self.emit('log', {
77 message: 'Client: ' + 77 message: 'Client: ' +
78 client.address + ':' + 78 client.address + ':' +
79 client.port + 79 client.port +
80 ' could not access directory: ' + 80 ' could not access directory: ' +
81 directory, 81 directory,
82 severity: 'warning' 82 severity: 'warning'
83 }); 83 });
84 self.emit('data', { 84 self.emit('data', {
85 status: 500, 85 status: 500,
86 data: new stream.Readable({ 86 data: new stream.Readable({
87 read(size) { 87 read(size) {
88 this.push(null); 88 this.push(null);
89 } 89 }
90 }), 90 }),
91 type: 'text/plain' 91 type: 'text/plain'
92 }); 92 });
93 return; 93 return;
94 } 94 }
95 self.emit('log', { 95 self.emit('log', {
96 message: 'Client: ' + 96 message: 'Client: ' +
97 client.address + ':' + 97 client.address + ':' +
98 client.port + 98 client.port +
99 ' accessed directory listing: ' + 99 ' accessed directory listing: ' +
100 directory, 100 directory,
101 severity: 'info' 101 severity: 'info'
102 }); 102 });
103 self.emit('data', { 103 self.emit('data', {
104 status: 200, 104 status: 200,
105 data: new stream.Readable({ 105 data: new stream.Readable({
106 read(size) { 106 read(size) {
107 this.push(JSON.stringify(paths)); 107 this.push(JSON.stringify(paths));
108 this.push(null); 108 this.push(null);
109 } 109 }
110 }), 110 }),
111 type: 'application/json' 111 type: 'application/json'
112 }); 112 });
113 }); 113 });
114 return; 114 return;
115 } 115 }
116 // Could not access directory index file and directory listing not allowed. 116 // Could not access directory index file and directory listing not allowed.
117 self.emit('log', { 117 self.emit('log', {
118 message: 'Client: ' + 118 message: 'Client: ' +
119 client.address + ':' + 119 client.address + ':' +
120 client.port + 120 client.port +
121 ' no index file found and accessing forbiden index: ' + 121 ' no index file found and accessing forbiden index: ' +
122 href, 122 href,
123 severity: 'warning' 123 severity: 'warning'
124 }); 124 });
125 self.emit('data', { 125 self.emit('data', {
126 status: 403, 126 status: 403,
127 data: new stream.Readable({ 127 data: new stream.Readable({
128 read(size) { 128 read(size) {
129 this.push(null); 129 this.push(null);
130 } 130 }
131 }), 131 }),
132 type: 'text/plain' 132 type: 'text/plain'
133 }); 133 });
134 return; 134 return;
135 } 135 }
136   136  
137 // Serve the document index. 137 // Serve the document index.
138 fs.access(root, fs.constants.R_OK, (error) => { 138 fs.access(root, fs.constants.R_OK, (error) => {
139 if (error) { 139 if (error) {
140 self.emit('log', { 140 self.emit('log', {
141 message: 'Client: ' + 141 message: 'Client: ' +
142 client.address + ':' + 142 client.address + ':' +
143 client.port + 143 client.port +
144 ' unable to access path: ' + 144 ' unable to access path: ' +
145 directory, 145 directory,
146 severity: 'warning' 146 severity: 'warning'
147 }); 147 });
148 self.emit('data', { 148 self.emit('data', {
149 status: 403, 149 status: 403,
150 data: new stream.Readable({ 150 data: new stream.Readable({
151 read(size) { 151 read(size) {
152 this.push(null); 152 this.push(null);
153 } 153 }
154 }), 154 }),
155 type: 'text/plain' 155 type: 'text/plain'
156 }); 156 });
157 return; 157 return;
158 } 158 }
159 self.emit('log', { 159 self.emit('log', {
160 message: 'Client: ' + 160 message: 'Client: ' +
161 client.address + ':' + 161 client.address + ':' +
162 client.port + 162 client.port +
163 ' sent file: ' + 163 ' sent file: ' +
164 root, 164 root,
165 severity: 'info' 165 severity: 'info'
166 }); 166 });
167 self.emit('data', { 167 self.emit('data', {
168 status: 200, 168 status: 200,
169 data: fs.createReadStream(root), 169 data: fs.createReadStream(root),
170 type: mime.lookup(root) 170 type: mime.lookup(root)
171 }); 171 });
172 }); 172 });
173 }); 173 });
174 } 174 }
175   175  
176 // Determines whether the requested filesystem request path is a directory or a file. 176 // Determines whether the requested filesystem request path is a directory or a file.
177 function serve(self, config, local, href, address) { 177 function serve(self, config, local, href, address) {
178 fs.stat(local, (error, stats) => { 178 fs.stat(local, (error, stats) => {
179 // Document does not exist. 179 // Document does not exist.
180 if (error) { 180 if (error) {
181 self.emit('log', { 181 self.emit('log', {
182 message: 'Client: ' + 182 message: 'Client: ' +
183 address.address + ':' + 183 address.address + ':' +
184 address.port + 184 address.port +
185 ' accessing non-existent document: ' + 185 ' accessing non-existent document: ' +
186 local, 186 local,
187 severity: 'warning' 187 severity: 'warning'
188 }); 188 });
189 self.emit('data', { 189 self.emit('data', {
190 status: 404, 190 status: 404,
191 data: new stream.Readable({ 191 data: new stream.Readable({
192 read(size) { 192 read(size) {
193 this.push(null); 193 this.push(null);
194 } 194 }
195 }), 195 }),
196 type: 'text/plain' 196 type: 'text/plain'
197 }); 197 });
198 return; 198 return;
199 } 199 }
200   200  
201 if (stats.isDirectory()) { 201 if (stats.isDirectory()) {
202 // Directory is requested so provide directory indexes. 202 // Directory is requested so provide directory indexes.
203 index(self, config, local, href, address); 203 index(self, config, local, href, address);
204 return; 204 return;
205 } 205 }
206 if (stats.isFile()) { 206 if (stats.isFile()) {
207 const file = path.parse(local).base; 207 const file = path.parse(local).base;
208   208  
209 // If the file matches the reject list or is not in the accept list, 209 // If the file matches the reject list or is not in the accept list,
210 // then there is no file to serve. 210 // then there is no file to serve.
211 if (config.site.reject.some((expression) => expression.test(file)) || 211 if (config.site.reject.some((expression) => expression.test(file)) ||
212 !config.site.accept.some((expression) => expression.test(file))) { 212 !config.site.accept.some((expression) => expression.test(file))) {
213 self.emit('log', { 213 self.emit('log', {
214 message: 'Client: ' + 214 message: 'Client: ' +
215 address.address + ':' + 215 address.address + ':' +
216 address.port + 216 address.port +
217 ' requested disallowed file: ' + 217 ' requested disallowed file: ' +
218 file, 218 file,
219 severity: 'warning' 219 severity: 'warning'
220 }); 220 });
221 self.emit('data', { 221 self.emit('data', {
222 status: 404, 222 status: 404,
223 data: new stream.Readable({ 223 data: new stream.Readable({
224 read(size) { 224 read(size) {
225 this.push(null); 225 this.push(null);
226 } 226 }
227 }), 227 }),
228 type: 'text/plain' 228 type: 'text/plain'
229 }); 229 });
230 return; 230 return;
231 } 231 }
232   232  
233 // A file was requested so provide the file. 233 // A file was requested so provide the file.
234 files(self, config, local, address); 234 files(self, config, local, address);
235 } 235 }
236 }); 236 });
237 } 237 }
238   238  
239 // Constructor. 239 // Constructor.
240 function Handler() { 240 function Handler() {
241 // Create events emitters for logging and data. 241 // Create events emitters for logging and data.
242 EventEmitter.call(this); 242 EventEmitter.call(this);
243 }; 243 };
244   244  
245 // Process a request. 245 // Process a request.
246 Handler.prototype.process = function(config, request, response, root) { 246 Handler.prototype.process = function(config, request, response, root) {
247 EventEmitter.call(this); 247 EventEmitter.call(this);
248 var self = this; 248 var self = this;
249   249  
250 // Get client details. 250 // Get client details.
251 const requestAddress = request.socket.address(); 251 const address = request.socket.address();
252 // Get requested URL. 252 // Get requested URL.
253 const requestURL = url.parse( 253 const requestURL = url.parse(
254 request.url, true 254 request.url, true
255 ); 255 );
256   256  
257 // Perform URL re-writes. 257 // Perform URL re-writes.
258 Object.keys(config.site.rewrite).forEach((key, index) => { 258 Object.keys(config.site.rewrite).forEach((key, index) => {
259 if (config.site.rewrite[key].test(requestURL.path)) { 259 if (config.site.rewrite[key].test(requestURL.path)) {
260 const originalPath = requestURL.path; 260 const originalPath = requestURL.path;
261 requestURL.path = requestURL 261 requestURL.path = requestURL
262 .path 262 .path
263 .replace( 263 .replace(
264 config.site.rewrite[key], key 264 config.site.rewrite[key], key
265 ); 265 );
266 requestURL.pathname = url.parse( 266 requestURL.pathname = url.parse(
267 requestURL 267 requestURL
268 .pathname 268 .pathname
269 .replace( 269 .replace(
270 config.site.rewrite[key], key 270 config.site.rewrite[key], key
271 ), 271 ),
272 true 272 true
273 ) 273 )
274 .pathname; 274 .pathname;
275 self.emit('log', { 275 self.emit('log', {
276 message: 'Rewrite path: ' + 276 message: 'Rewrite path: ' +
277 originalPath + 277 originalPath +
278 ' to: ' + 278 ' to: ' +
279 requestURL.path, 279 requestURL.path,
280 severity: 'info' 280 severity: 'info'
281 }); 281 });
282 } 282 }
283 }); 283 });
284   284  
285 const trimmedPath = requestURL 285 const trimmedPath = requestURL
286 .pathname 286 .pathname
287 .split('/') 287 .split('/')
288 .filter(Boolean) 288 .filter(Boolean)
289 .join('/'); 289 .join('/');
290 const requestPath = trimmedPath === '/' ? 290 const requestPath = trimmedPath === '/' ?
291 path.join(root, trimmedPath) : 291 path.join(root, trimmedPath) :
292 path.resolve(root, trimmedPath); 292 path.resolve(root, trimmedPath);
293   293  
294 fs.realpath(requestPath, (error, resolvedPath) => { 294 fs.realpath(requestPath, (error, resolvedPath) => {
295 // If the path does not exist, then return early. 295 // If the path does not exist, then return early.
296 if (error) { 296 if (error) {
297 self.emit('log', { 297 self.emit('log', {
298 message: 'Unknown path requested: ' + 298 message: 'Unknown path requested: ' +
299 requestAddress.address + ':' + 299 address.address + ':' +
300 requestAddress.port + 300 address.port +
301 ' requesting: ' + 301 ' requesting: ' +
302 requestURL.pathname, 302 requestURL.pathname,
303 severity: 'warning' 303 severity: 'warning'
304 }); 304 });
305 self.emit('done', { 305 self.emit('data', {
306 status: 404, 306 status: 404,
307 data: new stream.Readable({ 307 data: new stream.Readable({
308 read(size) { 308 read(size) {
309 this.push(null); 309 this.push(null);
310 } 310 }
311 }), 311 }),
312 type: 'text/plain' 312 type: 'text/plain'
313 }); 313 });
314 return; 314 return;
315 } 315 }
-   316
316 // Check for path traversals early on and bail if the requested path does not 317 // Check for path traversals early on and bail if the requested path does not
317 // lie within the specified document root. 318 // lie within the specified document root.
318 isRooted(resolvedPath, root, path.sep, (rooted) => { 319 isRooted(resolvedPath, root, path.sep, (rooted) => {
319 if (!rooted) { 320 if (!rooted) {
320 self.emit('log', { 321 self.emit('log', {
321 message: 'Attempted path traversal: ' + 322 message: 'Attempted path traversal: ' +
322 requestAddress.address + ':' + 323 address.address + ':' +
323 requestAddress.port + 324 address.port +
324 ' requesting: ' + 325 ' requesting: ' +
325 requestURL.pathname, 326 requestURL.pathname,
326 severity: 'warning' 327 severity: 'warning'
327 }); 328 });
328 self.emit('done', { 329 self.emit('done', {
329 status: 404, 330 status: 404,
330 data: new stream.Readable({ 331 data: new stream.Readable({
331 read(size) { 332 read(size) {
332 this.push(null); 333 this.push(null);
333 } 334 }
334 }), 335 }),
335 type: 'text/plain' 336 type: 'text/plain'
336 }); 337 });
337 return; 338 return;
338 } 339 }
339   340  
340 // If authentication is required for this path then perform authentication. 341 // If authentication is required for this path then perform authentication.
341 if (config.auth.locations.some( 342 if (config.auth.locations.some(
342 (authPath) => authPath.toUpperCase() === requestURL.pathname.toUpperCase())) { 343 (authPath) => authPath.toUpperCase() === requestURL.pathname.toUpperCase())) {
343 // Create digest authentication. 344 // Create digest authentication.
344 const authentication = auth.digest({ 345 const authentication = auth.digest({
345 realm: config.auth.realm, 346 realm: config.auth.realm,
346 file: path.resolve( 347 file: path.resolve(
347 path.dirname(require.main.filename), 348 path.dirname(require.main.filename),
348 config.auth.digest 349 config.auth.digest
349 ) 350 )
350 }); 351 });
351 // Requested location requires authentication. 352 // Requested location requires authentication.
352 authentication.check(request, response, (request, response) => { 353 authentication.check(request, response, (request, response) => {
353 self.emit('log', { 354 self.emit('log', {
354 message: 'Authenticated client: ' + 355 message: 'Authenticated client: ' +
355 requestAddress.address + ':' + 356 address.address + ':' +
356 requestAddress.port + 357 address.port +
357 ' accessing: ' + 358 ' accessing: ' +
358 requestURL.pathname, 359 requestURL.pathname,
359 severity: 'info' 360 severity: 'info'
360 }); 361 });
361 process.nextTick(() => 362 process.nextTick(() =>
362 serve(self, 363 serve(self,
363 config, 364 config,
364 requestPath, 365 requestPath,
365 requestURL.pathname, 366 requestURL.pathname,
366 requestAddress 367 address
367 ) 368 )
368 ); 369 );
369 }); 370 });
370 return; 371 return;
371 } 372 }
372   373  
373 // If no authentication is required then serve the request. 374 // If no authentication is required then serve the request.
374 self.emit('log', { 375 self.emit('log', {
375 message: 'Client: ' + 376 message: 'Client: ' +
376 requestAddress.address + ':' + 377 address.address + ':' +
377 requestAddress.port + 378 address.port +
378 ' accessing: ' + 379 ' accessing: ' +
379 requestURL.pathname, 380 requestURL.pathname,
380 severity: 'info' 381 severity: 'info'
381 }); 382 });
382 process.nextTick(() => 383 process.nextTick(() =>
383 serve(self, 384 serve(self,
384 config, 385 config,
385 requestPath, 386 requestPath,
386 requestURL.pathname, 387 requestURL.pathname,
387 requestAddress 388 address
388 ) 389 )
389 ); 390 );
390 }); 391 });
391 }); 392 });
392 393
393 return this; 394 return this;
394 }; 395 };
395   396  
396 util.inherits(Handler, EventEmitter); 397 util.inherits(Handler, EventEmitter);
397 module.exports = Handler; 398 module.exports = Handler;
398   399  
399   400  
400
Generated by GNU Enscript 1.6.5.90.
401
Generated by GNU Enscript 1.6.5.90.
401   402  
402   403  
403   404