-
Notifications
You must be signed in to change notification settings - Fork 6
/
server.js
393 lines (323 loc) · 13.6 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
"use strict";
/* globals require */
/// jsfs - Javascript filesystem with a REST interface
// *** CONVENTIONS ***
// strings are double-quoted, variables use underscores, constants are ALL CAPS
// *** UTILITIES & MODULES ***
var http = require("http");
var crypto = require("crypto");
var zlib = require("zlib");
var through = require("through");
var config = require("./config.js");
var log = require("./jlog-nomem.js");
var CONSTANTS = require("./lib/constants.js");
var utils = require("./lib/utils.js");
var validate = require("./lib/validate.js");
var operations = require("./lib/" + (config.CONFIGURED_STORAGE || "fs") + "/disk-operations.js");
// base storage object
var Inode = require("./lib/inode.js");
// get this now, rather than at several other points
var TOTAL_LOCATIONS = config.STORAGE_LOCATIONS.length;
// all responses include these headers to support cross-domain requests
var ALLOWED_METHODS = CONSTANTS.ALLOWED_METHODS.join(",");
var ALLOWED_HEADERS = CONSTANTS.ALLOWED_HEADERS.join(",");
var EXPOSED_HEADERS = CONSTANTS.EXPOSED_HEADERS.join(",");
var ACCEPTED_PARAMS = CONSTANTS.ACCEPTED_PARAMS;
// *** CONFIGURATION ***
log.level = config.LOG_LEVEL; // the minimum level of log messages to record: 0 = info, 1 = warn, 2 = error
log.message(log.INFO, "JSFS ready to process requests");
// at the highest level, jsfs is an HTTP server that accepts GET, POST, PUT, DELETE and OPTIONS methods
http.createServer(function(req, res){
// override default 2 minute time-out
res.setTimeout(config.REQUEST_TIMEOUT * 60 * 1000);
log.message(log.DEBUG, "Initial request received");
res.setHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
res.setHeader("Access-Control-Allow-Headers", ALLOWED_HEADERS);
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Expose-Headers", EXPOSED_HEADERS);
// all requests are interrorgated for these values
var target_url = utils.target_from_url(req.headers["host"], req.url);
// check for request parameters, first in the header and then in the querystring
// Moved these to an object to avoid possible issues from "private" being a reserved word
// (for future use) and to avoid jslint errors from unimplemented param handlers.
var params = utils.request_parameters(ACCEPTED_PARAMS, req.url, req.headers);
log.message(log.INFO, "Received " + req.method + " request for URL " + target_url);
// load requested inode
switch(req.method){
case "GET":
utils.load_inode(target_url, function(err, inode){
if (err) {
log.message(log.WARN, "Result: 404");
res.statusCode = 404;
return res.end();
}
var requested_file = inode;
// check authorization
if (inode.private){
if (validate.is_authorized(inode, req.method, params)) {
log.message(log.INFO, "GET request authorized");
} else {
log.message(log.WARN, "GET request unauthorized");
res.statusCode = 401;
return res.end();
}
}
var create_decryptor = function create_decryptor(options){
return options.encrypted ? crypto.createDecipher("aes-256-cbc", options.key) : through();
};
var create_unzipper = function create_unzipper(compressed){
return compressed ? zlib.createGunzip() : through();
};
// return status
res.statusCode = 200;
// return file metadata as HTTP headers
res.setHeader("Content-Type", requested_file.content_type);
res.setHeader("Content-Length", requested_file.file_size);
var total_blocks = requested_file.blocks.length;
var idx = 0;
var search_for_block = function search_for_block(_idx){
var location = config.STORAGE_LOCATIONS[_idx];
var search_path = location.path + requested_file.blocks[idx].block_hash;
_idx++;
operations.exists(search_path + "gz", function(err, result){
if (result) {
log.message(log.INFO, "Found compressed block " + requested_file.blocks[idx].block_hash + ".gz in " + location.path);
requested_file.blocks[idx].last_seen = location.path;
utils.save_inode(requested_file, function(){
return read_file(search_path + ".gz", true);
});
} else {
operations.exists(search_path, function(_err, _result){
if (_result) {
log.message(log.INFO, "Found block " + requested_file.blocks[idx].block_hash + " in " + location.path);
requested_file.blocks[idx].last_seen = location.path;
utils.save_inode(requested_file, function(){
return read_file(search_path, false);
});
} else {
if (_idx === TOTAL_LOCATIONS) {
// we get here if we didn't find the block
log.message(log.ERROR, "Unable to locate block in any storage location");
res.statusCode = 500;
return res.end("Unable to return file, missing blocks");
} else {
return search_for_block(_idx);
}
}
});
}
});
};
var read_file = function read_file(path, try_compressed){
var read_stream = operations.stream_read(path);
var decryptor = create_decryptor({ encrypted : requested_file.encrypted, key : requested_file.access_key});
var unzipper = create_unzipper(try_compressed);
var should_end = (idx + 1) === total_blocks;
function on_error(){
if (try_compressed) {
log.message(log.WARN, "Cannot locate compressed block in last_seen location, trying uncompressed");
return load_from_last_seen(false);
} else {
log.message(log.WARN, "Did not find block in expected location. Searching...");
return search_for_block(0);
}
}
function on_end(){
idx++;
read_stream.removeListener("end", on_end);
read_stream.removeListener("error", on_error);
if (res.getMaxListeners !== undefined) {
res.setMaxListeners(res.getMaxListeners() - 1);
}
send_blocks();
}
if (res.getMaxListeners !== undefined) {
res.setMaxListeners(res.getMaxListeners() + 1);
} else {
res.setMaxListeners(0);
}
read_stream.on("end", on_end);
read_stream.on("error", on_error);
read_stream.pipe(unzipper).pipe(decryptor).pipe(res, {end: should_end});
};
var load_from_last_seen = function load_from_last_seen(try_compressed){
var sfx = try_compressed ? ".gz" : "";
var block = requested_file.blocks[idx];
var block_filename = block.last_seen + block.block_hash + sfx;
read_file(block_filename, try_compressed);
};
var send_blocks = function send_blocks(){
if (idx === total_blocks) { // we're done
return;
} else {
if (requested_file.blocks[idx].last_seen) {
load_from_last_seen(true);
} else {
search_for_block(0);
}
}
};
send_blocks();
});
break;
case "POST":
case "PUT":
// check if a file exists at this url
log.message(log.DEBUG, "Begin checking for existing file");
utils.load_inode(target_url, function(err, inode){
if (inode){
// check authorization
if (validate.is_authorized(inode, req.method, params)){
log.message(log.INFO, "File update request authorized");
} else {
log.message(log.WARN, "File update request unauthorized");
res.statusCode = 401;
res.end();
return;
}
} else {
// if static access keys are configured, require an access key or token
// TODO: Maybe DRY up these unauthorized responses.
if("STATIC_ACCESS_KEYS" in config && config.STATIC_ACCESS_KEYS.length > 0){
if((!params.access_key || params.access_key.length < 1) && (!params.access_token || params.access_token.length < 1)) {
log.message(log.WARN, "File update request unauthorized");
res.statusCode = 401;
res.end();
return;
}
// if an access_key was provided, check against the static keys
if(params.access_key && params.access_key.length > 0){
if(!config.STATIC_ACCESS_KEYS.includes(params.access_key)){
log.message(log.WARN, "File update request unauthorized");
res.statusCode = 401;
res.end();
return;
}
}
}
log.message(log.DEBUG, "No existing file found, storing new file");
}
// store the posted data at the specified URL
var new_file = Object.create(Inode);
new_file.init(target_url);
log.message(log.DEBUG, "New file object created");
// set additional file properties (content-type, etc.)
if(params.content_type){
log.message(log.INFO, "Content-Type: " + params.content_type);
new_file.file_metadata.content_type = params.content_type;
}
if(params.private){
new_file.file_metadata.private = true;
}
if(params.encrypted){
new_file.file_metadata.encrypted = true;
}
// if access_key is supplied with update, replace the default one
if(params.access_key){
new_file.file_metadata.access_key = params.access_key;
}
log.message(log.INFO, "File properties set");
req.on("data", function(chunk){
new_file.write(chunk, req, function(result){
if (!result) {
log.message(log.ERROR, "Error writing data to storage object");
res.statusCode = 500;
res.end();
}
});
});
req.on("end", function(){
log.message(log.INFO, "End of request");
if(new_file){
log.message(log.DEBUG, "Closing new file");
new_file.close(function(result){
if(result){
res.end(JSON.stringify(result));
} else {
log.message(log.ERROR, "Error closing storage object");
res.statusCode = 500;
res.end();
}
});
}
});
});
break;
case "DELETE":
// remove the data stored at the specified URL
utils.load_inode(target_url, function(error, inode){
if (error) {
log.message(log.WARN, "Error loading inode: " + error.toString());
}
if(inode){
// authorize (only keyholder can delete)
if(validate.has_key(inode, params)){
// delete inode file
log.message(log.INFO, "Delete request authorized");
var remove_inode = function remove_inode(idx){
var location = config.STORAGE_LOCATIONS[idx];
var file = location.path + inode.fingerprint + ".json";
operations.delete(file, function(err){
idx++;
if (err) {
log.message(log.WARN, "Inode " + inode.fingerprint + " doesn't exist in location " + location.path);
}
if (idx === TOTAL_LOCATIONS) {
res.statusCode = 204;
return res.end();
} else {
remove_inode(idx);
}
});
};
remove_inode(0);
} else {
log.message(log.WARN, "Delete request unauthorized");
res.statusCode = 401;
res.end();
}
} else {
log.message(log.WARN, "Delete request file not found");
res.statusCode = 404;
res.end();
}
});
break;
case "HEAD":
utils.load_inode(target_url, function(error, requested_file){
if (error) {
log.message(log.WARN, "Error loading inode: " + error.toString());
}
if(requested_file){
// construct headers
res.setHeader("Content-Type", requested_file.content_type);
res.setHeader("Content-Length", requested_file.file_size);
// add extended object headers if we have them
if(requested_file.media_type){
res.setHeader("X-Media-Type", requested_file.media_type);
if(requested_file.media_type !== "unknown"){
res.setHeader("X-Media-Size", requested_file.media_size);
res.setHeader("X-Media-Channels", requested_file.media_channels);
res.setHeader("X-Media-Bitrate", requested_file.media_bitrate);
res.setHeader("X-Media-Resolution", requested_file.media_resolution);
res.setHeader("X-Media-Duration", requested_file.media_duration);
}
}
return res.end();
} else {
log.message(log.INFO, "HEAD Result: 404");
res.statusCode = 404;
return res.end();
}
});
break;
case "OPTIONS":
// support for OPTIONS is required to support cross-domain requests (CORS)
res.writeHead(204);
res.end();
break;
default:
res.writeHead(405);
res.end("method " + req.method + " is not supported");
}
}).listen(config.SERVER_PORT);