node-http-server – Blame information for rev 35

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  
35 office 16 // Local imports.
17 const Cache = require(
18 path
19 .resolve(
20 path.dirname(require.main.filename),
21 'src',
22 'cache'
23 )
24 );
25 const was = require(
26 path
27 .resolve(
28 path.dirname(require.main.filename),
29 'src',
30 'was'
31 )
32 );
8 office 33  
14 office 34 // Serves files.
35 office 35 function files(self, config, file, client, cache) {
36 // Check if the file is accessible.
37 fs.access(file, fs.constants.R_OK, (error) => {
38 if (error) {
39 self.emit('log', {
40 message: 'Client: ' +
41 client.address + ':' +
42 client.port +
43 ' requesting inaccessible path: ' +
44 file,
45 severity: 'warning'
46 });
47 self.emit('data', {
48 status: 403,
49 data: new stream.Readable({
50 read(size) {
51 this.push(null);
52 }
53 }),
54 type: 'text/plain'
55 });
56 return;
57 }
58  
59 cache.process(file, fs.createReadStream(file), mime.lookup(file))
60 .on('data', (result) => self.emit('data', result))
61 .on('log', (data) => self.emit('log', data));
62 });
15 office 63 }
64  
65 // Serves a directory listing or the document index in case it exists.
35 office 66 function index(self, config, directory, href, client, cache) {
67 const root = path.resolve(directory, config.site.index);
68 fs.stat(root, (error, stats) => {
69 if (error) {
70 if (config.site.indexing
71 .some((directory) =>
72 directory.toUpperCase() === href.toUpperCase())) {
73 fs.readdir(directory, (error, paths) => {
74 if (error) {
75 self.emit('log', {
76 message: 'Client: ' +
77 client.address + ':' +
78 client.port +
79 ' could not access directory: ' +
80 directory,
81 severity: 'warning'
82 });
83 self.emit('data', {
84 status: 500,
85 data: new stream.Readable({
86 read(size) {
87 this.push(null);
88 }
89 }),
90 type: 'text/plain'
91 });
92 return;
93 }
94 cache.process(directory, new stream.Readable({
95 read(size) {
96 this.push(JSON.stringify(paths));
97 this.push(null);
98 }
99 }), 'application/json')
100 .on('data', (result) => self.emit('data', result))
101 .on('log', (data) => self.emit('log', data));
102 });
103 return;
104 }
105 // Could not access directory index file and directory listing not allowed.
106 self.emit('log', {
107 message: 'Client: ' +
108 client.address + ':' +
109 client.port +
110 ' no index file found and accessing forbiden index: ' +
111 href,
112 severity: 'warning'
113 });
114 self.emit('data', {
115 status: 403,
116 data: new stream.Readable({
117 read(size) {
118 this.push(null);
119 }
120 }),
121 type: 'text/plain'
122 });
123 return;
124 }
15 office 125  
35 office 126 // Serve the document index.
127 fs.access(root, fs.constants.R_OK, (error) => {
128 if (error) {
129 self.emit('log', {
130 message: 'Client: ' +
131 client.address + ':' +
132 client.port +
133 ' unable to access path: ' +
134 directory,
135 severity: 'warning'
136 });
137 self.emit('data', {
138 status: 403,
139 data: new stream.Readable({
140 read(size) {
141 this.push(null);
142 }
143 }),
144 type: 'text/plain'
145 });
146 return;
147 }
148 cache.process(root, fs.createReadStream(root), mime.lookup(root))
149 .on('data', (result) => self.emit('data', result))
150 .on('log', (data) => self.emit('log', data));
151 });
152 });
14 office 153 }
154  
17 office 155 // Determines whether the requested filesystem request path is a directory or a file.
35 office 156 function serve(self, config, local, href, address, cache) {
157 fs.stat(local, (error, stats) => {
158 // Document does not exist.
159 if (error) {
160 self.emit('log', {
161 message: 'Client: ' +
162 address.address + ':' +
163 address.port +
164 ' accessing non-existent document: ' +
165 local,
166 severity: 'warning'
167 });
168 self.emit('data', {
169 status: 404,
170 data: new stream.Readable({
171 read(size) {
172 this.push(null);
173 }
174 }),
175 type: 'text/plain'
176 });
177 return;
178 }
14 office 179  
35 office 180 if (stats.isDirectory()) {
181 // Directory is requested so provide directory indexes.
182 index(self, config, local, href, address, cache);
183 return;
184 }
185 if (stats.isFile()) {
186 const file = path.parse(local).base;
23 office 187  
35 office 188 // If the file matches the reject list or is not in the accept list,
189 // then there is no file to serve.
190 if (config.site.reject.some((expression) => expression.test(file)) ||
191 !config.site.accept.some((expression) => expression.test(file))) {
192 self.emit('log', {
193 message: 'Client: ' +
194 address.address + ':' +
195 address.port +
196 ' requested disallowed file: ' +
197 file,
198 severity: 'warning'
199 });
200 self.emit('data', {
201 status: 404,
202 data: new stream.Readable({
203 read(size) {
204 this.push(null);
205 }
206 }),
207 type: 'text/plain'
208 });
209 return;
210 }
20 office 211  
35 office 212 // A file was requested so provide the file.
213 files(self, config, local, address, cache);
214 }
215 });
14 office 216 }
217  
31 office 218 // Constructor.
219 function Handler() {
35 office 220 // Create events emitters for logging and data.
221 EventEmitter.call(this);
31 office 222 };
30 office 223  
31 office 224 // Process a request.
225 Handler.prototype.process = function(config, request, response, root) {
35 office 226 EventEmitter.call(this);
227 var self = this;
31 office 228  
35 office 229 // Get client details.
230 const address = request.socket.address();
231 // Get requested URL.
232 const requestURL = url.parse(
233 request.url, true
234 );
31 office 235  
35 office 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 });
8 office 263  
35 office 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);
8 office 272  
35 office 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 }
31 office 295  
35 office 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 }
30 office 319  
35 office 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,
368 address,
369 new Cache(config, address, request, response)
370 )
371 );
372 });
373 });
374  
375 return this;
8 office 376 };
31 office 377  
378 util.inherits(Handler, EventEmitter);
35 office 379 util.inherits(Cache, EventEmitter);
31 office 380 module.exports = Handler;