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

Subversion Repositories:
Rev:
Only display areas with differencesIgnore whitespace
Rev 35 Rev 38
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 // 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',
30 'was' 30 'was'
31 ) 31 )
32 ); 32 )
33   33  
34 // Serves files. 34 // Serves files.
35 function files(self, config, file, client, cache) { 35 function files(self, config, file, client, cache) {
36 // Check if the file is accessible. 36 // Check if the file is accessible.
37 fs.access(file, fs.constants.R_OK, (error) => { 37 fs.access(file, fs.constants.R_OK, (error) => {
38 if (error) { 38 if (error) {
39 self.emit('log', { 39 self.emit('log', {
40 message: 'Client: ' + 40 message: 'Client: ' +
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 }
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 })
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
71 .some((directory) => 71 .some((directory) =>
72 directory.toUpperCase() === href.toUpperCase())) { 72 directory.toUpperCase() === href.toUpperCase())) {
73 fs.readdir(directory, (error, paths) => { 73 fs.readdir(directory, (error, paths) => {
74 if (error) { 74 if (error) {
75 self.emit('log', { 75 self.emit('log', {
76 message: 'Client: ' + 76 message: 'Client: ' +
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 }
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) => {
128 if (error) { 128 if (error) {
129 self.emit('log', { 129 self.emit('log', {
130 message: 'Client: ' + 130 message: 'Client: ' +
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 }
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) {
157 fs.stat(local, (error, stats) => { 157 fs.stat(local, (error, stats) => {
158 // Document does not exist. 158 // Document does not exist.
159 if (error) { 159 if (error) {
160 self.emit('log', { 160 self.emit('log', {
161 message: 'Client: ' + 161 message: 'Client: ' +
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 }
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()) {
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.
190 if (config.site.reject.some((expression) => expression.test(file)) || 190 if (config.site.reject.some((expression) => expression.test(file)) ||
191 !config.site.accept.some((expression) => expression.test(file))) { 191 !config.site.accept.some((expression) => expression.test(file))) {
192 self.emit('log', { 192 self.emit('log', {
193 message: 'Client: ' + 193 message: 'Client: ' +
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 }
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 })
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.
221 EventEmitter.call(this); 221 EventEmitter.call(this)
222 }; 222 }
223   223  
224 // Process a request. 224 // Process a request.
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()
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: ' +
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
266 .split('/') 266 .split('/')
267 .filter(Boolean) 267 .filter(Boolean)
268 .join('/'); 268 .join('/')
269 const requestPath = trimmedPath === '/' ? 269 const requestPath = trimmedPath === '/' ?
270 path.join(root, trimmedPath) : 270 path.join(root, trimmedPath) :
271 path.resolve(root, trimmedPath); 271 path.resolve(root, trimmedPath)
272   272  
273 fs.realpath(requestPath, (error, resolvedPath) => { 273 fs.realpath(requestPath, (error, resolvedPath) => {
274 // If the path does not exist, then return early. 274 // If the path does not exist, then return early.
275 if (error) { 275 if (error) {
276 self.emit('log', { 276 self.emit('log', {
277 message: 'Unknown path requested: ' + 277 message: 'Unknown path requested: ' +
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 }
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.
298 was.isRooted(resolvedPath, root, path.sep, (rooted) => { 298 was.isRooted(resolvedPath, root, path.sep, (rooted) => {
299 if (!rooted) { 299 if (!rooted) {
300 self.emit('log', { 300 self.emit('log', {
301 message: 'Attempted path traversal: ' + 301 message: 'Attempted path traversal: ' +
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 }
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
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)
379 util.inherits(Cache, EventEmitter); 379 util.inherits(Cache, EventEmitter)
380 module.exports = Handler; 380 module.exports = Handler
381   381  
382   382  
383
Generated by GNU Enscript 1.6.5.90.
383
Generated by GNU Enscript 1.6.5.90.
384   384  
385   385  
386   386