node-http-server – Blame information for rev 20

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