node-http-server – Blame information for rev 24

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