node-http-server – Blame information for rev 27

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