node-http-server – Blame information for rev 30

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