node-http-server – Diff between revs 35 and 38

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