node-http-server – Blame information for rev 14

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