This commit is contained in:
@@ -35,8 +35,9 @@ if (FEDERATION_DOMAIN) {
|
|||||||
publicKeyPem = crypto.createPublicKey(currentPrivateKey).export({ type: 'spki', format: 'pem' });
|
publicKeyPem = crypto.createPublicKey(currentPrivateKey).export({ type: 'spki', format: 'pem' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance discovery cache (domain → { baseUrl, publicKey })
|
// Instance discovery cache (domain → { baseUrl, publicKey, cachedAt })
|
||||||
const discoveryCache = new Map();
|
const discoveryCache = new Map();
|
||||||
|
const DISCOVERY_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get this instance's federation domain.
|
* Get this instance's federation domain.
|
||||||
@@ -101,19 +102,20 @@ export function verifyPayload(payload, signature, remotePublicKeyPem) {
|
|||||||
* @returns {Promise<{ baseUrl: string, publicKey: string }>}
|
* @returns {Promise<{ baseUrl: string, publicKey: string }>}
|
||||||
*/
|
*/
|
||||||
export async function discoverInstance(domain) {
|
export async function discoverInstance(domain) {
|
||||||
if (discoveryCache.has(domain)) {
|
const cached = discoveryCache.get(domain);
|
||||||
return discoveryCache.get(domain);
|
if (cached && (Date.now() - cached.cachedAt) < DISCOVERY_TTL_MS) {
|
||||||
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wellKnownUrl = `https://${domain}/.well-known/redlight`;
|
const wellKnownUrl = `https://${domain}/.well-known/redlight`;
|
||||||
|
const TIMEOUT_MS = 10_000; // 10 seconds
|
||||||
try {
|
try {
|
||||||
// Since we test locally, allow http fallback if the request fails (optional but good for testing)
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(wellKnownUrl);
|
response = await fetch(wellKnownUrl, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message.includes('fetch') && wellKnownUrl.startsWith('https://localhost')) {
|
if (e.message.includes('fetch') && wellKnownUrl.startsWith('https://localhost')) {
|
||||||
response = await fetch(`http://${domain}/.well-known/redlight`);
|
response = await fetch(`http://${domain}/.well-known/redlight`, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
||||||
} else throw e;
|
} else throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,17 +129,17 @@ export async function discoverInstance(domain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = `https://${domain}${data.federation_api || '/api/federation'}`;
|
const baseUrl = `https://${domain}${data.federation_api || '/api/federation'}`;
|
||||||
// Optionally handle local testing gracefully for baseUrl
|
|
||||||
const result = {
|
const result = {
|
||||||
baseUrl: baseUrl.replace('https://localhost', 'http://localhost'),
|
baseUrl: baseUrl.replace('https://localhost', 'http://localhost'),
|
||||||
publicKey: data.public_key
|
publicKey: data.public_key,
|
||||||
|
cachedAt: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
discoveryCache.set(domain, result);
|
discoveryCache.set(domain, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Federation discovery failed for ${domain}:`, error.message);
|
console.error(`Federation discovery failed for ${domain}:`, error.message);
|
||||||
throw new Error(`Could not discover Redlight instance at ${domain}`);
|
throw new Error(`Could not discover Redlight instance at ${domain}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export function initMailer() {
|
|||||||
port,
|
port,
|
||||||
secure: port === 465,
|
secure: port === 465,
|
||||||
auth: { user, pass },
|
auth: { user, pass },
|
||||||
|
connectionTimeout: 10_000, // 10 s to establish TCP connection
|
||||||
|
greetingTimeout: 10_000, // 10 s to receive SMTP greeting
|
||||||
|
socketTimeout: 15_000, // 15 s of inactivity before abort
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ SMTP mailer configured');
|
console.log('✅ SMTP mailer configured');
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ router.post('/invite', authenticateToken, async (req, res) => {
|
|||||||
'X-Federation-Origin': getFederationDomain(),
|
'X-Federation-Origin': getFederationDomain(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
|
signal: AbortSignal.timeout(15_000), // 15 second timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -192,17 +193,17 @@ router.post('/receive', async (req, res) => {
|
|||||||
} catch { /* column may not exist on very old installs */ }
|
} catch { /* column may not exist on very old installs */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send notification email (fire-and-forget, don't fail the request if mail fails)
|
// Send notification email (truly fire-and-forget – never blocks the response)
|
||||||
try {
|
if (targetUser.email) {
|
||||||
const appUrl = process.env.APP_URL || '';
|
const appUrl = process.env.APP_URL || '';
|
||||||
const inboxUrl = `${appUrl}/federation/inbox`;
|
const inboxUrl = `${appUrl}/federation/inbox`;
|
||||||
const appName = process.env.APP_NAME || 'Redlight';
|
const appName = process.env.APP_NAME || 'Redlight';
|
||||||
await sendFederationInviteEmail(
|
sendFederationInviteEmail(
|
||||||
targetUser.email, targetUser.name, from_user,
|
targetUser.email, targetUser.name, from_user,
|
||||||
room_name, message || null, inboxUrl, appName
|
room_name, message || null, inboxUrl, appName
|
||||||
);
|
).catch(mailErr => {
|
||||||
} catch (mailErr) {
|
|
||||||
console.warn('Federation invite mail failed (non-fatal):', mailErr.message);
|
console.warn('Federation invite mail failed (non-fatal):', mailErr.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user