node-http-server – Blame information for rev 17

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) {
56 if(config.site.indexing
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 }
84  
15 office 85 }
86  
87 // Serve the document index.
17 office 88 fs.access(requestPath, fs.constants.R_OK, (error) => {
14 office 89 if (error) {
15 office 90 process.nextTick(() => {
91 callback('The server was unable to access the filesystem path: ' +
17 office 92 requestPath,
15 office 93 module.exports.error.level.WARN
94 );
95 });
14 office 96 response.statusCode = 403;
97 response.end();
98 return;
99 }
100  
15 office 101 // Set MIME content type.
14 office 102 response.setHeader(
103 'Content-Type',
15 office 104 mime.lookup(root)
14 office 105 );
106  
15 office 107 var readStream = fs.createReadStream(root)
14 office 108 .on('open', () => {
109 response.statusCode = 200;
110 readStream.pipe(response);
111 })
112 .on('error', () => {
113 response.statusCode = 500;
114 response.end();
115 });
116  
117 });
118 });
119 }
120  
17 office 121 // Determines whether the requested filesystem request path is a directory or a file.
122 function serve(config, request, response, requestPath, requestURL, callback) {
123 fs.stat(requestPath, (error, stats) => {
15 office 124 // Document does not exist.
125 if (error) {
126 response.statusCode = 404;
127 response.end();
128 return;
129 }
14 office 130  
15 office 131 switch (stats.isDirectory()) {
132 case true: // Directory is requested so provide directory indexes.
17 office 133 index(config, request, response, requestPath, requestURL, callback);
15 office 134 break;
135 default: // Browser requesting file.
17 office 136 files(config, request, response, requestPath, callback);
15 office 137 break;
138 }
14 office 139 });
140 }
141  
8 office 142 module.exports = {
143 error: {
144 level: {
145 INFO: 1,
146 WARN: 2,
147 ERROR: 3
148 }
149 },
11 office 150 process: (config, request, response, root, callback) => {
8 office 151 process.nextTick(() => {
14 office 152 // Create digest authentication.
153 const authentication = auth.digest({
154 realm: config.auth.realm,
155 file: path.resolve(
156 path.dirname(require.main.filename),
157 config.auth.digest
158 )
159 });
160  
8 office 161 const requestAddress = request.socket.address();
17 office 162 const requestURL = url.parse(
14 office 163 request.url, true
164 );
8 office 165  
17 office 166 const trimmedPath = requestURL
8 office 167 .pathname
168 .split('/')
169 .filter(Boolean)
170 .join('/');
17 office 171 const requestPath = trimmedPath === '/' ?
8 office 172 path.join(root, trimmedPath) :
173 path.resolve(root, trimmedPath);
174  
17 office 175 isRooted(requestPath, root, path.sep, (rooted) => {
15 office 176 if (!rooted) {
177 process.nextTick(() => {
178 callback('Attempted path traversal: ' +
179 requestAddress.address + ':' +
180 requestAddress.port +
181 ' requesting: ' +
17 office 182 requestURL.pathname,
15 office 183 module.exports.error.level.WARN
184 );
185 });
186 response.statusCode = 404;
187 response.end();
188 return;
189 }
8 office 190  
15 office 191 // Check if the requested path requires authentication.
192 switch (config.auth.locations.some(
17 office 193 (authPath) => authPath.toUpperCase() === requestURL.pathname.toUpperCase())) {
15 office 194 case true:
195 // Requested location requires authentication.
196 authentication.check(request, response, (request, response) => {
197 process.nextTick(() => {
198 callback('Authenticated client: ' +
199 requestAddress.address + ':' +
200 requestAddress.port +
201 ' accessing: ' +
17 office 202 requestURL.pathname,
15 office 203 module.exports.error.level.INFO
204 );
205 });
17 office 206 serve(config,
207 request,
208 response,
209 requestPath,
210 requestURL.pathname,
211 callback
212 );
15 office 213 });
214 break;
215 default:
14 office 216 process.nextTick(() => {
15 office 217 callback('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 );
15 office 232 break;
233 }
234 });
8 office 235 });
236 }
237 };