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

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