node-http-server – Blame information for rev 32

Subversion Repositories:
Rev:
Rev Author Line No. Line
8 office 1 #!/usr/bin/env node
2  
9 office 3 /*************************************************************************/
4 /* Copyright (C) 2017 Wizardry and Steamworks - License: GNU GPLv3 */
5 /*************************************************************************/
6  
8 office 7 const url = require('url');
8 const path = require('path');
9 const fs = require('fs');
10 const mime = require('mime');
14 office 11 const auth = require("http-auth");
30 office 12 const stream = require('stream');
31 office 13 const util = require('util');
14 const EventEmitter = require('events').EventEmitter;
8 office 15  
14 office 16 // Checks whether userPath is a child of rootPath.
15 office 17 function isRooted(userPath, rootPath, separator, callback) {
25 office 18 userPath = userPath.split(separator).filter(Boolean);
19 rootPath = rootPath.split(separator).filter(Boolean);
20 callback(userPath.length >= rootPath.length &&
21 rootPath.every((e, i) => e === userPath[i]));
8 office 22 }
23  
14 office 24 // Serves files.
31 office 25 function files(self, config, file, client) {
25 office 26 // Check if the file is accessible.
29 office 27 fs.access(file, fs.constants.R_OK, (error) => {
25 office 28 if (error) {
31 office 29 self.emit('log', {
29 office 30 message: 'Client: ' +
31 client.address + ':' +
32 client.port +
30 office 33 ' requesting inaccessible path: ' +
29 office 34 file,
30 office 35 severity: 'warning'
31 office 36 });
37 self.emit('data', {
30 office 38 status: 403,
39 data: new stream.Readable({
40 read(size) {
41 this.push(null);
42 }
43 }),
44 type: 'text/plain'
25 office 45 });
30 office 46 return;
47 }
31 office 48 self.emit('log', {
30 office 49 message: 'Client: ' +
50 client.address + ':' +
51 client.port +
52 ' sent file: ' +
53 file,
54 severity: 'info'
31 office 55 });
56 self.emit('data', {
30 office 57 status: 200,
58 data: fs
59 .createReadStream(file),
60 type: mime
61 .lookup(file)
29 office 62 });
25 office 63 });
15 office 64 }
65  
66 // Serves a directory listing or the document index in case it exists.
31 office 67 function index(self, config, directory, href, client) {
29 office 68 const root = path.resolve(directory, config.site.index);
25 office 69 fs.stat(root, (error, stats) => {
70 if (error) {
71 if (config.site.indexing
72 .some((directory) =>
29 office 73 directory.toUpperCase() === href.toUpperCase())) {
74 fs.readdir(directory, (error, paths) => {
25 office 75 if (error) {
31 office 76 self.emit('log', {
29 office 77 message: 'Client: ' +
78 client.address + ':' +
79 client.port +
30 office 80 ' could not access directory: ' +
29 office 81 directory,
30 office 82 severity: 'warning'
31 office 83 });
84 self.emit('data', {
30 office 85 status: 500,
86 data: new stream.Readable({
87 read(size) {
88 this.push(null);
89 }
90 }),
91 type: 'text/plain'
29 office 92 });
30 office 93 return;
94 }
31 office 95 self.emit('log', {
30 office 96 message: 'Client: ' +
97 client.address + ':' +
98 client.port +
99 ' accessed directory listing: ' +
100 directory,
101 severity: 'info'
31 office 102 });
103 self.emit('data', {
30 office 104 status: 200,
105 data: new stream.Readable({
106 read(size) {
107 this.push(JSON.stringify(paths));
108 this.push(null);
109 }
110 }),
111 type: 'application/json'
25 office 112 });
113 });
114 return;
115 }
29 office 116 // Could not access directory index file and directory listing not allowed.
31 office 117 self.emit('log', {
30 office 118 message: 'Client: ' +
119 client.address + ':' +
120 client.port +
121 ' no index file found and accessing forbiden index: ' +
122 href,
123 severity: 'warning'
31 office 124 });
125 self.emit('data', {
30 office 126 status: 403,
127 data: new stream.Readable({
128 read(size) {
129 this.push(null);
130 }
131 }),
132 type: 'text/plain'
27 office 133 });
25 office 134 return;
135 }
15 office 136  
25 office 137 // Serve the document index.
138 fs.access(root, fs.constants.R_OK, (error) => {
139 if (error) {
31 office 140 self.emit('log', {
29 office 141 message: 'Client: ' +
142 client.address + ':' +
143 client.port +
30 office 144 ' unable to access path: ' +
145 directory,
146 severity: 'warning'
31 office 147 });
148 self.emit('data', {
30 office 149 status: 403,
150 data: new stream.Readable({
151 read(size) {
152 this.push(null);
153 }
154 }),
155 type: 'text/plain'
25 office 156 });
30 office 157 return;
158 }
31 office 159 self.emit('log', {
30 office 160 message: 'Client: ' +
161 client.address + ':' +
162 client.port +
163 ' sent file: ' +
164 root,
165 severity: 'info'
31 office 166 });
167 self.emit('data', {
30 office 168 status: 200,
169 data: fs.createReadStream(root),
170 type: mime.lookup(root)
29 office 171 });
25 office 172 });
173 });
14 office 174 }
175  
17 office 176 // Determines whether the requested filesystem request path is a directory or a file.
31 office 177 function serve(self, config, local, href, address) {
29 office 178 fs.stat(local, (error, stats) => {
25 office 179 // Document does not exist.
180 if (error) {
31 office 181 self.emit('log', {
29 office 182 message: 'Client: ' +
183 address.address + ':' +
184 address.port +
185 ' accessing non-existent document: ' +
186 local,
30 office 187 severity: 'warning'
31 office 188 });
189 self.emit('data', {
30 office 190 status: 404,
191 data: new stream.Readable({
192 read(size) {
193 this.push(null);
194 }
195 }),
196 type: 'text/plain'
29 office 197 });
25 office 198 return;
199 }
14 office 200  
25 office 201 if (stats.isDirectory()) {
202 // Directory is requested so provide directory indexes.
31 office 203 index(self, config, local, href, address);
25 office 204 return;
205 }
206 if (stats.isFile()) {
29 office 207 const file = path.parse(local).base;
23 office 208  
25 office 209 // If the file matches the reject list or is not in the accept list,
210 // then there is no file to serve.
211 if (config.site.reject.some((expression) => expression.test(file)) ||
212 !config.site.accept.some((expression) => expression.test(file))) {
31 office 213 self.emit('log', {
30 office 214 message: 'Client: ' +
215 address.address + ':' +
216 address.port +
217 ' requested disallowed file: ' +
218 file,
219 severity: 'warning'
31 office 220 });
221 self.emit('data', {
30 office 222 status: 404,
223 data: new stream.Readable({
224 read(size) {
225 this.push(null);
226 }
227 }),
228 type: 'text/plain'
27 office 229 });
25 office 230 return;
231 }
20 office 232  
25 office 233 // A file was requested so provide the file.
31 office 234 files(self, config, local, address);
25 office 235 }
236 });
14 office 237 }
238  
31 office 239 // Constructor.
240 function Handler() {
241 // Create events emitters for logging and data.
242 EventEmitter.call(this);
243 };
30 office 244  
31 office 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.
32 office 251 const address = request.socket.address();
31 office 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
30 office 269 .replace(
270 config.site.rewrite[key], key
31 office 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 });
8 office 284  
31 office 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);
8 office 293  
31 office 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: ' +
32 office 299 address.address + ':' +
300 address.port +
31 office 301 ' requesting: ' +
302 requestURL.pathname,
303 severity: 'warning'
304 });
32 office 305 self.emit('data', {
31 office 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 }
32 office 316  
31 office 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: ' +
32 office 323 address.address + ':' +
324 address.port +
30 office 325 ' requesting: ' +
326 requestURL.pathname,
327 severity: 'warning'
31 office 328 });
329 self.emit('done', {
30 office 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 }
31 office 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: ' +
32 office 356 address.address + ':' +
357 address.port +
31 office 358 ' accessing: ' +
30 office 359 requestURL.pathname,
31 office 360 severity: 'info'
29 office 361 });
31 office 362 process.nextTick(() =>
363 serve(self,
364 config,
30 office 365 requestPath,
366 requestURL.pathname,
32 office 367 address
31 office 368 )
369 );
370 });
371 return;
372 }
30 office 373  
31 office 374 // If no authentication is required then serve the request.
375 self.emit('log', {
376 message: 'Client: ' +
32 office 377 address.address + ':' +
378 address.port +
31 office 379 ' accessing: ' +
380 requestURL.pathname,
381 severity: 'info'
382 });
383 process.nextTick(() =>
384 serve(self,
385 config,
30 office 386 requestPath,
387 requestURL.pathname,
32 office 388 address
31 office 389 )
390 );
25 office 391 });
31 office 392 });
393  
394 return this;
8 office 395 };
31 office 396  
397 util.inherits(Handler, EventEmitter);
398 module.exports = Handler;