node-http-server – Diff between revs 27 and 29

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