node-http-server – Blame information for rev 29

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