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