node-http-server – Blame information for rev 15

Subversion Repositories:
Rev:
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) {
15 process.nextTick(() => {
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]));
20 });
8 office 21 }
22  
14 office 23 // Serves files.
24 function files(config, request, response, resource, callback) {
25 // Check if the file is accessible.
15 office 26 fs.access(resource, fs.constants.R_OK, (error) => {
27 if (error) {
28 response.statusCode = 403;
29 response.end();
30 return;
31 }
32  
33 response.setHeader(
34 'Content-Type',
35 mime.lookup(resource)
36 );
37  
38 var readStream = fs.createReadStream(resource)
39 .on('open', () => {
40 response.statusCode = 200;
41 readStream.pipe(response);
42 })
43 .on('error', () => {
44 response.statusCode = 500;
45 response.end();
46 });
47  
48 });
49 }
50  
51 // Serves a directory listing or the document index in case it exists.
52 function index(config, request, response, resource, root, callback) {
53 fs.stat(root, (error, stats) => {
54 if (error) {
55 fs.readdir(resource, (error, paths) => {
56 if (error) {
57 process.nextTick(() => {
58 callback('Could not list directory: ' +
59 resource,
60 module.exports.error.level.ERROR
61 );
62 });
63 response.statusCode = 500;
64 response.end();
65 return;
66 }
67 process.nextTick(() => {
68 callback('Directory listing requested for: ' +
69 resource,
70 module.exports.error.level.INFO
71 );
72 });
73 response.statusCode = 200;
74 response.write(JSON.stringify(paths));
75 response.end();
76 });
77  
78 return;
79 }
80  
81 // Serve the document index.
14 office 82 fs.access(resource, fs.constants.R_OK, (error) => {
83 if (error) {
15 office 84 process.nextTick(() => {
85 callback('The server was unable to access the filesystem path: ' +
86 resource,
87 module.exports.error.level.WARN
88 );
89 });
14 office 90 response.statusCode = 403;
91 response.end();
92 return;
93 }
94  
15 office 95 // Set MIME content type.
14 office 96 response.setHeader(
97 'Content-Type',
15 office 98 mime.lookup(root)
14 office 99 );
100  
15 office 101 var readStream = fs.createReadStream(root)
14 office 102 .on('open', () => {
103 response.statusCode = 200;
104 readStream.pipe(response);
105 })
106 .on('error', () => {
107 response.statusCode = 500;
108 response.end();
109 });
110  
111 });
112 });
113 }
114  
115 // Determines whether the requested resource is a directory or a file.
116 function serve(config, request, response, resource, callback) {
15 office 117 fs.stat(resource, (error, stats) => {
118 // Document does not exist.
119 if (error) {
120 response.statusCode = 404;
121 response.end();
122 return;
123 }
14 office 124  
15 office 125 switch (stats.isDirectory()) {
126 case true: // Directory is requested so provide directory indexes.
127 index(config, request, response, resource, path.resolve(resource, config.site.index), callback)
128 break;
129 default: // Browser requesting file.
130 files(config, request, response, resource, callback);
131 break;
132 }
14 office 133 });
134 }
135  
8 office 136 module.exports = {
137 error: {
138 level: {
139 INFO: 1,
140 WARN: 2,
141 ERROR: 3
142 }
143 },
11 office 144 process: (config, request, response, root, callback) => {
8 office 145 process.nextTick(() => {
14 office 146 // Create digest authentication.
147 const authentication = auth.digest({
148 realm: config.auth.realm,
149 file: path.resolve(
150 path.dirname(require.main.filename),
151 config.auth.digest
152 )
153 });
154  
8 office 155 const requestAddress = request.socket.address();
14 office 156 const requestedURL = url.parse(
157 request.url, true
158 );
8 office 159  
160 const trimmedPath = requestedURL
161 .pathname
162 .split('/')
163 .filter(Boolean)
164 .join('/');
14 office 165 const resource = trimmedPath === '/' ?
8 office 166 path.join(root, trimmedPath) :
167 path.resolve(root, trimmedPath);
168  
15 office 169 isRooted(resource, root, path.sep, (rooted) => {
170 if (!rooted) {
171 process.nextTick(() => {
172 callback('Attempted path traversal: ' +
173 requestAddress.address + ':' +
174 requestAddress.port +
175 ' requesting: ' +
176 requestedURL.pathname,
177 module.exports.error.level.WARN
178 );
179 });
180 response.statusCode = 404;
181 response.end();
182 return;
183 }
8 office 184  
15 office 185 // Check if the requested path requires authentication.
186 switch (config.auth.locations.some(
187 (authPath) => authPath.toUpperCase() === requestedURL.pathname.toUpperCase())) {
188 case true:
189 // Requested location requires authentication.
190 authentication.check(request, response, (request, response) => {
191 process.nextTick(() => {
192 callback('Authenticated client: ' +
193 requestAddress.address + ':' +
194 requestAddress.port +
195 ' accessing: ' +
196 requestedURL.pathname,
197 module.exports.error.level.INFO
198 );
199 });
200 serve(config, request, response, resource, callback);
201 });
202 break;
203 default:
14 office 204 process.nextTick(() => {
15 office 205 callback('Client: ' +
14 office 206 requestAddress.address + ':' +
207 requestAddress.port +
208 ' accessing: ' +
209 requestedURL.pathname,
210 module.exports.error.level.INFO
211 );
8 office 212 });
14 office 213 serve(config, request, response, resource, callback);
15 office 214 break;
215 }
216 });
8 office 217 });
218 }
219 };