You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
397 lines
11 KiB
397 lines
11 KiB
// core-node.js
|
|
// Copyright (C) 2022 DTP Technologies, LLC
|
|
// License: Apache-2.0
|
|
|
|
'use strict';
|
|
|
|
const uuidv4 = require('uuid').v4;
|
|
const fetch = require('node-fetch'); // jshint ignore:line
|
|
|
|
const mongoose = require('mongoose');
|
|
|
|
const CoreNode = mongoose.model('CoreNode');
|
|
const CoreUser = mongoose.model('CoreUser');
|
|
const CoreNodeRequest = mongoose.model('CoreNodeRequest');
|
|
|
|
const passport = require('passport');
|
|
const OAuth2Strategy = require('passport-oauth2');
|
|
|
|
const { SiteService, SiteError } = require('../../lib/site-lib');
|
|
|
|
class CoreAddress {
|
|
|
|
constructor (host, port) {
|
|
this.host = host;
|
|
this.port = port;
|
|
}
|
|
|
|
parse (host) {
|
|
const tokens = host.split(':');
|
|
this.host = tokens[0];
|
|
if (tokens[1]) {
|
|
this.port = parseInt(tokens[1], 10);
|
|
} else {
|
|
this.port = 443;
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
class CoreNodeService extends SiteService {
|
|
|
|
constructor (dtp) {
|
|
super(dtp, module.exports);
|
|
this.populateCoreUser = [
|
|
{
|
|
path: 'core'
|
|
},
|
|
];
|
|
}
|
|
|
|
async start ( ) {
|
|
const cores = await this.getConnectedCores(null, true);
|
|
cores.forEach((core) => this.registerPassportCoreOAuth2(core));
|
|
}
|
|
|
|
async attachExpressRoutes (router) {
|
|
const cores = await this.getConnectedCores(null, true);
|
|
cores.forEach((core) => {
|
|
const coreAuthStrategyName = this.getCoreAuthStrategyName(core);
|
|
const coreAuthUri = `/core/${core._id}`;
|
|
const coreAuthCallbackUri = `/core/${core._id}/callback`;
|
|
|
|
this.log.info('attach Core Auth route', {
|
|
coreId: core._id,
|
|
name: core.meta.name,
|
|
strategy: coreAuthStrategyName,
|
|
auth: coreAuthUri,
|
|
callback: coreAuthCallbackUri,
|
|
});
|
|
router.get(
|
|
coreAuthUri,
|
|
(req, res, next) => {
|
|
this.log.debug('Core auth request', { coreAuthStrategyName, clientId: core.oauth.clientId });
|
|
return next();
|
|
},
|
|
passport.authenticate(coreAuthStrategyName),
|
|
);
|
|
|
|
router.get(
|
|
coreAuthCallbackUri,
|
|
(req, res, next) => {
|
|
this.log.debug('Core auth callback', { strategy: coreAuthStrategyName });
|
|
return next();
|
|
},
|
|
passport.authenticate(coreAuthStrategyName, { failureRedirect: '/' }),
|
|
async (req, res, next) => {
|
|
req.session.userType = 'Core';
|
|
req.session.coreId = core._id;
|
|
req.login(req.user, (error) => {
|
|
if (error) {
|
|
return next(error);
|
|
}
|
|
req.session.userType = 'Core';
|
|
req.session.coreId = core._id;
|
|
return res.redirect('/');
|
|
});
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
registerPassportCoreOAuth2 (core) {
|
|
const { coreNode: coreNodeService } = this.dtp.services;
|
|
const AUTH_SCHEME = coreNodeService.getCoreRequestScheme();
|
|
|
|
const coreAuthStrategyName = this.getCoreAuthStrategyName(core);
|
|
const authorizationHost = `${core.address.host}:${core.address.port}`;
|
|
const authorizationURL = `${AUTH_SCHEME}://${authorizationHost}/oauth2/authorize`;
|
|
const tokenURL = `${AUTH_SCHEME}://${authorizationHost}/oauth2/token`;
|
|
const callbackURL = `${AUTH_SCHEME}://${process.env.DTP_SITE_DOMAIN}/auth/core/${core._id}/callback`;
|
|
|
|
const coreAuthStrategy = new OAuth2Strategy(
|
|
{
|
|
authorizationURL,
|
|
tokenURL,
|
|
clientID: core.oauth.clientId.toString(),
|
|
clientSecret: core.oauth.clientSecret,
|
|
callbackURL,
|
|
},
|
|
async (accessToken, refreshToken, params, profile, cb) => {
|
|
const NOW = new Date();
|
|
try {
|
|
const coreUserId = mongoose.Types.ObjectId(params.coreUserId);
|
|
const user = await CoreUser.findOneAndUpdate(
|
|
{
|
|
core: core._id,
|
|
coreUserId,
|
|
},
|
|
{
|
|
$setOnInsert: {
|
|
created: NOW,
|
|
core: core._id,
|
|
coreUserId,
|
|
flags: {
|
|
isAdmin: false,
|
|
isModerator: false,
|
|
},
|
|
permissions: {
|
|
canLogin: true,
|
|
canChat: true,
|
|
canComment: true,
|
|
canReport: true,
|
|
},
|
|
optIn: {
|
|
system: true,
|
|
marketing: false,
|
|
},
|
|
theme: 'dtp-light',
|
|
stats: {
|
|
uniqueVisitCount: 0,
|
|
totalVisitCount: 0,
|
|
},
|
|
},
|
|
$set: {
|
|
updated: NOW,
|
|
username: params.username,
|
|
username_lc: params.username_lc,
|
|
displayName: params.displayName,
|
|
bio: params.bio,
|
|
},
|
|
},
|
|
{
|
|
upsert: true,
|
|
new: true,
|
|
},
|
|
);
|
|
return cb(null, user.toObject());
|
|
} catch (error) {
|
|
return cb(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
this.log.info('registering Core auth strategy', {
|
|
name: coreAuthStrategyName,
|
|
host: core.address.host,
|
|
port: core.address.port,
|
|
clientID: core.oauth.clientId.toString(),
|
|
callbackURL,
|
|
});
|
|
passport.use(coreAuthStrategyName, coreAuthStrategy);
|
|
}
|
|
|
|
parseCoreAddress (host) {
|
|
const address = new CoreAddress();
|
|
return address.parse(host);
|
|
}
|
|
|
|
async getCoreByAddress (address) {
|
|
const core = await CoreNode
|
|
.findOne({
|
|
'address.host': address.host,
|
|
'address.port': address.port,
|
|
})
|
|
.lean();
|
|
return core;
|
|
}
|
|
|
|
/**
|
|
* First ensures that a record exists in the local database for the Core node.
|
|
* Then, calls the node's info services to resolve more metadata about the
|
|
* node, it's operation, policies, and available services.
|
|
*
|
|
* @param {String} host hostname and optional port number of Core node to be
|
|
* resolved.
|
|
* @returns CoreNode document describing the Core node.
|
|
*/
|
|
async resolveCore (host) {
|
|
const NOW = new Date();
|
|
this.log.info('resolving Core node', { host });
|
|
|
|
const address = this.parseCoreAddress(host);
|
|
let core = await this.getCoreByAddress(address);
|
|
if (!core) {
|
|
core = new CoreNode();
|
|
core.created = NOW;
|
|
core.address.host = address.host;
|
|
core.address.port = address.port;
|
|
await core.save();
|
|
core = core.toObject();
|
|
}
|
|
|
|
const txSite = await this.sendRequest(core, {
|
|
method: 'GET',
|
|
url: '/core/info/site',
|
|
});
|
|
const txPackage = await this.sendRequest(core, {
|
|
method: 'GET',
|
|
url: '/core/info/package',
|
|
});
|
|
|
|
await CoreNode.updateOne(
|
|
{ _id: core._id },
|
|
{
|
|
$set: {
|
|
updated: NOW,
|
|
'meta.name': txSite.response.site.name,
|
|
'meta.description': txSite.response.site.description,
|
|
'meta.domain': txSite.response.site.domain,
|
|
'meta.domainKey': txSite.response.site.domainKey,
|
|
'meta.version': txPackage.response.package.version,
|
|
'meta.admin': txSite.response.site.admin,
|
|
'meta.supportEmail': txSite.response.site.supportEmail,
|
|
},
|
|
},
|
|
);
|
|
|
|
core = await CoreNode.findOne({ _id: core._id }).lean();
|
|
this.log.info('resolved Core node', { core });
|
|
return core;
|
|
}
|
|
|
|
async getConstellationStats ( ) {
|
|
const connectedCount = await CoreNode.countDocuments({ 'flags.isConnected': true });
|
|
const pendingCount = await CoreNode.countDocuments({ 'flags.isConnected': false });
|
|
const potentialReach = Math.round(Math.random() * 6000000);
|
|
return { connectedCount, pendingCount, potentialReach };
|
|
}
|
|
|
|
async broadcast (request) {
|
|
const results = [ ];
|
|
await CoreNode
|
|
.find()
|
|
.cursor()
|
|
.eachAsync(async (core) => {
|
|
try {
|
|
const response = await this.sendRequest(core, request);
|
|
results.push({ coreId: core._id, request, response });
|
|
} catch (error) {
|
|
this.log.error('failed to send Core Node request', { core: core._id, request: request.url, error });
|
|
}
|
|
});
|
|
return results;
|
|
}
|
|
|
|
getCoreAuthStrategyName (core) {
|
|
return `dtp:${core.meta.domainKey}`;
|
|
}
|
|
|
|
getCoreRequestScheme ( ) {
|
|
return process.env.DTP_CORE_AUTH_SCHEME || 'https';
|
|
}
|
|
|
|
getCoreRequestUrl (core, requestUrl) {
|
|
const coreScheme = this.getCoreRequestScheme();
|
|
return `${coreScheme}://${core.address.host}:${core.address.port}${requestUrl}`;
|
|
}
|
|
|
|
async sendRequest (core, request) {
|
|
try {
|
|
const req = new CoreNodeRequest();
|
|
const options = {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
};
|
|
|
|
req.created = new Date();
|
|
req.core = core._id;
|
|
if (request.tokenized) {
|
|
req.token = {
|
|
value: uuidv4(),
|
|
claimed: false,
|
|
};
|
|
options.headers['X-DTP-Core-Token'] = req.token.value;
|
|
}
|
|
options.method = req.method = request.method || 'GET';
|
|
req.url = request.url;
|
|
await req.save();
|
|
|
|
if (request.body) {
|
|
options.body = JSON.stringify(request.body);
|
|
}
|
|
|
|
this.log.info('sending Core node request', { request: req });
|
|
const requestUrl = this.getCoreRequestUrl(core, request.url);
|
|
const response = await fetch(requestUrl, options);
|
|
if (!response.ok) {
|
|
throw new SiteError(response.status, response.statusText);
|
|
}
|
|
|
|
const json = await response.json();
|
|
|
|
/*
|
|
* capture a little inline health monitoring data, which can be used to
|
|
* generate health alerts.
|
|
*/
|
|
const DONE = new Date();
|
|
const ELAPSED = DONE.valueOf() - req.created.valueOf();
|
|
await CoreNodeRequest.updateOne(
|
|
{ _id: req._id },
|
|
{
|
|
$set: {
|
|
'response.received': DONE,
|
|
'response.elapsed': ELAPSED,
|
|
'response.success': json.success,
|
|
},
|
|
},
|
|
);
|
|
|
|
this.log.info('Core node request complete', { request: req });
|
|
return { request: req.toObject(), response: json };
|
|
} catch (error) {
|
|
this.log.error('failed to send Core Node request', { core: core._id, request: request.url, error });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async setCoreOAuth2Credentials (core, credentials) {
|
|
const { client } = credentials;
|
|
this.log.info('updating Core Connect credentials', { core, client });
|
|
await CoreNode.updateOne(
|
|
{ _id: core._id },
|
|
{
|
|
$set: {
|
|
'flags.isConnected': true,
|
|
'oauth.clientId': client._id,
|
|
'oauth.clientSecret': client.secret,
|
|
'oauth.scopes': client.scopes,
|
|
'oauth.redirectUri': client.redirectUri,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
async getConnectedCores (pagination, withOAuth = false) {
|
|
let q = CoreNode.find({ 'flags.isConnected': true });
|
|
if (!withOAuth) {
|
|
q = q.select('-oauth');
|
|
}
|
|
|
|
q = q.sort({ 'meta.domain': 1 });
|
|
if (pagination) {
|
|
q = q
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp);
|
|
}
|
|
|
|
const cores = await q.lean();
|
|
return cores;
|
|
}
|
|
|
|
async getUserByLocalId (userId) {
|
|
const user = await CoreUser
|
|
.findOne({ _id: userId })
|
|
.select('+flags +permissions +optIn')
|
|
.populate(this.populateCoreUser)
|
|
.lean();
|
|
return user;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
slug: 'core-node',
|
|
name: 'coreNode',
|
|
create: (dtp) => { return new CoreNodeService(dtp); },
|
|
};
|