node-http-server – Diff between revs 29 and 30
?pathlinks?
Rev 29 | Rev 30 | |||
---|---|---|---|---|
Line 7... | Line 7... | |||
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 | const stream = require('stream'); |
|
Line 13... | Line 13... | |||
13 | |
13 | |
|
14 | // Checks whether userPath is a child of rootPath. |
14 | // Checks whether userPath is a child of rootPath. |
|
15 | function isRooted(userPath, rootPath, separator, callback) { |
15 | function isRooted(userPath, rootPath, separator, callback) { |
|
16 | userPath = userPath.split(separator).filter(Boolean); |
16 | userPath = userPath.split(separator).filter(Boolean); |
|
Line 22... | Line 22... | |||
22 | // Serves files. |
22 | // Serves files. |
|
23 | function files(config, file, client, callback) { |
23 | function files(config, file, client, callback) { |
|
24 | // Check if the file is accessible. |
24 | // Check if the file is accessible. |
|
25 | fs.access(file, fs.constants.R_OK, (error) => { |
25 | fs.access(file, fs.constants.R_OK, (error) => { |
|
26 | if (error) { |
26 | if (error) { |
|
27 | process.nextTick(() => { |
- | ||
28 | callback({ |
- | ||
29 | message: 'Client: ' + |
- | ||
30 | client.address + ':' + |
- | ||
31 | client.port + |
- | ||
32 | ' requesting inaccessible path: ' + |
- | ||
33 | file, |
- | ||
34 | severity: 'warning', |
- | ||
35 | status: 403 |
- | ||
36 | |
- | ||
37 | }); |
- | ||
38 | }); |
- | ||
39 | return; |
- | ||
40 | } |
- | ||
41 | process.nextTick(() => { |
- | ||
42 | callback({ |
27 | callback({ |
|
43 | message: 'Client: ' + |
28 | message: 'Client: ' + |
|
44 | client.address + ':' + |
29 | client.address + ':' + |
|
45 | client.port + |
30 | client.port + |
|
46 | ' sent file: ' + |
31 | ' requesting inaccessible path: ' + |
|
47 | file, |
32 | file, |
|
48 | severity: 'info', |
33 | severity: 'warning' |
|
- | 34 | }, { |
||
49 | status: 200, |
35 | status: 403, |
|
- | 36 | data: new stream.Readable({ |
||
50 | data: fs |
37 | read(size) { |
|
51 | .createReadStream(file), |
38 | this.push(null); |
|
- | 39 | } |
||
52 | type: mime |
40 | }), |
|
53 | .lookup(file) |
41 | type: 'text/plain' |
|
54 | }); |
42 | }); |
|
- | 43 | return; |
||
- | 44 | } |
||
- | 45 | callback({ |
||
- | 46 | message: 'Client: ' + |
||
- | 47 | client.address + ':' + |
||
- | 48 | client.port + |
||
- | 49 | ' sent file: ' + |
||
- | 50 | file, |
||
- | 51 | severity: 'info' |
||
- | 52 | }, { |
||
- | 53 | status: 200, |
||
- | 54 | data: fs |
||
- | 55 | .createReadStream(file), |
||
- | 56 | type: mime |
||
- | 57 | .lookup(file) |
||
55 | }); |
58 | }); |
|
56 | |
- | ||
57 | }); |
59 | }); |
|
58 | } |
60 | } |
|
Line 59... | Line 61... | |||
59 | |
61 | |
|
60 | // Serves a directory listing or the document index in case it exists. |
62 | // Serves a directory listing or the document index in case it exists. |
|
Line 65... | Line 67... | |||
65 | if (config.site.indexing |
67 | if (config.site.indexing |
|
66 | .some((directory) => |
68 | .some((directory) => |
|
67 | directory.toUpperCase() === href.toUpperCase())) { |
69 | directory.toUpperCase() === href.toUpperCase())) { |
|
68 | fs.readdir(directory, (error, paths) => { |
70 | fs.readdir(directory, (error, paths) => { |
|
69 | if (error) { |
71 | if (error) { |
|
70 | process.nextTick(() => { |
- | ||
71 | callback({ |
- | ||
72 | message: 'Client: ' + |
- | ||
73 | client.address + ':' + |
- | ||
74 | client.port + |
- | ||
75 | ' could not access directory: ' + |
72 | console.log("listing forbidden..."); |
|
76 | directory, |
- | ||
77 | severity: 'warning', |
- | ||
78 | status: 500 |
- | ||
79 | }); |
- | ||
80 | }); |
- | ||
81 | return; |
- | ||
82 | } |
- | ||
83 | process.nextTick(() => { |
- | ||
84 | callback({ |
73 | callback({ |
|
85 | message: 'Client: ' + |
74 | message: 'Client: ' + |
|
86 | client.address + ':' + |
75 | client.address + ':' + |
|
87 | client.port + |
76 | client.port + |
|
88 | ' accessed directory listing: ' + |
77 | ' could not access directory: ' + |
|
89 | directory, |
78 | directory, |
|
90 | severity: 'warning', |
79 | severity: 'warning' |
|
- | 80 | }, { |
||
91 | status: 200, |
81 | status: 500, |
|
92 | data: JSONStream.parse(paths) |
82 | data: new stream.Readable({ |
|
- | 83 | read(size) { |
||
- | 84 | this.push(null); |
||
- | 85 | } |
||
- | 86 | }), |
||
- | 87 | type: 'text/plain' |
||
93 | }); |
88 | }); |
|
- | 89 | return; |
||
- | 90 | } |
||
- | 91 | console.log("sending listing..."); |
||
- | 92 | callback({ |
||
- | 93 | message: 'Client: ' + |
||
- | 94 | client.address + ':' + |
||
- | 95 | client.port + |
||
- | 96 | ' accessed directory listing: ' + |
||
- | 97 | directory, |
||
- | 98 | severity: 'info' |
||
- | 99 | }, { |
||
- | 100 | status: 200, |
||
- | 101 | data: new stream.Readable({ |
||
- | 102 | read(size) { |
||
- | 103 | this.push(JSON.stringify(paths)); |
||
- | 104 | this.push(null); |
||
- | 105 | } |
||
- | 106 | }), |
||
- | 107 | type: 'application/json' |
||
94 | }); |
108 | }); |
|
95 | }); |
109 | }); |
|
96 | return; |
110 | return; |
|
97 | } |
111 | } |
|
98 | // Could not access directory index file and directory listing not allowed. |
112 | // Could not access directory index file and directory listing not allowed. |
|
99 | process.nextTick(() => { |
113 | console.log("no dirindex..."); |
|
100 | callback({ |
114 | callback({ |
|
101 | message: 'Client: ' + |
115 | message: 'Client: ' + |
|
102 | client.address + ':' + |
116 | client.address + ':' + |
|
103 | client.port + |
117 | client.port + |
|
104 | ' no index file found and accessing forbiden index: ' + |
118 | ' no index file found and accessing forbiden index: ' + |
|
105 | href, |
119 | href, |
|
106 | severity: 'warning', |
120 | severity: 'warning' |
|
- | 121 | }, { |
||
107 | status: 400 |
122 | status: 403, |
|
- | 123 | data: new stream.Readable({ |
||
- | 124 | read(size) { |
||
- | 125 | this.push(null); |
||
- | 126 | } |
||
108 | }); |
127 | }), |
|
- | 128 | type: 'text/plain' |
||
109 | }); |
129 | }); |
|
110 | return; |
130 | return; |
|
111 | |
- | ||
112 | } |
131 | } |
|
Line 113... | Line 132... | |||
113 | |
132 | |
|
114 | // Serve the document index. |
133 | // Serve the document index. |
|
115 | fs.access(root, fs.constants.R_OK, (error) => { |
134 | fs.access(root, fs.constants.R_OK, (error) => { |
|
116 | if (error) { |
- | ||
117 | process.nextTick(() => { |
- | ||
118 | callback({ |
- | ||
119 | message: 'Client: ' + |
- | ||
120 | client.address + ':' + |
- | ||
121 | client.port + |
- | ||
122 | ' unable to access path: ' + |
- | ||
123 | directory, |
- | ||
124 | severity: 'warning', |
- | ||
125 | status: 403 |
- | ||
126 | }); |
- | ||
127 | }); |
- | ||
128 | return; |
- | ||
129 | } |
- | ||
130 | process.nextTick(() => { |
135 | if (error) { |
|
131 | callback({ |
136 | callback({ |
|
132 | message: 'Client: ' + |
137 | message: 'Client: ' + |
|
133 | client.address + ':' + |
138 | client.address + ':' + |
|
134 | client.port + |
139 | client.port + |
|
135 | ' sent file: ' + |
140 | ' unable to access path: ' + |
|
136 | root, |
141 | directory, |
|
- | 142 | severity: 'warning' |
||
137 | severity: 'info', |
143 | }, { |
|
138 | status: 200, |
144 | status: 403, |
|
- | 145 | data: new stream.Readable({ |
||
- | 146 | read(size) { |
||
- | 147 | this.push(null); |
||
- | 148 | } |
||
139 | data: fs.createReadStream(root), |
149 | }), |
|
140 | type: mime.lookup(root) |
150 | type: 'text/plain' |
|
- | 151 | }); |
||
- | 152 | return; |
||
- | 153 | } |
||
- | 154 | callback({ |
||
- | 155 | message: 'Client: ' + |
||
- | 156 | client.address + ':' + |
||
- | 157 | client.port + |
||
- | 158 | ' sent file: ' + |
||
- | 159 | root, |
||
- | 160 | severity: 'info' |
||
- | 161 | }, { |
||
- | 162 | status: 200, |
||
- | 163 | data: fs.createReadStream(root), |
||
141 | }); |
164 | type: mime.lookup(root) |
|
142 | }); |
165 | }); |
|
143 | }); |
166 | }); |
|
144 | }); |
167 | }); |
|
Line 153... | Line 176... | |||
153 | message: 'Client: ' + |
176 | message: 'Client: ' + |
|
154 | address.address + ':' + |
177 | address.address + ':' + |
|
155 | address.port + |
178 | address.port + |
|
156 | ' accessing non-existent document: ' + |
179 | ' accessing non-existent document: ' + |
|
157 | local, |
180 | local, |
|
158 | severity: 'warning', |
181 | severity: 'warning' |
|
- | 182 | }, { |
||
159 | status: 404 |
183 | status: 404, |
|
- | 184 | data: new stream.Readable({ |
||
- | 185 | read(size) { |
||
- | 186 | this.push(null); |
||
- | 187 | } |
||
- | 188 | }), |
||
- | 189 | type: 'text/plain' |
||
160 | }); |
190 | }); |
|
161 | return; |
191 | return; |
|
162 | } |
192 | } |
|
Line 163... | Line 193... | |||
163 | |
193 | |
|
Line 171... | Line 201... | |||
171 | |
201 | |
|
172 | // If the file matches the reject list or is not in the accept list, |
202 | // If the file matches the reject list or is not in the accept list, |
|
173 | // then there is no file to serve. |
203 | // then there is no file to serve. |
|
174 | if (config.site.reject.some((expression) => expression.test(file)) || |
204 | if (config.site.reject.some((expression) => expression.test(file)) || |
|
175 | !config.site.accept.some((expression) => expression.test(file))) { |
- | ||
176 | process.nextTick(() => { |
205 | !config.site.accept.some((expression) => expression.test(file))) { |
|
177 | callback({ |
206 | callback({ |
|
178 | message: 'Client: ' + |
207 | message: 'Client: ' + |
|
179 | address.address + ':' + |
208 | address.address + ':' + |
|
180 | address.port + |
209 | address.port + |
|
181 | ' requested disallowed file: ' + |
210 | ' requested disallowed file: ' + |
|
182 | file, |
211 | file, |
|
- | 212 | severity: 'warning' |
||
183 | severity: 'warning', |
213 | }, { |
|
- | 214 | status: 404, |
||
- | 215 | data: new stream.Readable({ |
||
- | 216 | read(size) { |
||
- | 217 | this.push(null); |
||
184 | status: 404 |
218 | } |
|
- | 219 | }), |
||
185 | }); |
220 | type: 'text/plain' |
|
186 | }); |
221 | }); |
|
187 | return; |
222 | return; |
|
Line 188... | Line 223... | |||
188 | } |
223 | } |
|
Line 193... | Line 228... | |||
193 | }); |
228 | }); |
|
194 | } |
229 | } |
|
Line 195... | Line 230... | |||
195 | |
230 | |
|
196 | module.exports = { |
231 | module.exports = { |
|
197 | process: (config, request, response, root, callback) => { |
- | ||
198 | process.nextTick(() => { |
232 | process: (config, request, response, root, callback) => { |
|
199 | const requestAddress = request.socket.address(); |
233 | const requestAddress = request.socket.address(); |
|
200 | const requestURL = url.parse( |
234 | const requestURL = url.parse( |
|
201 | request.url, true |
235 | request.url, true |
|
202 | ); |
236 | ); |
|
203 | |
237 | |
|
204 | // Perform URL re-writes. |
238 | // Perform URL re-writes. |
|
205 | Object.keys(config.site.rewrite).forEach((key, index) => { |
239 | Object.keys(config.site.rewrite).forEach((key, index) => { |
|
206 | if(config.site.rewrite[key].test(requestURL.path)) { |
240 | if (config.site.rewrite[key].test(requestURL.path)) { |
|
207 | const originalPath = requestURL.path; |
241 | const originalPath = requestURL.path; |
|
208 | requestURL.path = requestURL |
242 | requestURL.path = requestURL |
|
209 | .path |
243 | .path |
|
210 | .replace( |
244 | .replace( |
|
211 | config.site.rewrite[key], key |
245 | config.site.rewrite[key], key |
|
212 | ); |
246 | ); |
|
213 | requestURL.pathname = url.parse( |
247 | requestURL.pathname = url.parse( |
|
214 | requestURL |
248 | requestURL |
|
215 | .pathname |
249 | .pathname |
|
216 | .replace( |
250 | .replace( |
|
217 | config.site.rewrite[key], key |
251 | config.site.rewrite[key], key |
|
218 | ), |
252 | ), |
|
219 | true |
253 | true |
|
220 | ) |
254 | ) |
|
221 | .pathname; |
255 | .pathname; |
|
222 | callback({ |
256 | callback({ |
|
223 | message: 'Rewrite path: ' + |
257 | message: 'Rewrite path: ' + |
|
224 | originalPath + |
258 | originalPath + |
|
225 | ' to: ' + |
259 | ' to: ' + |
|
226 | requestURL.path, |
260 | requestURL.path, |
|
227 | severity: 'info' |
261 | severity: 'info' |
|
228 | }); |
262 | }); |
|
229 | } |
263 | } |
|
Line 230... | Line 264... | |||
230 | }); |
264 | }); |
|
231 | |
265 | |
|
232 | const trimmedPath = requestURL |
266 | const trimmedPath = requestURL |
|
233 | .pathname |
267 | .pathname |
|
234 | .split('/') |
268 | .split('/') |
|
235 | .filter(Boolean) |
269 | .filter(Boolean) |
|
236 | .join('/'); |
270 | .join('/'); |
|
237 | const requestPath = trimmedPath === '/' ? |
271 | const requestPath = trimmedPath === '/' ? |
|
238 | path.join(root, trimmedPath) : |
272 | path.join(root, trimmedPath) : |
|
239 | path.resolve(root, trimmedPath); |
273 | path.resolve(root, trimmedPath); |
|
240 | |
274 | |
|
241 | fs.realpath(requestPath, (error, resolvedPath) => { |
275 | fs.realpath(requestPath, (error, resolvedPath) => { |
|
- | 276 | // If the path does not exist, then return early. |
||
- | 277 | if (error) { |
||
- | 278 | callback({ |
||
- | 279 | message: 'Unknown path requested: ' + |
||
242 | // If the path does not exist, then return early. |
280 | requestAddress.address + ':' + |
|
- | 281 | requestAddress.port + |
||
- | 282 | ' requesting: ' + |
||
- | 283 | requestURL.pathname, |
||
- | 284 | severity: 'warning' |
||
- | 285 | }, { |
||
- | 286 | status: 404, |
||
- | 287 | data: new stream.Readable({ |
||
- | 288 | read(size) { |
||
- | 289 | this.push(null); |
||
- | 290 | } |
||
- | 291 | }), |
||
- | 292 | type: 'text/plain' |
||
- | 293 | }); |
||
- | 294 | return; |
||
- | 295 | } |
||
- | 296 | // Check for path traversals early on and bail if the requested path does not |
||
- | 297 | // lie within the specified document root. |
||
243 | if (error) { |
298 | isRooted(resolvedPath, root, path.sep, (rooted) => { |
|
244 | process.nextTick(() => { |
299 | if (!rooted) { |
|
245 | callback({ |
300 | callback({ |
|
246 | message: 'Unknown path requested: ' + |
301 | message: 'Attempted path traversal: ' + |
|
247 | requestAddress.address + ':' + |
302 | requestAddress.address + ':' + |
|
248 | requestAddress.port + |
303 | requestAddress.port + |
|
249 | ' requesting: ' + |
304 | ' requesting: ' + |
|
- | 305 | requestURL.pathname, |
||
250 | requestURL.pathname, |
306 | severity: 'warning' |
|
- | 307 | }, { |
||
- | 308 | status: 404, |
||
- | 309 | data: new stream.Readable({ |
||
- | 310 | read(size) { |
||
251 | severity: 'warning', |
311 | this.push(null); |
|
- | 312 | } |
||
252 | status: 404 |
313 | }), |
|
253 | }); |
314 | type: 'text/plain' |
|
254 | }); |
315 | }); |
|
255 | return; |
- | ||
256 | } |
- | ||
257 | // Check for path traversals early on and bail if the requested path does not |
- | ||
258 | // lie within the specified document root. |
- | ||
259 | isRooted(resolvedPath, root, path.sep, (rooted) => { |
- | ||
260 | if (!rooted) { |
- | ||
261 | process.nextTick(() => { |
- | ||
262 | callback({ |
- | ||
263 | message: 'Attempted path traversal: ' + |
- | ||
264 | requestAddress.address + ':' + |
- | ||
265 | requestAddress.port + |
- | ||
266 | ' requesting: ' + |
- | ||
267 | requestURL.pathname, |
- | ||
268 | severity: 'warning', |
- | ||
269 | status: 404 |
- | ||
270 | }); |
- | ||
271 | }); |
- | ||
272 | return; |
- | ||
273 | } |
- | ||
274 | |
- | ||
275 | // If authentication is required for this path then perform authentication. |
- | ||
276 | if (config.auth.locations.some( |
- | ||
277 | (authPath) => authPath.toUpperCase() === requestURL.pathname.toUpperCase())) { |
- | ||
278 | // Create digest authentication. |
- | ||
279 | const authentication = auth.digest({ |
- | ||
280 | realm: config.auth.realm, |
- | ||
281 | file: path.resolve( |
- | ||
282 | path.dirname(require.main.filename), |
- | ||
283 | config.auth.digest |
- | ||
284 | ) |
- | ||
285 | }); |
- | ||
286 | // Requested location requires authentication. |
- | ||
287 | authentication.check(request, response, (request, response) => { |
- | ||
288 | process.nextTick(() => { |
- | ||
289 | callback({ |
- | ||
290 | message: 'Authenticated client: ' + |
- | ||
291 | requestAddress.address + ':' + |
- | ||
292 | requestAddress.port + |
- | ||
293 | ' accessing: ' + |
- | ||
294 | requestURL.pathname, |
- | ||
295 | severity: 'info' |
- | ||
296 | }); |
- | ||
297 | }); |
- | ||
298 | serve(config, |
- | ||
299 | requestPath, |
- | ||
300 | requestURL.pathname, |
- | ||
301 | requestAddress, |
- | ||
302 | callback |
- | ||
303 | ); |
- | ||
304 | }); |
- | ||
Line 305... | Line 316... | |||
305 | return; |
316 | return; |
|
- | 317 | } |
||
- | 318 | |
||
- | 319 | // If authentication is required for this path then perform authentication. |
||
- | 320 | if (config.auth.locations.some( |
||
306 | } |
321 | (authPath) => authPath.toUpperCase() === requestURL.pathname.toUpperCase())) { |
|
- | 322 | // Create digest authentication. |
||
- | 323 | const authentication = auth.digest({ |
||
- | 324 | realm: config.auth.realm, |
||
- | 325 | file: path.resolve( |
||
- | 326 | path.dirname(require.main.filename), |
||
- | 327 | config.auth.digest |
||
- | 328 | ) |
||
307 | |
329 | }); |
|
308 | // If no authentication is required then serve the request. |
330 | // Requested location requires authentication. |
|
309 | process.nextTick(() => { |
331 | authentication.check(request, response, (request, response) => { |
|
310 | callback({ |
332 | callback({ |
|
311 | message: 'Client: ' + |
333 | message: 'Authenticated client: ' + |
|
312 | requestAddress.address + ':' + |
334 | requestAddress.address + ':' + |
|
313 | requestAddress.port + |
335 | requestAddress.port + |
|
314 | ' accessing: ' + |
336 | ' accessing: ' + |
|
- | 337 | requestURL.pathname, |
||
- | 338 | severity: 'info' |
||
- | 339 | }); |
||
- | 340 | serve(config, |
||
- | 341 | requestPath, |
||
- | 342 | requestURL.pathname, |
||
315 | requestURL.pathname, |
343 | requestAddress, |
|
316 | severity: 'info' |
344 | callback |
|
- | 345 | ); |
||
- | 346 | }); |
||
- | 347 | return; |
||
- | 348 | } |
||
- | 349 | |
||
- | 350 | // If no authentication is required then serve the request. |
||
317 | }); |
351 | callback({ |
|
- | 352 | message: 'Client: ' + |
||
318 | }); |
353 | requestAddress.address + ':' + |
|
319 | serve(config, |
354 | requestAddress.port + |
|
320 | requestPath, |
- | ||
321 | requestURL.pathname, |
- | ||
322 | requestAddress, |
355 | ' accessing: ' + |
|
- | 356 | requestURL.pathname, |
||
- | 357 | severity: 'info' |
||
- | 358 | }); |
||
- | 359 | serve(config, |
||
- | 360 | requestPath, |
||
- | 361 | requestURL.pathname, |
||
323 | callback |
362 | requestAddress, |
|
324 | ); |
363 | callback |
|
325 | }); |
364 | ); |
|
326 | }); |
365 | }); |