node-http-server – Diff between revs 35 and 38
?pathlinks?
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) |