node-http-server – Blame information for rev 26

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