node-http-server – Blame information for rev 30
?pathlinks?
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"); |
30 | office | 12 | const stream = require('stream'); |
8 | office | 13 | |
14 | office | 14 | // Checks whether userPath is a child of rootPath. |
15 | office | 15 | function isRooted(userPath, rootPath, separator, callback) { |
25 | office | 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])); |
||
8 | office | 20 | } |
21 | |||
14 | office | 22 | // Serves files. |
29 | office | 23 | function files(config, file, client, callback) { |
25 | office | 24 | // Check if the file is accessible. |
29 | office | 25 | fs.access(file, fs.constants.R_OK, (error) => { |
25 | office | 26 | if (error) { |
29 | office | 27 | callback({ |
28 | message: 'Client: ' + |
||
29 | client.address + ':' + |
||
30 | client.port + |
||
30 | office | 31 | ' requesting inaccessible path: ' + |
29 | office | 32 | file, |
30 | office | 33 | severity: 'warning' |
34 | }, { |
||
35 | status: 403, |
||
36 | data: new stream.Readable({ |
||
37 | read(size) { |
||
38 | this.push(null); |
||
39 | } |
||
40 | }), |
||
41 | type: 'text/plain' |
||
25 | office | 42 | }); |
30 | office | 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) |
||
29 | office | 58 | }); |
25 | office | 59 | }); |
15 | office | 60 | } |
61 | |||
62 | // Serves a directory listing or the document index in case it exists. |
||
29 | office | 63 | function index(config, directory, href, client, callback) { |
64 | const root = path.resolve(directory, config.site.index); |
||
25 | office | 65 | fs.stat(root, (error, stats) => { |
66 | if (error) { |
||
67 | if (config.site.indexing |
||
68 | .some((directory) => |
||
29 | office | 69 | directory.toUpperCase() === href.toUpperCase())) { |
70 | fs.readdir(directory, (error, paths) => { |
||
25 | office | 71 | if (error) { |
30 | office | 72 | console.log("listing forbidden..."); |
29 | office | 73 | callback({ |
74 | message: 'Client: ' + |
||
75 | client.address + ':' + |
||
76 | client.port + |
||
30 | office | 77 | ' could not access directory: ' + |
29 | office | 78 | directory, |
30 | office | 79 | severity: 'warning' |
80 | }, { |
||
81 | status: 500, |
||
82 | data: new stream.Readable({ |
||
83 | read(size) { |
||
84 | this.push(null); |
||
85 | } |
||
86 | }), |
||
87 | type: 'text/plain' |
||
29 | office | 88 | }); |
30 | office | 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' |
||
25 | office | 108 | }); |
109 | }); |
||
110 | return; |
||
111 | } |
||
29 | office | 112 | // Could not access directory index file and directory listing not allowed. |
30 | office | 113 | console.log("no dirindex..."); |
114 | callback({ |
||
115 | message: 'Client: ' + |
||
116 | client.address + ':' + |
||
117 | client.port + |
||
118 | ' no index file found and accessing forbiden index: ' + |
||
119 | href, |
||
120 | severity: 'warning' |
||
121 | }, { |
||
122 | status: 403, |
||
123 | data: new stream.Readable({ |
||
124 | read(size) { |
||
125 | this.push(null); |
||
126 | } |
||
127 | }), |
||
128 | type: 'text/plain' |
||
27 | office | 129 | }); |
25 | office | 130 | return; |
131 | } |
||
15 | office | 132 | |
25 | office | 133 | // Serve the document index. |
134 | fs.access(root, fs.constants.R_OK, (error) => { |
||
135 | if (error) { |
||
29 | office | 136 | callback({ |
137 | message: 'Client: ' + |
||
138 | client.address + ':' + |
||
139 | client.port + |
||
30 | office | 140 | ' unable to access path: ' + |
141 | directory, |
||
142 | severity: 'warning' |
||
143 | }, { |
||
144 | status: 403, |
||
145 | data: new stream.Readable({ |
||
146 | read(size) { |
||
147 | this.push(null); |
||
148 | } |
||
149 | }), |
||
150 | type: 'text/plain' |
||
25 | office | 151 | }); |
30 | office | 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), |
||
164 | type: mime.lookup(root) |
||
29 | office | 165 | }); |
25 | office | 166 | }); |
167 | }); |
||
14 | office | 168 | } |
169 | |||
17 | office | 170 | // Determines whether the requested filesystem request path is a directory or a file. |
29 | office | 171 | function serve(config, local, href, address, callback) { |
172 | fs.stat(local, (error, stats) => { |
||
25 | office | 173 | // Document does not exist. |
174 | if (error) { |
||
29 | office | 175 | callback({ |
176 | message: 'Client: ' + |
||
177 | address.address + ':' + |
||
178 | address.port + |
||
179 | ' accessing non-existent document: ' + |
||
180 | local, |
||
30 | office | 181 | severity: 'warning' |
182 | }, { |
||
183 | status: 404, |
||
184 | data: new stream.Readable({ |
||
185 | read(size) { |
||
186 | this.push(null); |
||
187 | } |
||
188 | }), |
||
189 | type: 'text/plain' |
||
29 | office | 190 | }); |
25 | office | 191 | return; |
192 | } |
||
14 | office | 193 | |
25 | office | 194 | if (stats.isDirectory()) { |
195 | // Directory is requested so provide directory indexes. |
||
29 | office | 196 | index(config, local, href, address, callback); |
25 | office | 197 | return; |
198 | } |
||
199 | if (stats.isFile()) { |
||
29 | office | 200 | const file = path.parse(local).base; |
23 | office | 201 | |
25 | office | 202 | // If the file matches the reject list or is not in the accept list, |
203 | // then there is no file to serve. |
||
204 | if (config.site.reject.some((expression) => expression.test(file)) || |
||
205 | !config.site.accept.some((expression) => expression.test(file))) { |
||
30 | office | 206 | callback({ |
207 | message: 'Client: ' + |
||
208 | address.address + ':' + |
||
209 | address.port + |
||
210 | ' requested disallowed file: ' + |
||
211 | file, |
||
212 | severity: 'warning' |
||
213 | }, { |
||
214 | status: 404, |
||
215 | data: new stream.Readable({ |
||
216 | read(size) { |
||
217 | this.push(null); |
||
218 | } |
||
219 | }), |
||
220 | type: 'text/plain' |
||
27 | office | 221 | }); |
25 | office | 222 | return; |
223 | } |
||
20 | office | 224 | |
25 | office | 225 | // A file was requested so provide the file. |
29 | office | 226 | files(config, local, address, callback); |
25 | office | 227 | } |
228 | }); |
||
14 | office | 229 | } |
230 | |||
8 | office | 231 | module.exports = { |
25 | office | 232 | process: (config, request, response, root, callback) => { |
30 | office | 233 | const requestAddress = request.socket.address(); |
234 | const requestURL = url.parse( |
||
235 | request.url, true |
||
236 | ); |
||
237 | |||
238 | // Perform URL re-writes. |
||
239 | Object.keys(config.site.rewrite).forEach((key, index) => { |
||
240 | if (config.site.rewrite[key].test(requestURL.path)) { |
||
241 | const originalPath = requestURL.path; |
||
242 | requestURL.path = requestURL |
||
243 | .path |
||
244 | .replace( |
||
245 | config.site.rewrite[key], key |
||
246 | ); |
||
247 | requestURL.pathname = url.parse( |
||
248 | requestURL |
||
249 | .pathname |
||
27 | office | 250 | .replace( |
251 | config.site.rewrite[key], key |
||
30 | office | 252 | ), |
27 | office | 253 | true |
254 | ) |
||
255 | .pathname; |
||
30 | office | 256 | callback({ |
257 | message: 'Rewrite path: ' + |
||
258 | originalPath + |
||
259 | ' to: ' + |
||
260 | requestURL.path, |
||
261 | severity: 'info' |
||
262 | }); |
||
263 | } |
||
264 | }); |
||
8 | office | 265 | |
30 | office | 266 | const trimmedPath = requestURL |
267 | .pathname |
||
268 | .split('/') |
||
269 | .filter(Boolean) |
||
270 | .join('/'); |
||
271 | const requestPath = trimmedPath === '/' ? |
||
272 | path.join(root, trimmedPath) : |
||
273 | path.resolve(root, trimmedPath); |
||
8 | office | 274 | |
30 | office | 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: ' + |
||
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. |
||
298 | isRooted(resolvedPath, root, path.sep, (rooted) => { |
||
299 | if (!rooted) { |
||
300 | callback({ |
||
301 | message: 'Attempted path traversal: ' + |
||
302 | requestAddress.address + ':' + |
||
303 | requestAddress.port + |
||
304 | ' requesting: ' + |
||
305 | requestURL.pathname, |
||
306 | severity: 'warning' |
||
307 | }, { |
||
308 | status: 404, |
||
309 | data: new stream.Readable({ |
||
310 | read(size) { |
||
311 | this.push(null); |
||
312 | } |
||
313 | }), |
||
314 | type: 'text/plain' |
||
29 | office | 315 | }); |
25 | office | 316 | return; |
317 | } |
||
8 | office | 318 | |
30 | office | 319 | // If authentication is required for this path then perform authentication. |
320 | if (config.auth.locations.some( |
||
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 | ) |
||
329 | }); |
||
330 | // Requested location requires authentication. |
||
331 | authentication.check(request, response, (request, response) => { |
||
29 | office | 332 | callback({ |
30 | office | 333 | message: 'Authenticated client: ' + |
29 | office | 334 | requestAddress.address + ':' + |
335 | requestAddress.port + |
||
336 | ' accessing: ' + |
||
337 | requestURL.pathname, |
||
338 | severity: 'info' |
||
339 | }); |
||
30 | office | 340 | serve(config, |
341 | requestPath, |
||
342 | requestURL.pathname, |
||
343 | requestAddress, |
||
344 | callback |
||
345 | ); |
||
25 | office | 346 | }); |
30 | office | 347 | return; |
348 | } |
||
349 | |||
350 | // If no authentication is required then serve the request. |
||
351 | callback({ |
||
352 | message: 'Client: ' + |
||
353 | requestAddress.address + ':' + |
||
354 | requestAddress.port + |
||
355 | ' accessing: ' + |
||
25 | office | 356 | requestURL.pathname, |
30 | office | 357 | severity: 'info' |
25 | office | 358 | }); |
30 | office | 359 | serve(config, |
360 | requestPath, |
||
361 | requestURL.pathname, |
||
362 | requestAddress, |
||
363 | callback |
||
364 | ); |
||
25 | office | 365 | }); |
366 | }); |
||
367 | } |
||
8 | office | 368 | }; |