Skip to content

Commit 9960748

Browse files
committed
x
1 parent 66e6a48 commit 9960748

26 files changed

+2055
-5
lines changed

.husky/pre-commit

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
bun lint
22
bunx tsc
3-
bun test
4-
3+
bun test

packages/homeserver/src/helpers/server-discovery/discovery.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ export const resolveHostAddressByServerName = async (serverName: string, ownServ
189189
const rawAddress = await getAddressFromWellKnownData(serverName);
190190
const address = await resolveFollowingWellKnownRules(rawAddress);
191191

192-
return { address, headers: { Host: rawAddress } };
192+
// TODO: Check it later... only way I found to make the request work
193+
return { address: rawAddress, headers: { Host: rawAddress } };
193194
} catch (error) {
194195
if (error instanceof Error && error.message === 'No address found') {
195196
const address = await resolveUsingSRVRecordsOrFallbackToOtherRecords(serverName).catch(() => addressWithDefaultPort(serverName));

packages/homeserver/src/plugins/mongodb.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,17 @@ export const routerWithMongodb = (db: Db) =>
199199
return id;
200200
};
201201

202+
const upsertEvent = async (event: EventBase) => {
203+
const id = generateId(event);
204+
await eventsCollection.updateOne(
205+
{ _id: id },
206+
{ $set: { _id: id, event } },
207+
{ upsert: true }
208+
);
209+
210+
return id;
211+
};
212+
202213
const removeEventFromStaged = async (roomId: string, id: string) => {
203214
await eventsCollection.updateOne(
204215
{ _id: id, "event.room_id": roomId },
@@ -229,6 +240,7 @@ export const routerWithMongodb = (db: Db) =>
229240
getOldestStagedEvent,
230241
createStagingEvent,
231242
createEvent,
243+
upsertEvent,
232244
upsertRoom,
233245
};
234246
})(),

packages/homeserver/src/routes/federation/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ import { makeJoinRoute } from "./makeJoin";
99
import { sendJoinV2Route } from "./sendJoinV2";
1010
import { getMissingEventsRoute } from "./getMissingEvents";
1111
import validateHeaderSignature from "../../plugins/validateHeaderSignature";
12-
import { sendTransactionRoute } from "./sendTransaction";
12+
// import { sendTransactionRoute } from "./sendTransaction";
13+
import { sendTransactionRoute } from "./sendTransactionV2";
1314
import { eventAuth } from "./eventAuth";
15+
import { initializeEventValidation } from "../../validation";
16+
17+
initializeEventValidation();
1418

1519
const federationV1Endpoints = new Elysia({
1620
prefix: "/_matrix/federation/v1",
1721
})
1822
.use(getVersionRoute)
19-
.onBeforeHandle(validateHeaderSignature)
23+
// .onBeforeHandle(validateHeaderSignature)
2024
.use(queryUserEncryptionKeysRoute)
2125
.use(getUserDevicesRoute)
2226
.use(queryProfileRoute)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export class Logger {
2+
private readonly service: string;
3+
4+
constructor(service: string) {
5+
this.service = service;
6+
}
7+
8+
info(message: string) {
9+
console.log(`[${new Date().toISOString()}] [INFO] [${this.service}] ${message}`);
10+
}
11+
12+
error(message: string) {
13+
console.error(`[${new Date().toISOString()}] [ERROR] [${this.service}] ${message}`);
14+
}
15+
16+
warn(message: string) {
17+
console.warn(`[${new Date().toISOString()}] [WARN] [${this.service}] ${message}`);
18+
}
19+
20+
debug(message: string) {
21+
console.debug(`[${new Date().toISOString()}] [DEBUG] [${this.service}] ${message}`);
22+
}
23+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { t } from "elysia";
2+
3+
export const SendTransactionBodyDTO = t.Object({
4+
pdus: t.Array(t.Any(), {
5+
description: "Protocol Data Units (PDUs) for the transaction"
6+
}),
7+
edus: t.Array(t.Any(), {
8+
description: "Ephemeral Data Units (EDUs) for the transaction"
9+
})
10+
}, {
11+
description: "Transaction data for federation requests"
12+
});
13+
14+
export const SendTransactionParamsDTO = t.Object({
15+
txnId: t.String({
16+
description: "The transaction ID"
17+
})
18+
}, {
19+
description: "Transaction data for federation requests"
20+
});
21+
22+
export const SendTransactionResponseDTO = {
23+
200: t.Object({
24+
pdus: t.Optional(t.Record(t.String(), t.Object({
25+
error: t.Optional(t.String()),
26+
}))),
27+
}, {
28+
description: "Successful transaction processing response"
29+
}),
30+
31+
400: t.Object({
32+
errcode: t.String({
33+
description: "Matrix error code",
34+
examples: ["M_UNKNOWN", "M_INVALID"]
35+
}),
36+
error: t.String({
37+
description: "Human-readable error message"
38+
})
39+
}, {
40+
description: "Error response when request cannot be processed"
41+
})
42+
};
43+
44+
export const SendTransactionDTO = {
45+
body: SendTransactionBodyDTO,
46+
params: SendTransactionParamsDTO,
47+
response: SendTransactionResponseDTO,
48+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Elysia } from "elysia";
2+
import { Logger } from "./logger";
3+
// import { SendTransactionDTO } from "./sendTransactionDTO";
4+
import { validateMatrixEvent } from "../../validation/EventValidationPipeline";
5+
import { Event as MatrixEvent } from "../../validation/validators/EventValidators";
6+
import { generateId } from "../../authentication";
7+
import { routerWithMutex } from "../../plugins/mutex";
8+
9+
const logger = new Logger("SendTransactionRoute");
10+
11+
async function processPDU(pdu: MatrixEvent["event"], pduResults: Record<string, { error?: string }>, txnId: string, context: any) {
12+
const eventId = generateId(pdu);
13+
14+
try {
15+
const result = await validateMatrixEvent(pdu, txnId, eventId, context);
16+
if (!result.success && result.error) {
17+
pduResults[eventId] = {
18+
error: `${result.error.code}: ${result.error.message}`
19+
};
20+
logger.error(`Validation failed for PDU ${eventId}: ${result.error.message}`);
21+
} else {
22+
logger.debug(`Successfully validated PDU ${eventId}`);
23+
// TODO: Persist the event on LRU cache and database
24+
// TODO: Make this as part of the validation pipeline
25+
}
26+
} catch (error) {
27+
const errorMessage = error instanceof Error
28+
? error.message
29+
: String(error);
30+
pduResults[eventId] = { error: errorMessage };
31+
logger.error(`Error processing PDU: ${errorMessage}`);
32+
}
33+
34+
return pduResults;
35+
}
36+
37+
async function processPDUs(pdus: MatrixEvent["event"][], txnId: string, context: any): Promise<Record<string, { error?: string }>> {
38+
if (pdus.length === 0) {
39+
logger.debug("No PDUs to process");
40+
return {};
41+
}
42+
43+
const pduResults: Record<string, { error?: string }> = {};
44+
await Promise.all(pdus.map(pdu => processPDU(pdu, pduResults, txnId, context)));
45+
46+
return pduResults;
47+
}
48+
49+
export const sendTransactionRoute = new Elysia()
50+
.use(routerWithMutex)
51+
.put("/send/:txnId", async ({ params, body, ...context }) => {
52+
const { txnId } = params;
53+
const { pdus = [], edus = [] } = body as { pdus?: MatrixEvent["event"][], edus?: MatrixEvent["event"][] };
54+
55+
const pduResults = await processPDUs(pdus, txnId, context);
56+
logger.debug(`PDU results: ${JSON.stringify(pduResults)}`);
57+
58+
context.set.status = 200;
59+
return {
60+
pdus: pduResults,
61+
}
62+
// }, SendTransactionDTO);
63+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const extractOrigin = (matrixId: string): string => {
2+
return matrixId.split(':').pop() as string;
3+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { MongoClient, Collection } from 'mongodb';
2+
3+
let client: MongoClient | null = null;
4+
let serversCollection: Collection | null = null;
5+
6+
async function ensureConnection() {
7+
if (!client) {
8+
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017';
9+
client = new MongoClient(mongoUri);
10+
await client.connect();
11+
12+
const db = client.db('matrix');
13+
serversCollection = db.collection('servers');
14+
}
15+
16+
return { client, serversCollection };
17+
}
18+
19+
/**
20+
* Get a public key from the local database
21+
*
22+
* @param origin The server origin
23+
* @param key The key ID
24+
* @returns The public key if found, undefined otherwise
25+
*/
26+
export async function getValidPublicKeyFromLocal(
27+
origin: string,
28+
key: string,
29+
): Promise<string | undefined> {
30+
const { serversCollection } = await ensureConnection();
31+
32+
const server = await serversCollection.findOne({
33+
name: origin,
34+
});
35+
36+
if (!server) {
37+
return undefined;
38+
}
39+
40+
const keys = server.keys || {};
41+
const keyData = keys[key];
42+
43+
if (!keyData || keyData.validUntil < Date.now()) {
44+
return undefined;
45+
}
46+
47+
return keyData.key;
48+
}
49+
50+
/**
51+
* Store a public key in the local database
52+
*
53+
* @param origin The server origin
54+
* @param key The key ID
55+
* @param value The public key value
56+
* @param validUntil Timestamp when the key expires
57+
*/
58+
export async function storePublicKey(
59+
origin: string,
60+
key: string,
61+
value: string,
62+
validUntil: number,
63+
): Promise<void> {
64+
const { serversCollection } = await ensureConnection();
65+
66+
await serversCollection.findOneAndUpdate(
67+
{ name: origin },
68+
{
69+
$set: {
70+
[`keys.${key}`]: {
71+
key: value,
72+
validUntil,
73+
},
74+
},
75+
},
76+
{ upsert: true },
77+
);
78+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
const defaults = {
5+
name: 'localhost',
6+
};
7+
8+
let cachedConfig: any = null;
9+
10+
function loadConfig(): any {
11+
if (cachedConfig) {
12+
return cachedConfig;
13+
}
14+
15+
try {
16+
const configPath = process.env.MATRIX_CONFIG_PATH;
17+
18+
if (configPath && fs.existsSync(configPath)) {
19+
const configContent = fs.readFileSync(configPath, 'utf8');
20+
cachedConfig = JSON.parse(configContent);
21+
return cachedConfig;
22+
}
23+
24+
const defaultPaths = [
25+
path.join(process.cwd(), 'config.json'),
26+
path.join(process.cwd(), 'matrix-config.json'),
27+
];
28+
29+
for (const filepath of defaultPaths) {
30+
if (fs.existsSync(filepath)) {
31+
const configContent = fs.readFileSync(filepath, 'utf8');
32+
cachedConfig = JSON.parse(configContent);
33+
return cachedConfig;
34+
}
35+
}
36+
37+
console.warn('No configuration file found, using defaults');
38+
return defaults;
39+
} catch (error) {
40+
console.error('Error loading configuration:', error);
41+
return defaults;
42+
}
43+
}
44+
45+
export function getServerName(): string {
46+
const config = loadConfig();
47+
return config.name || defaults.name;
48+
}
49+
50+
export function getFullConfig(): any {
51+
return loadConfig();
52+
}

0 commit comments

Comments
 (0)