2019-03-30 00:03:53 +08:00
|
|
|
/* jshint esversion: 6 */
|
|
|
|
|
|
|
|
const nThen = require('nthen');
|
|
|
|
const Crypto = require('crypto');
|
2020-01-24 03:47:55 +08:00
|
|
|
const WriteQueue = require("./write-queue");
|
|
|
|
const BatchRead = require("./batch-read");
|
2020-02-04 03:20:05 +08:00
|
|
|
const RPC = require("./rpc");
|
2020-02-08 07:58:57 +08:00
|
|
|
const HK = require("./hk-util.js");
|
2020-02-17 23:30:44 +08:00
|
|
|
const Core = require("./commands/core");
|
2020-02-04 03:20:05 +08:00
|
|
|
|
2020-02-17 22:01:10 +08:00
|
|
|
const Store = require("./storage/file");
|
|
|
|
const BlobStore = require("./storage/blob");
|
2020-03-28 01:38:27 +08:00
|
|
|
const Workers = require("./workers/index");
|
2020-09-30 17:12:14 +08:00
|
|
|
//const Eviction = require("./eviction");
|
2020-02-17 22:01:10 +08:00
|
|
|
|
2020-02-08 07:58:57 +08:00
|
|
|
module.exports.create = function (config, cb) {
|
|
|
|
const Log = config.log;
|
2020-02-17 22:01:10 +08:00
|
|
|
var WARN = function (e, output) {
|
|
|
|
if (e && output) {
|
|
|
|
Log.warn(e, {
|
|
|
|
output: output,
|
|
|
|
message: String(e),
|
|
|
|
stack: new Error(e).stack,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
2019-04-17 21:28:29 +08:00
|
|
|
|
2019-04-18 16:36:49 +08:00
|
|
|
Log.silly('HK_LOADING', 'LOADING HISTORY_KEEPER MODULE');
|
2019-03-30 00:03:53 +08:00
|
|
|
|
2020-02-08 07:58:57 +08:00
|
|
|
const Env = {
|
|
|
|
Log: Log,
|
|
|
|
// store
|
|
|
|
id: Crypto.randomBytes(8).toString('hex'),
|
2019-07-04 17:04:28 +08:00
|
|
|
|
2020-09-30 17:12:14 +08:00
|
|
|
inactiveTime: config.inactiveTime,
|
|
|
|
archiveRetentionTime: config.archiveRetentionTime,
|
|
|
|
accountRetentionTime: config.accountRetentionTime,
|
|
|
|
|
2020-02-08 07:58:57 +08:00
|
|
|
metadata_cache: {},
|
|
|
|
channel_cache: {},
|
|
|
|
queueStorage: WriteQueue(),
|
2020-04-10 04:29:52 +08:00
|
|
|
queueDeletes: WriteQueue(),
|
2020-05-01 06:18:05 +08:00
|
|
|
queueValidation: WriteQueue(),
|
2020-02-28 04:00:31 +08:00
|
|
|
|
2020-02-08 07:58:57 +08:00
|
|
|
batchIndexReads: BatchRead("HK_GET_INDEX"),
|
2020-02-25 22:09:16 +08:00
|
|
|
batchMetadata: BatchRead('GET_METADATA'),
|
2020-02-28 04:00:31 +08:00
|
|
|
batchRegisteredUsers: BatchRead("GET_REGISTERED_USERS"),
|
|
|
|
batchDiskUsage: BatchRead('GET_DISK_USAGE'),
|
2020-03-25 23:39:14 +08:00
|
|
|
batchUserPins: BatchRead('LOAD_USER_PINS'),
|
2020-03-26 05:43:57 +08:00
|
|
|
batchTotalSize: BatchRead('GET_TOTAL_SIZE'),
|
2020-02-17 22:01:10 +08:00
|
|
|
|
|
|
|
//historyKeeper: config.historyKeeper,
|
|
|
|
intervals: config.intervals || {},
|
|
|
|
maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024),
|
2020-02-27 02:09:11 +08:00
|
|
|
premiumUploadSize: false, // overridden below...
|
2020-02-17 22:01:10 +08:00
|
|
|
Sessions: {},
|
|
|
|
paths: {},
|
|
|
|
//msgStore: config.store,
|
|
|
|
|
2020-02-25 22:09:16 +08:00
|
|
|
netfluxUsers: {},
|
|
|
|
|
2020-02-17 22:01:10 +08:00
|
|
|
pinStore: undefined,
|
|
|
|
pinnedPads: {},
|
|
|
|
pinsLoaded: false,
|
|
|
|
pendingPinInquiries: {},
|
|
|
|
pendingUnpins: {},
|
|
|
|
pinWorkers: 5,
|
|
|
|
|
|
|
|
limits: {},
|
|
|
|
admins: [],
|
|
|
|
WARN: WARN,
|
|
|
|
flushCache: config.flushCache,
|
|
|
|
adminEmail: config.adminEmail,
|
2020-02-29 03:24:53 +08:00
|
|
|
allowSubscriptions: config.allowSubscriptions === true,
|
|
|
|
blockDailyCheck: config.blockDailyCheck === true,
|
|
|
|
|
2020-03-04 22:59:17 +08:00
|
|
|
myDomain: config.myDomain,
|
2020-03-04 04:52:49 +08:00
|
|
|
mySubdomain: config.mySubdomain, // only exists for the accounts integration
|
2020-02-28 23:49:20 +08:00
|
|
|
customLimits: config.customLimits || {},
|
2020-02-17 22:01:10 +08:00
|
|
|
// FIXME this attribute isn't in the default conf
|
|
|
|
// but it is referenced in Quota
|
|
|
|
domain: config.domain
|
|
|
|
};
|
|
|
|
|
2020-02-27 02:09:11 +08:00
|
|
|
(function () {
|
|
|
|
var pes = config.premiumUploadSize;
|
|
|
|
if (!isNaN(pes) && pes >= Env.maxUploadSize) {
|
|
|
|
Env.premiumUploadSize = pes;
|
|
|
|
}
|
|
|
|
}());
|
|
|
|
|
2020-02-17 22:01:10 +08:00
|
|
|
var paths = Env.paths;
|
|
|
|
|
|
|
|
var keyOrDefaultString = function (key, def) {
|
|
|
|
return typeof(config[key]) === 'string'? config[key]: def;
|
2019-03-30 00:03:53 +08:00
|
|
|
};
|
|
|
|
|
2020-02-17 22:01:10 +08:00
|
|
|
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
|
|
|
|
paths.block = keyOrDefaultString('blockPath', './block');
|
|
|
|
paths.data = keyOrDefaultString('filePath', './datastore');
|
|
|
|
paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
|
|
|
|
paths.blob = keyOrDefaultString('blobPath', './blob');
|
|
|
|
|
2020-04-12 02:45:28 +08:00
|
|
|
Env.defaultStorageLimit = typeof(config.defaultStorageLimit) === 'number' && config.defaultStorageLimit >= 0?
|
2020-02-17 22:01:10 +08:00
|
|
|
config.defaultStorageLimit:
|
|
|
|
Core.DEFAULT_LIMIT;
|
|
|
|
|
|
|
|
try {
|
|
|
|
Env.admins = (config.adminKeys || []).map(function (k) {
|
|
|
|
k = k.replace(/\/+$/, '');
|
|
|
|
var s = k.split('/');
|
|
|
|
return s[s.length-1];
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Can't parse admin keys. Please update or fix your config.js file!");
|
|
|
|
}
|
|
|
|
|
|
|
|
config.historyKeeper = Env.historyKeeper = {
|
2020-02-08 07:58:57 +08:00
|
|
|
metadata_cache: Env.metadata_cache,
|
|
|
|
channel_cache: Env.channel_cache,
|
2019-09-04 17:51:33 +08:00
|
|
|
|
2020-02-08 07:58:57 +08:00
|
|
|
id: Env.id,
|
2020-02-04 03:20:05 +08:00
|
|
|
|
|
|
|
channelMessage: function (Server, channel, msgStruct) {
|
|
|
|
// netflux-server emits 'channelMessage' events whenever someone broadcasts to a channel
|
|
|
|
// historyKeeper stores these messages if the channel id indicates that they are
|
|
|
|
// a channel type with permanent history
|
2020-02-08 07:58:57 +08:00
|
|
|
HK.onChannelMessage(Env, Server, channel, msgStruct);
|
2020-02-03 23:03:43 +08:00
|
|
|
},
|
|
|
|
channelClose: function (channelName) {
|
2020-02-04 03:20:05 +08:00
|
|
|
// netflux-server emits 'channelClose' events whenever everyone leaves a channel
|
|
|
|
// we drop cached metadata and indexes at the same time
|
2020-02-08 07:58:57 +08:00
|
|
|
HK.dropChannel(Env, channelName);
|
2020-02-03 23:03:43 +08:00
|
|
|
},
|
2020-02-25 22:09:16 +08:00
|
|
|
channelOpen: function (Server, channelName, userId, wait) {
|
2020-02-18 00:31:00 +08:00
|
|
|
Env.channel_cache[channelName] = Env.channel_cache[channelName] || {};
|
2020-02-25 22:09:16 +08:00
|
|
|
|
|
|
|
var sendHKJoinMessage = function () {
|
|
|
|
Server.send(userId, [
|
|
|
|
0,
|
|
|
|
Env.id,
|
|
|
|
'JOIN',
|
|
|
|
channelName
|
|
|
|
]);
|
|
|
|
};
|
|
|
|
|
|
|
|
// a little backwards compatibility in case you don't have the latest server
|
|
|
|
// allow lists won't work unless you update, though
|
|
|
|
if (typeof(wait) !== 'function') { return void sendHKJoinMessage(); }
|
|
|
|
|
|
|
|
var next = wait();
|
|
|
|
var cb = function (err, info) {
|
|
|
|
next(err, info, sendHKJoinMessage);
|
|
|
|
};
|
|
|
|
|
|
|
|
// only conventional channels can be restricted
|
|
|
|
if ((channelName || "").length !== HK.STANDARD_CHANNEL_LENGTH) {
|
|
|
|
return void cb();
|
|
|
|
}
|
|
|
|
|
|
|
|
// gets and caches the metadata...
|
|
|
|
HK.getMetadata(Env, channelName, function (err, metadata) {
|
|
|
|
if (err) {
|
|
|
|
Log.error('HK_METADATA_ERR', {
|
|
|
|
channel: channelName,
|
|
|
|
error: err,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (!metadata || (metadata && !metadata.restricted)) {
|
|
|
|
// the channel doesn't have metadata, or it does and it's not restricted
|
|
|
|
// either way, let them join.
|
|
|
|
return void cb();
|
|
|
|
}
|
|
|
|
|
|
|
|
// this channel is restricted. verify that the user in question is in the allow list
|
|
|
|
|
|
|
|
// construct a definitive list (owners + allowed)
|
|
|
|
var allowed = HK.listAllowedUsers(metadata);
|
|
|
|
// and get the list of keys for which this user has already authenticated
|
|
|
|
var session = HK.getNetfluxSession(Env, userId);
|
|
|
|
|
|
|
|
if (HK.isUserSessionAllowed(allowed, session)) {
|
|
|
|
return void cb();
|
|
|
|
}
|
|
|
|
|
|
|
|
// otherwise they're not allowed.
|
|
|
|
// respond with a special error that includes the list of keys
|
|
|
|
// which would be allowed...
|
2020-02-25 22:48:41 +08:00
|
|
|
// FIXME RESTRICT bonus points if you hash the keys to limit data exposure
|
2020-02-25 22:09:16 +08:00
|
|
|
cb("ERESTRICTED", allowed);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
sessionClose: function (userId, reason) {
|
|
|
|
HK.closeNetfluxSession(Env, userId);
|
2020-03-27 05:11:43 +08:00
|
|
|
if (['BAD_MESSAGE', 'SEND_MESSAGE_FAIL_2'].indexOf(reason) !== -1) {
|
2020-02-25 22:09:16 +08:00
|
|
|
if (reason && reason.code === 'ECONNRESET') { return; }
|
|
|
|
return void Log.error('SESSION_CLOSE_WITH_ERROR', {
|
|
|
|
userId: userId,
|
|
|
|
reason: reason,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-02-25 22:48:41 +08:00
|
|
|
if (['SOCKET_CLOSED', 'SOCKET_ERROR'].indexOf(reason)) { return; }
|
2020-02-25 22:09:16 +08:00
|
|
|
Log.verbose('SESSION_CLOSE_ROUTINE', {
|
|
|
|
userId: userId,
|
|
|
|
reason: reason,
|
|
|
|
});
|
2020-02-03 23:03:43 +08:00
|
|
|
},
|
2020-02-04 04:47:41 +08:00
|
|
|
directMessage: function (Server, seq, userId, json) {
|
2020-02-04 03:20:05 +08:00
|
|
|
// netflux-server allows you to register an id with a handler
|
|
|
|
// this handler is invoked every time someone sends a message to that id
|
2020-02-08 07:58:57 +08:00
|
|
|
HK.onDirectMessage(Env, Server, seq, userId, json);
|
2020-02-03 23:03:43 +08:00
|
|
|
},
|
2019-03-30 00:03:53 +08:00
|
|
|
};
|
2020-02-04 03:20:05 +08:00
|
|
|
|
2020-02-08 07:58:57 +08:00
|
|
|
Log.verbose('HK_ID', 'History keeper ID: ' + Env.id);
|
|
|
|
|
|
|
|
nThen(function (w) {
|
2020-02-17 22:01:10 +08:00
|
|
|
// create a pin store
|
|
|
|
Store.create({
|
|
|
|
filePath: pinPath,
|
2020-03-25 01:31:03 +08:00
|
|
|
}, w(function (err, s) {
|
|
|
|
if (err) { throw err; }
|
2020-02-17 22:01:10 +08:00
|
|
|
Env.pinStore = s;
|
|
|
|
}));
|
|
|
|
|
|
|
|
// create a channel store
|
2020-03-25 01:31:03 +08:00
|
|
|
Store.create(config, w(function (err, _store) {
|
|
|
|
if (err) { throw err; }
|
2020-02-08 07:58:57 +08:00
|
|
|
config.store = _store;
|
2020-02-17 22:01:10 +08:00
|
|
|
Env.msgStore = _store; // API used by rpc
|
|
|
|
Env.store = _store; // API used by historyKeeper
|
|
|
|
}));
|
|
|
|
|
|
|
|
// create a blob store
|
|
|
|
BlobStore.create({
|
|
|
|
blobPath: config.blobPath,
|
|
|
|
blobStagingPath: config.blobStagingPath,
|
|
|
|
archivePath: config.archivePath,
|
|
|
|
getSession: function (safeKey) {
|
2020-02-17 23:30:44 +08:00
|
|
|
return Core.getSession(Env.Sessions, safeKey);
|
2020-02-17 22:01:10 +08:00
|
|
|
},
|
|
|
|
}, w(function (err, blob) {
|
|
|
|
if (err) { throw new Error(err); }
|
|
|
|
Env.blobStore = blob;
|
2020-02-08 07:58:57 +08:00
|
|
|
}));
|
2020-03-19 22:46:18 +08:00
|
|
|
}).nThen(function (w) {
|
2020-03-28 01:38:27 +08:00
|
|
|
Workers.initialize(Env, {
|
2020-03-26 05:43:57 +08:00
|
|
|
blobPath: config.blobPath,
|
|
|
|
blobStagingPath: config.blobStagingPath,
|
2020-03-28 02:38:58 +08:00
|
|
|
taskPath: config.taskPath,
|
2020-03-25 23:39:14 +08:00
|
|
|
pinPath: pinPath,
|
2020-03-19 22:46:18 +08:00
|
|
|
filePath: config.filePath,
|
|
|
|
archivePath: config.archivePath,
|
|
|
|
channelExpirationMs: config.channelExpirationMs,
|
|
|
|
verbose: config.verbose,
|
|
|
|
openFileLimit: config.openFileLimit,
|
2020-04-17 01:53:45 +08:00
|
|
|
|
2020-09-30 17:12:14 +08:00
|
|
|
inactiveTime: config.inactiveTime,
|
|
|
|
archiveRetentionTime: config.archiveRetentionTime,
|
|
|
|
accountRetentionTime: config.accountRetentionTime,
|
|
|
|
|
2020-04-17 01:53:45 +08:00
|
|
|
maxWorkers: config.maxWorkers,
|
2020-03-25 05:43:15 +08:00
|
|
|
}, w(function (err) {
|
2020-03-19 22:46:18 +08:00
|
|
|
if (err) {
|
|
|
|
throw new Error(err);
|
|
|
|
}
|
|
|
|
}));
|
2020-04-17 23:43:54 +08:00
|
|
|
}).nThen(function () {
|
2020-03-28 02:38:58 +08:00
|
|
|
config.intervals = config.intervals || {};
|
2020-09-30 17:12:14 +08:00
|
|
|
if (config.disableIntegratedTasks) { return; }
|
2020-03-28 02:38:58 +08:00
|
|
|
|
|
|
|
var tasks_running;
|
|
|
|
config.intervals.taskExpiration = setInterval(function () {
|
|
|
|
if (tasks_running) { return; }
|
|
|
|
tasks_running = true;
|
|
|
|
Env.runTasks(function (err) {
|
|
|
|
if (err) {
|
|
|
|
Log.error('TASK_RUNNER_ERR', err);
|
|
|
|
}
|
|
|
|
tasks_running = false;
|
|
|
|
});
|
|
|
|
}, 1000 * 60 * 5); // run every five minutes
|
2020-09-30 17:12:14 +08:00
|
|
|
}).nThen(function () {
|
|
|
|
if (config.disableIntegratedEviction) { return; }
|
|
|
|
const ONE_DAY = 24 * 1000 * 60 * 60;
|
|
|
|
// setting the time of the last eviction to "now"
|
|
|
|
// effectively makes it so that we'll start evicting after the server
|
|
|
|
// has been up for at least one day
|
|
|
|
var last_eviction = +new Date();
|
|
|
|
|
|
|
|
var active = false;
|
|
|
|
config.intervals.eviction = setInterval(function () {
|
|
|
|
if (active) { return; }
|
|
|
|
var now = +new Date();
|
|
|
|
// evict inactive data once per day
|
|
|
|
if (last_eviction && (now - ONE_DAY) < last_eviction) { return; }
|
|
|
|
active = true;
|
|
|
|
Env.evictInactive(function (err) {
|
|
|
|
if (err) {
|
|
|
|
// NO_INACTIVE_TIME
|
|
|
|
Log.error('EVICT_INACTIVE_MAIN_ERROR', err);
|
|
|
|
}
|
|
|
|
active = false;
|
|
|
|
last_eviction = now;
|
|
|
|
});
|
|
|
|
}, 60 * 1000);
|
2020-02-08 07:58:57 +08:00
|
|
|
}).nThen(function () {
|
2020-02-17 22:01:10 +08:00
|
|
|
RPC.create(Env, function (err, _rpc) {
|
2020-02-08 07:58:57 +08:00
|
|
|
if (err) { throw err; }
|
|
|
|
|
|
|
|
Env.rpc = _rpc;
|
|
|
|
cb(void 0, config.historyKeeper);
|
|
|
|
});
|
2020-02-04 03:20:05 +08:00
|
|
|
});
|
2019-03-30 00:03:53 +08:00
|
|
|
};
|