node-http-server – Diff between revs 27 and 29
?pathlinks?
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 |
|
332 |
|
|
339 | |
333 | |
|
340 | |
334 | |
|
341 | |
335 | |