mStream/modules/federation.js
2021-01-04 23:04:55 -05:00

210 lines
6.9 KiB
JavaScript

const jwt = require('jsonwebtoken');
const path = require('path');
const axios = require('axios');
const mkdirp = require('make-dir');
const fs = require('fs');
const express = require('express');
const sync = require('./sync');
const config = require('../src/state/config');
exports.setup = function (mstream, program) {
mstream.post('/federation/invite/exchange', (req, res) => {
try {
var decodedToken = jwt.verify(req.body.token, program.secret);
}catch(err) {
return res.status(500).json({ error: 'Token verification failed' });
}
if (!program.users[decodedToken.username]) {
return res.status(500).json({ error: 'User does not exist' });
}
if (!req.body.federationId) {
return res.status(500).json({ error: 'Missing Federation ID' });
}
// add server's federationID into the syncThing config
try {
sync.addDevice(req.body.federationId, decodedToken.vPaths);
}catch (err) {
return res.status(403).json({ error: err.message });
}
res.json({});
});
mstream.post('/federation/invite/accept', async (req, res) => {
if (!req.body.invite || !req.body.paths) {
return res.status(403).json({ error: 'Missing Input Params' });
}
var loadJson;
var decodedToken;
try {
decodedToken = jwt.decode(req.body.invite.trim());
// Validate directories
const xmlObj = sync.getXml();
const idCache = {};
const directoryCache = {};
xmlObj.configuration.folder.forEach(f => {
idCache[f['@_id']] = true;
directoryCache[f['@_path']] = true;
});
if (sync.getId() === decodedToken.federationId) {
throw new Error('Cannot use your own token');
}
Object.keys(req.body.paths).forEach(p => {
// paths includes value not in token OR folder ID already exists. remove it
if (!decodedToken.vPaths[p] || idCache[decodedToken.vPaths[p]]) {
delete req.body.paths[p];
return;
}
// Validate the new folder names
if (!/^([a-z0-9 _-]{1,})$/.test(req.body.paths[p])) {
throw new Error('Folder Name Cannot Contain Special Characters');
}
// Make sure folder name doesn't already exist
if (program.folders[req.body.paths[p]]) {
throw new Error('Folder Name Already Exists');
}
// Check if directory is in syncthing config
if (directoryCache[path.join(program.federation.folder, req.body.paths[p])]) {
throw new Error(`Directory ${p} is already federated. Choose another name`);
}
// Create directory if necessary
mkdirp.sync(path.join(program.federation.folder, req.body.paths[p]));
});
if (Object.keys(req.body.paths).length === 0) {
throw new Error('Folders already federated');
}
loadJson = JSON.parse(fs.readFileSync(config.configFile, 'utf8'));
}catch (err) {
return res.status(403).json({ error: err.message });
}
// Handle case where federationId is attached
if (decodedToken.for) {
if (decodedToken.for !== sync.getId()) {
return res.status(500).json({ error: 'This token is for different Federation ID' });
}
} else {
const newURL = new URL(decodedToken.url);
newURL.pathname = '/federation/invite/exchange';
// call server
try {
await axios({
method: 'post',
url: newURL.toString(),
headers: { 'accept': 'application/json' },
responseType: 'json',
data: { token: req.body.invite, federationId: sync.getId() }
});
}catch(err) {
return res.status(500).json({message: 'Invalid Token'});
}
}
try {
Object.keys(req.body.paths).forEach(p => {
// Add new vpaths to config file
loadJson.folders[req.body.paths[p]] = { root: path.join(program.federation.folder, req.body.paths[p]) }
program.folders[req.body.paths[p]] = { root: path.join(program.federation.folder, req.body.paths[p]) }
// add vpath to user permissions
loadJson.users[req.user.username].vpaths.push(req.body.paths[p]);
program.users[req.user.username].vpaths.push(req.body.paths[p]);
// Add to server
mstream.use(`/media/${req.body.paths[p]}/`, express.static(path.join(program.federation.folder, req.body.paths[p])));
// add directory to syncthing
sync.addFederatedDirectory(req.body.paths[p], decodedToken.vPaths[p], path.join(program.federation.folder, req.body.paths[p]), decodedToken.federationId);
});
// add user to syncthing
sync.addDevice(decodedToken.federationId, {});
// Save config file
fs.writeFileSync(config.configFile, JSON.stringify(loadJson, null, 2), 'utf8');
}catch (err) {
return res.status(403).json({ error: err.message });
}
res.json({success: true});
});
mstream.post('/federation/invite/generate', (req, res) => {
if (!program.federation || !program.federation.folder) {
return res.status(403).json({ error: 'Invites Disabled' });
}
if (!req.body.paths) {
return res.status(403).json({ error: 'Missing Input Params' });
}
// XOR
if ((!req.body.url && !req.body.federationId) && !(req.body.url && req.body.federationId)) {
return res.status(403).json({ error: 'Missing Input Params (or maybe too many?)' });
}
// Verify user has access to vpaths
if (!req.body.paths.every((currentValue) => {
return req.user.vpaths.includes(currentValue);
})) {
return res.status(403).json({ error: 'Invalid Input Params' });
}
const pathObject = {};
req.user.vpaths.forEach(path => {
pathObject[path] = sync.getPathId(path);
});
// Setup Token Data
const tokenData = {
invite: true,
federationId: sync.getId(),
vPaths: pathObject,
username: req.user.username
}
if (req.body.url) { tokenData.url = req.body.url; }
if (req.body.federationId) {
console.log(req.body.federationId );
console.log(sync.getId());
if (req.body.federationId === sync.getId()) {
return res.status(403).json({ error: 'Cannot generate an invite for yourself' });
}
tokenData.for = req.body.federationId;
// add ID to syncthing config
try {
sync.addDevice(req.body.federationId, pathObject);
} catch (err) {
return res.status(403).json({ error: 'Federation ID is incorrect length' });
}
}
const options = {};
req.body.expirationTimeInDays = Number(req.body.expirationTimeInDays);
if (req.body.expirationTimeInDays && Number.isInteger(req.body.expirationTimeInDays) && req.body.expirationTimeInDays > 0) {
options.expiresIn = `${req.body.expirationTimeInDays}d`;
}
res.json({ token: jwt.sign(tokenData, program.secret, options) });
});
mstream.get('/federation/stats', (req, res) => {
res.json(sync.getXml());
});
}