cryptpad/rpc.js

1066 lines
31 KiB
JavaScript
Raw Normal View History

2017-04-25 14:04:17 +00:00
/*@flow*/
/*jshint esversion: 6 */
2017-03-27 16:15:15 +00:00
/* Use Nacl for checking signatures of messages */
var Nacl = require("tweetnacl");
2017-05-04 09:46:11 +00:00
/* globals Buffer*/
/* globals process */
var Fs = require("fs");
2017-05-04 09:36:56 +00:00
var Path = require("path");
2017-05-11 14:12:44 +00:00
var Https = require("https");
const Package = require('./package.json');
var RPC = module.exports;
2017-04-03 17:24:57 +00:00
var Store = require("./storage/file");
2017-05-11 14:12:44 +00:00
2017-05-22 09:16:01 +00:00
var DEFAULT_LIMIT = 50 * 1024 * 1024;
var isValidId = function (chan) {
2017-05-10 13:36:14 +00:00
return /^[a-fA-F0-9]/.test(chan) ||
[32, 48].indexOf(chan.length) !== -1;
2017-03-15 14:55:25 +00:00
};
2017-05-04 09:36:56 +00:00
var uint8ArrayToHex = function (a) {
// call slice so Uint8Arrays work as expected
2017-05-04 14:16:09 +00:00
return Array.prototype.slice.call(a).map(function (e) {
2017-05-04 09:36:56 +00:00
var n = Number(e & 0xff).toString(16);
if (n === 'NaN') {
throw new Error('invalid input resulted in NaN');
}
switch (n.length) {
case 0: return '00'; // just being careful, shouldn't happen
case 1: return '0' + n;
case 2: return n;
default: throw new Error('unexpected value');
}
}).join('');
};
2017-05-10 13:36:14 +00:00
var createFileId = function () {
var id = uint8ArrayToHex(Nacl.randomBytes(24));
if (id.length !== 48 || /[^a-f0-9]/.test(id)) {
throw new Error('file ids must consist of 48 hex characters');
2017-05-04 09:36:56 +00:00
}
return id;
};
var makeToken = function () {
return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
.toString(16);
};
var makeCookie = function (token) {
var time = (+new Date());
time -= time % 5000;
2017-03-27 16:15:15 +00:00
return [
time,
2017-05-04 09:46:11 +00:00
process.pid,
token
2017-04-03 17:24:57 +00:00
];
2017-03-27 16:15:15 +00:00
};
var parseCookie = function (cookie) {
if (!(cookie && cookie.split)) { return null; }
var parts = cookie.split('|');
if (parts.length !== 3) { return null; }
2017-03-27 16:15:15 +00:00
var c = {};
c.time = new Date(parts[0]);
2017-04-03 17:24:57 +00:00
c.pid = Number(parts[1]);
c.seq = parts[2];
2017-03-27 16:15:15 +00:00
return c;
};
var escapeKeyCharacters = function (key) {
return key.replace(/\//g, '-');
};
var unescapeKeyCharacters = function (key) {
return key.replace(/\-/g, '/');
};
2017-06-01 16:16:02 +00:00
// TODO Rename to getSession ?
2017-04-10 15:42:35 +00:00
var beginSession = function (Sessions, key) {
var safeKey = escapeKeyCharacters(key);
if (Sessions[safeKey]) {
Sessions[safeKey].atime = +new Date();
return Sessions[safeKey];
2017-04-10 15:42:35 +00:00
}
var user = Sessions[safeKey] = {};
user.atime = +new Date();
2017-04-10 15:42:35 +00:00
user.tokens = [
makeToken()
];
return user;
};
var isTooOld = function (time, now) {
return (now - time) > 300000;
};
2017-04-03 17:24:57 +00:00
2017-04-10 15:42:35 +00:00
var expireSessions = function (Sessions) {
var now = +new Date();
Object.keys(Sessions).forEach(function (key) {
2017-05-05 07:12:16 +00:00
var session = Sessions[key];
2017-04-10 15:42:35 +00:00
if (isTooOld(Sessions[key].atime, now)) {
if (session.blobstage) {
session.blobstage.close();
}
2017-04-10 15:42:35 +00:00
delete Sessions[key];
}
});
};
var addTokenForKey = function (Sessions, publicKey, token) {
if (!Sessions[publicKey]) { throw new Error('undefined user'); }
var user = beginSession(Sessions, publicKey);
2017-04-10 15:42:35 +00:00
user.tokens.push(token);
user.atime = +new Date();
if (user.tokens.length > 2) { user.tokens.shift(); }
};
var isValidCookie = function (Sessions, publicKey, cookie) {
var parsed = parseCookie(cookie);
if (!parsed) { return false; }
2017-04-03 17:24:57 +00:00
var now = +new Date();
if (!parsed.time) { return false; }
if (isTooOld(parsed.time, now)) {
2017-03-27 16:15:15 +00:00
return false;
}
// different process. try harder
2017-05-04 09:46:11 +00:00
if (process.pid !== parsed.pid) {
2017-03-27 16:15:15 +00:00
return false;
}
var user = beginSession(Sessions, publicKey);
if (!user) { return false; }
var idx = user.tokens.indexOf(parsed.seq);
if (idx === -1) { return false; }
if (idx > 0) {
// make a new token
2017-04-10 15:42:35 +00:00
addTokenForKey(Sessions, publicKey, makeToken());
}
2017-03-27 16:15:15 +00:00
return true;
};
var checkSignature = function (signedMsg, signature, publicKey) {
if (!(signedMsg && publicKey)) { return false; }
2017-03-20 17:02:11 +00:00
var signedBuffer;
var pubBuffer;
2017-03-27 16:15:15 +00:00
var signatureBuffer;
try {
2017-03-27 16:15:15 +00:00
signedBuffer = Nacl.util.decodeUTF8(signedMsg);
} catch (e) {
2017-03-27 16:15:15 +00:00
console.log('invalid signedBuffer');
console.log(signedMsg);
return null;
}
2017-03-27 16:15:15 +00:00
try {
pubBuffer = Nacl.util.decodeBase64(publicKey);
} catch (e) {
return false;
}
try {
signatureBuffer = Nacl.util.decodeBase64(signature);
} catch (e) {
return false;
}
if (pubBuffer.length !== 32) {
console.log('public key length: ' + pubBuffer.length);
console.log(publicKey);
return false;
}
2017-03-27 16:15:15 +00:00
if (signatureBuffer.length !== 64) {
return false;
}
2017-03-27 16:15:15 +00:00
return Nacl.sign.detached.verify(signedBuffer, signatureBuffer, pubBuffer);
};
2017-05-16 16:08:59 +00:00
var loadUserPins = function (Env, publicKey, cb) {
2017-05-16 15:07:53 +00:00
var pinStore = Env.pinStore;
2017-05-16 16:08:59 +00:00
var session = beginSession(Env.Sessions, publicKey);
2017-04-10 15:42:35 +00:00
if (session.channels) {
return cb(session.channels);
}
// if channels aren't in memory. load them from disk
2017-04-03 17:24:57 +00:00
var pins = {};
2017-04-10 15:42:35 +00:00
var pin = function (channel) {
pins[channel] = true;
};
var unpin = function (channel) {
pins[channel] = false;
};
2017-05-16 15:07:53 +00:00
pinStore.getMessages(publicKey, function (msg) {
2017-04-03 17:24:57 +00:00
// handle messages...
var parsed;
try {
parsed = JSON.parse(msg);
switch (parsed[0]) {
case 'PIN':
2017-04-10 15:42:35 +00:00
parsed[1].forEach(pin);
2017-04-03 17:24:57 +00:00
break;
case 'UNPIN':
2017-04-10 15:42:35 +00:00
parsed[1].forEach(unpin);
2017-04-03 17:24:57 +00:00
break;
case 'RESET':
2017-04-10 15:42:35 +00:00
Object.keys(pins).forEach(unpin);
2017-04-07 09:37:19 +00:00
if (parsed[1] && parsed[1].length) {
2017-04-10 15:42:35 +00:00
parsed[1].forEach(pin);
2017-04-07 09:37:19 +00:00
}
2017-04-03 17:24:57 +00:00
break;
default:
console.error('invalid message read from store');
}
} catch (e) {
console.log('invalid message read from store');
console.error(e);
}
}, function () {
// no more messages
2017-04-10 15:42:35 +00:00
// only put this into the cache if it completes
session.channels = pins;
cb(pins);
});
};
var truthyKeys = function (O) {
return Object.keys(O).filter(function (k) {
return O[k];
});
};
2017-05-16 16:08:59 +00:00
var getChannelList = function (Env, publicKey, cb) {
loadUserPins(Env, publicKey, function (pins) {
2017-04-10 15:42:35 +00:00
cb(truthyKeys(pins));
2017-04-03 17:24:57 +00:00
});
};
var makeFilePath = function (root, id) {
if (typeof(id) !== 'string' || id.length <= 2) { return null; }
return Path.join(root, id.slice(0, 2), id);
};
2017-05-16 15:07:53 +00:00
var getUploadSize = function (Env, channel, cb) {
var paths = Env.paths;
var path = makeFilePath(paths.blob, channel);
if (!path) {
return cb('INVALID_UPLOAD_ID');
}
2017-05-10 13:36:14 +00:00
Fs.stat(path, function (err, stats) {
if (err) { return void cb(err); }
cb(void 0, stats.size);
});
};
2017-05-16 16:08:59 +00:00
var getFileSize = function (Env, channel, cb) {
if (!isValidId(channel)) { return void cb('INVALID_CHAN'); }
2017-05-10 13:36:14 +00:00
if (channel.length === 32) {
2017-05-16 16:08:59 +00:00
if (typeof(Env.msgStore.getChannelSize) !== 'function') {
2017-05-10 13:36:14 +00:00
return cb('GET_CHANNEL_SIZE_UNSUPPORTED');
}
2017-05-16 16:08:59 +00:00
return void Env.msgStore.getChannelSize(channel, function (e, size) {
if (e) {
if (e === 'ENOENT') { return void cb(void 0, 0); }
return void cb(e.code);
}
2017-05-10 13:36:14 +00:00
cb(void 0, size);
});
2017-04-21 12:51:00 +00:00
}
2017-04-07 08:09:59 +00:00
2017-05-10 13:36:14 +00:00
// 'channel' refers to a file, so you need anoter API
2017-05-16 15:07:53 +00:00
getUploadSize(Env, channel, function (e, size) {
2017-05-10 13:36:14 +00:00
if (e) { return void cb(e); }
2017-04-07 08:09:59 +00:00
cb(void 0, size);
});
};
2017-05-16 16:08:59 +00:00
var getMultipleFileSize = function (Env, channels, cb) {
var msgStore = Env.msgStore;
if (!Array.isArray(channels)) { return cb('INVALID_PIN_LIST'); }
if (typeof(msgStore.getChannelSize) !== 'function') {
2017-04-21 12:51:00 +00:00
return cb('GET_CHANNEL_SIZE_UNSUPPORTED');
}
var i = channels.length;
var counts = {};
var done = function () {
i--;
if (i === 0) { return cb(void 0, counts); }
};
channels.forEach(function (channel) {
2017-05-16 16:08:59 +00:00
getFileSize(Env, channel, function (e, size) {
2017-04-21 12:51:00 +00:00
if (e) {
console.error(e);
2017-04-21 12:51:00 +00:00
counts[channel] = -1;
return done();
}
counts[channel] = size;
done();
});
});
};
2017-05-16 16:08:59 +00:00
var getTotalSize = function (Env, publicKey, cb) {
2017-04-07 08:09:59 +00:00
var bytes = 0;
2017-05-16 16:08:59 +00:00
return void getChannelList(Env, publicKey, function (channels) {
if (!channels) { return cb('INVALID_PIN_LIST'); } // unexpected
2017-04-07 08:09:59 +00:00
var count = channels.length;
if (!count) { cb(void 0, 0); }
channels.forEach(function (channel) {
2017-05-16 16:08:59 +00:00
getFileSize(Env, channel, function (e, size) {
2017-04-07 08:09:59 +00:00
count--;
if (!e) { bytes += size; }
if (count === 0) { return cb(void 0, bytes); }
});
});
});
};
2017-04-03 17:24:57 +00:00
var hashChannelList = function (A) {
var uniques = [];
A.forEach(function (a) {
if (uniques.indexOf(a) === -1) { uniques.push(a); }
});
uniques.sort();
var hash = Nacl.util.encodeBase64(Nacl.hash(Nacl
.util.decodeUTF8(JSON.stringify(uniques))));
return hash;
};
2017-05-16 16:08:59 +00:00
var getHash = function (Env, publicKey, cb) {
getChannelList(Env, publicKey, function (channels) {
2017-04-07 08:30:40 +00:00
cb(void 0, hashChannelList(channels));
});
};
// The limits object contains storage limits for all the publicKey that have paid
// To each key is associated an object containing the 'limit' value and a 'note' explaining that limit
var limits = {};
var updateLimits = function (config, publicKey, cb) {
if (config.adminEmail === false) {
if (config.allowSubscriptions === false) { return; }
throw new Error("allowSubscriptions must be false if adminEmail is false");
}
if (typeof cb !== "function") { cb = function () {}; }
var defaultLimit = typeof(config.defaultStorageLimit) === 'number'?
config.defaultStorageLimit: DEFAULT_LIMIT;
2017-05-23 09:46:59 +00:00
var userId;
if (publicKey) {
userId = unescapeKeyCharacters(publicKey);
}
var body = JSON.stringify({
domain: config.myDomain,
subdomain: config.mySubdomain,
adminEmail: config.adminEmail,
version: Package.version
});
var options = {
host: 'accounts.cryptpad.fr',
path: '/api/getauthorized',
method: 'POST',
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
}
};
var req = Https.request(options, function (response) {
if (!('' + response.statusCode).match(/^2\d\d$/)) {
return void cb('SERVER ERROR ' + response.statusCode);
}
var str = '';
2017-05-16 15:07:53 +00:00
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
try {
var json = JSON.parse(str);
limits = json;
var l;
2017-05-23 09:46:59 +00:00
if (userId) {
var limit = limits[userId];
l = limit && typeof limit.limit === "number" ?
2017-05-22 13:56:24 +00:00
[limit.limit, limit.plan, limit.note] : [defaultLimit, '', ''];
}
cb(void 0, l);
} catch (e) {
cb(e);
}
});
});
req.on('error', function (e) {
if (!config.domain) { return cb(); }
cb(e);
});
req.end(body);
};
var getLimit = function (Env, publicKey, cb) {
var unescapedKey = unescapeKeyCharacters(publicKey);
var limit = limits[unescapedKey];
var defaultLimit = typeof(Env.defaultStorageLimit) === 'number'?
Env.defaultStorageLimit: DEFAULT_LIMIT;
var toSend = limit && typeof(limit.limit) === "number"?
2017-05-22 13:56:24 +00:00
[limit.limit, limit.plan, limit.note] : [defaultLimit, '', ''];
cb(void 0, toSend);
};
var getFreeSpace = function (Env, publicKey, cb) {
getLimit(Env, publicKey, function (e, limit) {
if (e) { return void cb(e); }
getTotalSize(Env, publicKey, function (e, size) {
if (e) { return void cb(e); }
var rem = limit[0] - size;
if (typeof(rem) !== 'number') {
return void cb('invalid_response');
}
cb(void 0, rem);
});
});
};
var sumChannelSizes = function (sizes) {
return Object.keys(sizes).map(function (id) { return sizes[id]; })
.filter(function (x) {
// only allow positive numbers
return !(typeof(x) !== 'number' || x <= 0);
})
2017-05-19 07:11:28 +00:00
.reduce(function (a, b) { return a + b; }, 0);
};
var pinChannel = function (Env, publicKey, channels, cb) {
2017-04-10 15:42:35 +00:00
if (!channels && channels.filter) {
return void cb('INVALID_PIN_LIST');
2017-04-10 15:42:35 +00:00
}
2017-04-07 08:30:40 +00:00
// get channel list ensures your session has a cached channel list
2017-05-16 16:08:59 +00:00
getChannelList(Env, publicKey, function (pinned) {
var session = beginSession(Env.Sessions, publicKey);
2017-04-10 15:42:35 +00:00
// only pin channels which are not already pinned
var toStore = channels.filter(function (channel) {
return pinned.indexOf(channel) === -1;
});
if (toStore.length === 0) {
2017-05-16 16:08:59 +00:00
return void getHash(Env, publicKey, cb);
2017-04-10 15:42:35 +00:00
}
getMultipleFileSize(Env, toStore, function (e, sizes) {
2017-04-10 15:42:35 +00:00
if (e) { return void cb(e); }
var pinSize = sumChannelSizes(sizes);
getFreeSpace(Env, publicKey, function (e, free) {
if (e) {
console.error(e);
return void cb(e);
}
if (pinSize > free) { return void cb('E_OVER_LIMIT'); }
Env.pinStore.message(publicKey, JSON.stringify(['PIN', toStore]),
function (e) {
if (e) { return void cb(e); }
toStore.forEach(function (channel) {
session.channels[channel] = true;
});
getHash(Env, publicKey, cb);
});
2017-04-10 15:42:35 +00:00
});
2017-04-07 08:30:40 +00:00
});
2017-04-03 17:24:57 +00:00
});
};
2017-05-16 16:08:59 +00:00
var unpinChannel = function (Env, publicKey, channels, cb) {
2017-05-16 15:07:53 +00:00
var pinStore = Env.pinStore;
2017-04-10 15:42:35 +00:00
if (!channels && channels.filter) {
// expected array
return void cb('INVALID_PIN_LIST');
2017-04-10 15:42:35 +00:00
}
2017-04-07 08:30:40 +00:00
2017-05-16 16:08:59 +00:00
getChannelList(Env, publicKey, function (pinned) {
var session = beginSession(Env.Sessions, publicKey);
2017-04-10 15:42:35 +00:00
// only unpin channels which are pinned
var toStore = channels.filter(function (channel) {
return pinned.indexOf(channel) !== -1;
});
if (toStore.length === 0) {
2017-05-16 16:08:59 +00:00
return void getHash(Env, publicKey, cb);
2017-04-10 15:42:35 +00:00
}
pinStore.message(publicKey, JSON.stringify(['UNPIN', toStore]),
2017-04-10 15:42:35 +00:00
function (e) {
if (e) { return void cb(e); }
toStore.forEach(function (channel) {
2017-05-10 13:36:14 +00:00
delete session.channels[channel];
2017-04-10 15:42:35 +00:00
});
2017-05-16 16:08:59 +00:00
getHash(Env, publicKey, cb);
2017-04-07 08:30:40 +00:00
});
});
};
2017-05-16 16:08:59 +00:00
var resetUserPins = function (Env, publicKey, channelList, cb) {
if (!Array.isArray(channelList)) { return void cb('INVALID_PIN_LIST'); }
2017-05-16 15:07:53 +00:00
var pinStore = Env.pinStore;
2017-05-16 16:08:59 +00:00
var session = beginSession(Env.Sessions, publicKey);
2017-04-10 15:42:35 +00:00
if (!channelList.length) {
return void getHash(Env, publicKey, function (e, hash) {
if (e) { return cb(e); }
cb(void 0, hash);
});
}
2017-04-10 15:42:35 +00:00
var pins = session.channels = {};
getMultipleFileSize(Env, channelList, function (e, sizes) {
2017-04-07 09:37:19 +00:00
if (e) { return void cb(e); }
var pinSize = sumChannelSizes(sizes);
getFreeSpace(Env, publicKey, function (e, free) {
if (e) {
console.error(e);
return void cb(e);
}
if (pinSize > free) { return void(cb('E_OVER_LIMIT')); }
pinStore.message(publicKey, JSON.stringify(['RESET', channelList]),
function (e) {
if (e) { return void cb(e); }
channelList.forEach(function (channel) {
pins[channel] = true;
});
2017-04-07 09:37:19 +00:00
getHash(Env, publicKey, function (e, hash) {
cb(e, hash);
});
});
2017-04-07 09:37:19 +00:00
});
});
2017-04-07 08:30:40 +00:00
};
var getPrivilegedUserList = function (cb) {
Fs.readFile('./privileged.conf', 'utf8', function (e, body) {
if (e) {
if (e.code === 'ENOENT') {
return void cb(void 0, []);
}
return void (e.code);
}
var list = body.split(/\n/)
.map(function (line) {
return line.replace(/#.*$/, '').trim();
})
.filter(function (x) { return x; });
cb(void 0, list);
});
};
var isPrivilegedUser = function (publicKey, cb) {
getPrivilegedUserList(function (e, list) {
if (e) { return void cb(false); }
cb(list.indexOf(publicKey) !== -1);
});
};
var safeMkdir = function (path, cb) {
Fs.mkdir(path, function (e) {
if (!e || e.code === 'EEXIST') { return void cb(); }
cb(e);
});
2017-04-28 09:46:13 +00:00
};
2017-05-04 09:36:56 +00:00
var makeFileStream = function (root, id, cb) {
var stub = id.slice(0, 2);
var full = makeFilePath(root, id);
safeMkdir(Path.join(root, stub), function (e) {
if (e) { return void cb(e); }
try {
var stream = Fs.createWriteStream(full, {
flags: 'a',
encoding: 'binary',
2017-06-01 16:16:02 +00:00
highWaterMark: Math.pow(2, 16),
2017-05-04 09:36:56 +00:00
});
stream.on('open', function () {
cb(void 0, stream);
});
} catch (err) {
cb('BAD_STREAM');
}
});
};
2017-05-16 16:08:59 +00:00
var upload = function (Env, publicKey, content, cb) {
2017-05-16 15:07:53 +00:00
var paths = Env.paths;
2017-06-01 16:16:02 +00:00
var dec;
try { dec = Buffer.from(content, 'base64'); }
catch (e) { return void cb(e); }
var len = dec.length;
2017-04-28 09:46:13 +00:00
2017-05-16 16:08:59 +00:00
var session = beginSession(Env.Sessions, publicKey);
2017-06-01 16:16:02 +00:00
if (typeof(session.currentUploadSize) !== 'number' ||
typeof(session.currentUploadSize) !== 'number') {
// improperly initialized... maybe they didn't check before uploading?
// reject it, just in case
return cb('NOT_READY');
}
if (session.currentUploadSize > session.pendingUploadSize) {
return cb('E_OVER_LIMIT');
}
2017-05-04 09:36:56 +00:00
if (!session.blobstage) {
2017-05-10 13:36:14 +00:00
makeFileStream(paths.staging, publicKey, function (e, stream) {
2017-05-04 09:36:56 +00:00
if (e) { return void cb(e); }
2017-04-28 15:11:50 +00:00
2017-05-04 09:36:56 +00:00
var blobstage = session.blobstage = stream;
blobstage.write(dec);
session.currentUploadSize += len;
2017-05-04 09:36:56 +00:00
cb(void 0, dec.length);
});
} else {
session.blobstage.write(dec);
session.currentUploadSize += len;
2017-05-04 09:36:56 +00:00
cb(void 0, dec.length);
}
};
2017-05-16 16:08:59 +00:00
var upload_cancel = function (Env, publicKey, cb) {
2017-05-16 15:07:53 +00:00
var paths = Env.paths;
2017-06-01 16:16:02 +00:00
var session = beginSession(Env.Sessions, publicKey);
delete session.currentUploadSize;
delete session.pendingUploadSize;
if (session.blobstage) { session.blobstage.close(); }
2017-05-10 13:36:14 +00:00
var path = makeFilePath(paths.staging, publicKey);
2017-05-04 09:36:56 +00:00
if (!path) {
2017-05-10 13:36:14 +00:00
console.log(paths.staging, publicKey);
2017-05-04 09:36:56 +00:00
console.log(path);
return void cb('NO_FILE');
}
Fs.unlink(path, function (e) {
if (e) { return void cb('E_UNLINK'); }
cb(void 0);
});
2017-04-28 15:11:50 +00:00
};
2017-05-04 09:36:56 +00:00
var isFile = function (filePath, cb) {
Fs.stat(filePath, function (e, stats) {
if (e) {
if (e.code === 'ENOENT') { return void cb(void 0, false); }
return void cb(e.message);
}
return void cb(void 0, stats.isFile());
});
};
2017-05-16 16:08:59 +00:00
var upload_complete = function (Env, publicKey, cb) {
2017-05-16 15:07:53 +00:00
var paths = Env.paths;
2017-05-16 16:08:59 +00:00
var session = beginSession(Env.Sessions, publicKey);
2017-05-04 09:36:56 +00:00
if (session.blobstage && session.blobstage.close) {
session.blobstage.close();
delete session.blobstage;
}
2017-05-10 13:36:14 +00:00
var oldPath = makeFilePath(paths.staging, publicKey);
2017-05-04 09:36:56 +00:00
var tryRandomLocation = function (cb) {
2017-05-10 13:36:14 +00:00
var id = createFileId();
2017-05-04 09:36:56 +00:00
var prefix = id.slice(0, 2);
2017-05-10 13:36:14 +00:00
var newPath = makeFilePath(paths.blob, id);
2017-05-04 09:36:56 +00:00
2017-05-10 13:36:14 +00:00
safeMkdir(Path.join(paths.blob, prefix), function (e) {
2017-05-04 09:36:56 +00:00
if (e) {
console.error('[safeMkdir]');
2017-05-04 09:36:56 +00:00
console.error(e);
console.log();
2017-05-04 09:36:56 +00:00
return void cb('RENAME_ERR');
}
isFile(newPath, function (e, yes) {
if (e) {
console.error(e);
return void cb(e);
}
if (yes) {
return void tryRandomLocation(cb);
}
cb(void 0, newPath, id);
});
});
};
var retries = 3;
var handleMove = function (e, newPath, id) {
if (e) {
if (retries--) {
setTimeout(function () {
return tryRandomLocation(handleMove);
}, 750);
}
}
// lol wut handle ur errors
2017-05-04 09:36:56 +00:00
Fs.rename(oldPath, newPath, function (e) {
if (e) {
console.error(e);
if (retries--) {
return setTimeout(function () {
tryRandomLocation(handleMove);
}, 750);
}
2017-05-04 09:36:56 +00:00
return cb(e);
}
cb(void 0, id);
});
};
tryRandomLocation(handleMove);
2017-05-04 09:36:56 +00:00
};
2017-05-16 16:08:59 +00:00
var upload_status = function (Env, publicKey, filesize, cb) {
2017-05-16 15:07:53 +00:00
var paths = Env.paths;
// validate that the provided size is actually a positive number
if (typeof(filesize) !== 'number' &&
filesize >= 0) { return void cb('E_INVALID_SIZE'); }
// validate that the provided path is not junk
2017-05-10 13:36:14 +00:00
var filePath = makeFilePath(paths.staging, publicKey);
2017-05-04 09:36:56 +00:00
if (!filePath) { return void cb('E_INVALID_PATH'); }
getFreeSpace(Env, publicKey, function (e, free) {
if (e) { return void cb(e); }
if (filesize >= free) { return cb('NOT_ENOUGH_SPACE'); }
isFile(filePath, function (e, yes) {
if (e) {
console.error("uploadError: [%s]", e);
return cb('UNNOWN_ERROR');
}
cb(e, yes);
});
2017-05-04 09:36:56 +00:00
});
2017-04-28 09:46:13 +00:00
};
2017-05-31 10:51:26 +00:00
var isAuthenticatedCall = function (call) {
return [
'COOKIE',
2017-05-31 10:51:26 +00:00
'RESET',
'PIN',
'UNPIN',
'GET_HASH',
'GET_TOTAL_SIZE',
'GET_FILE_SIZE',
'UPDATE_LIMITS',
'GET_LIMIT',
'GET_MULTIPLE_FILE_SIZE',
2017-06-01 16:16:02 +00:00
//'UPLOAD',
2017-05-31 10:51:26 +00:00
'UPLOAD_COMPLETE',
'UPLOAD_CANCEL',
].indexOf(call) !== -1;
};
2017-04-25 14:04:17 +00:00
/*::const ConfigType = require('./config.example.js');*/
RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) {
// load pin-store...
console.log('loading rpc module...');
2017-03-27 16:15:15 +00:00
var warn = function (e, output) {
if (e && !config.suppressRPCErrors) {
2017-05-29 15:35:13 +00:00
console.error(new Date().toISOString() + ' [' + e + ']', output);
}
};
2017-05-04 09:36:56 +00:00
var keyOrDefaultString = function (key, def) {
return typeof(config[key]) === 'string'? config[key]: def;
};
2017-05-16 15:07:53 +00:00
var Env = {};
Env.defaultStorageLimit = config.defaultStorageLimit;
Env.maxUploadSize = config.maxUploadSize || (20 * 1024 * 1024);
2017-05-16 16:08:59 +00:00
var Sessions = Env.Sessions = {};
2017-05-16 15:07:53 +00:00
var paths = Env.paths = {};
2017-05-10 13:36:14 +00:00
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob');
var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
2017-05-04 09:36:56 +00:00
2017-04-25 14:04:17 +00:00
var rpc = function (
ctx /*:{ store: Object }*/,
data /*:Array<Array<any>>*/,
respond /*:(?string, ?Array<any>)=>void*/)
{
2017-04-24 10:10:12 +00:00
if (!Array.isArray(data)) {
return void respond('INVALID_ARG_FORMAT');
}
2017-03-27 16:15:15 +00:00
if (!data.length) {
return void respond("INSUFFICIENT_ARGS");
2017-03-27 16:15:15 +00:00
} else if (data.length !== 1) {
2017-04-24 10:10:12 +00:00
console.log('[UNEXPECTED_ARGUMENTS_LENGTH] %s', data.length);
}
2017-03-27 16:15:15 +00:00
var msg = data[0].slice(0);
2017-04-03 17:24:57 +00:00
2017-04-24 10:10:12 +00:00
if (!Array.isArray(msg)) {
return void respond('INVALID_ARG_FORMAT');
}
2017-03-27 16:15:15 +00:00
var signature = msg.shift();
var publicKey = msg.shift();
// make sure a user object is initialized in the cookie jar
2017-04-10 15:42:35 +00:00
beginSession(Sessions, publicKey);
var cookie = msg[0];
2017-04-10 15:42:35 +00:00
if (!isValidCookie(Sessions, publicKey, cookie)) {
2017-03-27 16:15:15 +00:00
// no cookie is fine if the RPC is to get a cookie
if (msg[1] !== 'COOKIE') {
2017-03-27 16:15:15 +00:00
return void respond('NO_COOKIE');
}
}
var serialized = JSON.stringify(msg);
2017-04-05 15:28:04 +00:00
if (!(serialized && typeof(publicKey) === 'string')) {
2017-03-27 16:15:15 +00:00
return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY');
}
2017-05-31 10:51:26 +00:00
if (isAuthenticatedCall(msg[1])) {
if (checkSignature(serialized, signature, publicKey) !== true) {
return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY");
}
2017-03-27 16:15:15 +00:00
}
var safeKey = escapeKeyCharacters(publicKey);
/* If you have gotten this far, you have signed the message with the
public key which you provided.
We can safely modify the state for that key
OR it's an unauthenticated call, which must not modify the state
for that key in a meaningful way.
*/
// discard validated cookie from message
msg.shift();
var Respond = function (e, msg) {
var token = Sessions[safeKey].tokens.slice(-1)[0];
var cookie = makeCookie(token).join('|');
2017-05-04 09:36:56 +00:00
respond(e, [cookie].concat(typeof(msg) !== 'undefined' ?msg: []));
};
if (typeof(msg) !== 'object' || !msg.length) {
return void Respond('INVALID_MSG');
}
var deny = function () {
Respond('E_ACCESS_DENIED');
};
2017-05-16 16:08:59 +00:00
if (!Env.msgStore) { Env.msgStore = ctx.store; }
var handleMessage = function (privileged) {
switch (msg[0]) {
2017-04-07 09:37:19 +00:00
case 'COOKIE': return void Respond(void 0);
case 'RESET':
2017-05-16 16:08:59 +00:00
return resetUserPins(Env, safeKey, msg[1], function (e, hash) {
//warn(e, hash);
2017-04-07 09:37:19 +00:00
return void Respond(e, hash);
2017-04-03 17:24:57 +00:00
});
case 'PIN':
2017-05-16 16:08:59 +00:00
return pinChannel(Env, safeKey, msg[1], function (e, hash) {
warn(e, hash);
2017-04-07 08:30:40 +00:00
Respond(e, hash);
2017-04-03 17:24:57 +00:00
});
case 'UNPIN':
2017-05-16 16:08:59 +00:00
return unpinChannel(Env, safeKey, msg[1], function (e, hash) {
warn(e, hash);
2017-04-07 08:30:40 +00:00
Respond(e, hash);
2017-04-03 17:24:57 +00:00
});
case 'GET_HASH':
2017-05-16 16:08:59 +00:00
return void getHash(Env, safeKey, function (e, hash) {
warn(e, hash);
2017-04-07 08:30:40 +00:00
Respond(e, hash);
2017-04-03 17:24:57 +00:00
});
2017-04-28 09:46:13 +00:00
case 'GET_TOTAL_SIZE': // TODO cache this, since it will get called quite a bit
2017-05-16 16:08:59 +00:00
return getTotalSize(Env, safeKey, function (e, size) {
if (e) {
warn(e, safeKey);
return void Respond(e);
}
2017-04-07 08:09:59 +00:00
Respond(e, size);
2017-03-15 14:55:25 +00:00
});
2017-04-07 08:09:59 +00:00
case 'GET_FILE_SIZE':
return void getFileSize(Env, msg[2], function (e, size) {
warn(e, msg[2]);
Respond(e, size);
});
2017-05-11 14:12:44 +00:00
case 'UPDATE_LIMITS':
2017-05-12 12:19:36 +00:00
return void updateLimits(config, safeKey, function (e, limit) {
if (e) {
warn(e, limit);
return void Respond(e);
}
2017-05-11 14:12:44 +00:00
Respond(void 0, limit);
});
case 'GET_LIMIT':
return void getLimit(Env, safeKey, function (e, limit) {
if (e) {
warn(e, limit);
return void Respond(e);
}
2017-05-11 14:12:44 +00:00
Respond(void 0, limit);
2017-04-28 09:46:13 +00:00
});
2017-04-21 12:51:00 +00:00
case 'GET_MULTIPLE_FILE_SIZE':
2017-05-16 16:08:59 +00:00
return void getMultipleFileSize(Env, msg[1], function (e, dict) {
if (e) {
warn(e, dict);
return void Respond(e);
}
2017-04-21 12:51:00 +00:00
Respond(void 0, dict);
});
2017-04-28 15:11:50 +00:00
// restricted to privileged users...
2017-04-28 15:11:50 +00:00
case 'UPLOAD':
if (!privileged) { return deny(); }
2017-05-16 16:08:59 +00:00
return void upload(Env, safeKey, msg[1], function (e, len) {
warn(e, len);
2017-05-04 09:36:56 +00:00
Respond(e, len);
});
case 'UPLOAD_STATUS':
if (!privileged) { return deny(); }
var filesize = msg[1];
2017-05-16 16:08:59 +00:00
return void upload_status(Env, safeKey, msg[1], function (e, yes) {
if (!e && !yes) {
// no pending uploads, set the new size
var user = beginSession(Sessions, safeKey);
user.pendingUploadSize = filesize;
user.currentUploadSize = 0;
}
Respond(e, yes);
2017-05-04 09:36:56 +00:00
});
case 'UPLOAD_COMPLETE':
if (!privileged) { return deny(); }
2017-05-16 16:08:59 +00:00
return void upload_complete(Env, safeKey, function (e, hash) {
warn(e, hash);
2017-05-04 09:36:56 +00:00
Respond(e, hash);
2017-04-28 15:11:50 +00:00
});
2017-05-04 09:36:56 +00:00
case 'UPLOAD_CANCEL':
if (!privileged) { return deny(); }
2017-05-16 16:08:59 +00:00
return void upload_cancel(Env, safeKey, function (e) {
warn(e);
2017-04-28 15:11:50 +00:00
Respond(e);
});
default:
return void Respond('UNSUPPORTED_RPC_CALL', msg);
}
};
// reject uploads unless explicitly enabled
if (config.enableUploads !== true) {
return void handleMessage(false);
}
// restrict upload capability unless explicitly disabled
if (config.restrictUploads === false) {
return void handleMessage(true);
}
// if session has not been authenticated, do so
var session = beginSession(Sessions, safeKey);
if (typeof(session.privilege) !== 'boolean') {
return void isPrivilegedUser(publicKey, function (yes) {
session.privilege = yes;
handleMessage(yes);
});
}
// if authenticated, proceed
handleMessage(session.privilege);
};
2017-05-11 14:12:44 +00:00
var updateLimitDaily = function () {
2017-05-12 12:19:36 +00:00
updateLimits(config, undefined, function (e) {
2017-05-11 14:12:44 +00:00
if (e) { console.error('Error updating the storage limits', e); }
});
};
updateLimitDaily();
setInterval(updateLimitDaily, 24*3600*1000);
2017-04-03 17:24:57 +00:00
Store.create({
filePath: pinPath,
2017-04-03 17:24:57 +00:00
}, function (s) {
2017-05-16 15:07:53 +00:00
Env.pinStore = s;
safeMkdir(blobPath, function (e) {
if (e) { throw e; }
safeMkdir(blobStagingPath, function (e) {
if (e) { throw e; }
cb(void 0, rpc);
// expire old sessions once per minute
setInterval(function () {
expireSessions(Sessions);
}, 60000);
2017-04-28 15:11:50 +00:00
});
});
2017-04-03 17:24:57 +00:00
});
};