Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sendtransaction validation pdu #10

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/events/m.room.create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ test("roomCreateEvent", async () => {
});

const signed = await signEvent(event, signature, "hs1");

// @ts-ignore
expect(signed).toStrictEqual(finalEvent);
expect(signed).toHaveProperty(
"signatures.hs1.ed25519:a_HDhg",
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/events/m.room.power_levels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface RoomPowerLevelsEvent extends EventBase {
redact: number;
invite: number;
historical: number;

notifications?: {
[key: string]: number;
};
};
unsigned?: {
age_ts: number;
Expand Down
10 changes: 8 additions & 2 deletions packages/fake/src/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,17 @@ export const fakeEndpoints = new Elysia({ prefix: "/fake" })
}

const lastEventId = events[events.length - 1]._id;
const lastEvent = events[events.length - 1].event as any; //TODO: fix typing
const lastEvent = events[events.length - 1].event;

const inviteEvent = await signEvent(
roomMemberEvent({
auth_events: lastEvent.auth_events,
auth_events: {
// that's not true but it's a fake operation
create: lastEvent.auth_events[0],
power_levels: lastEvent.auth_events[1],
join_rules: lastEvent.auth_events[2],
history_visibility: lastEvent.auth_events[3],
},
membership: "invite",
depth: lastEvent.depth + 1,
// origin: lastEvent.origin,
Expand Down
9 changes: 6 additions & 3 deletions packages/homeserver/src/fixtures/ContextBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
}

class MockedRoom {
private events: EventStore[] = [];
public events: EventStore[] = [];
constructor(
private roomId: string,
public roomId: string,
events: EventStore[],
) {
for (const event of events) {
Expand All @@ -61,8 +61,8 @@
return new ContextBuilder(name);
}

public withName(name: string) {
this.name = name;

Check warning on line 65 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

64-65 lines are not covered with tests
return this;
}

Expand All @@ -76,18 +76,18 @@
return this;
}

public withConfig(config: any) {
this.config = config;

Check warning on line 80 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

79-80 lines are not covered with tests
return this;
}

public withMongo(mongo: any) {
this.mongo = mongo;

Check warning on line 85 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

84-85 lines are not covered with tests
return this;
}

public withMutex(mutex: any) {
this.mutex = mutex;

Check warning on line 90 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

89-90 lines are not covered with tests
return this;
}

Expand All @@ -101,8 +101,8 @@
return this;
}

public withRemoteSigningKey(remote: string, signingKey: SigningKey) {
this.remoteRemoteSigningKeys.set(remote, signingKey);

Check warning on line 105 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

104-105 lines are not covered with tests
return this;
}

Expand Down Expand Up @@ -143,13 +143,13 @@
?.filter((event) => eventIds.includes(event._id)) ?? []
);
},
createStagingEvent: async (event: EventBase) => {
const id = generateId(event);
this.events.get(event.room_id)?.push({
_id: id,
event,
staged: true,
});

Check warning on line 152 in packages/homeserver/src/fixtures/ContextBuilder.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

146-152 lines are not covered with tests
return id;
},
createEvent: async (event: EventBase) => {
Expand Down Expand Up @@ -191,7 +191,10 @@
makeRequest,
createRoom: async (sender: string, ...members: string[]) => {
const { roomId, events } = await createRoom(
[sender, ...members],
[
`@${sender}:${config.name}`,
...members.map((member) => `@${member}:${config.name}`),
],
createSignedEvent(config.signingKey[0], config.name),
`!${createMediaId(18)}:${config.name}`,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import type { EventBase } from "@hs/core/src/events/eventBase";
import { generateId } from "../../authentication";
import type { EventStore } from "../../plugins/mongodb";
import { isRoomCreateEvent } from "@hs/core/src/events/m.room.create";
import {
type RoomMemberEvent,
isRoomMemberEvent,
} from "@hs/core/src/events/m.room.member";
import {
type PowerLevelNames,
isRoomPowerLevelsEvent,
} from "@hs/core/src/events/m.room.power_levels";
import { validateRoomCreateEvent } from "./validateRoomCreateEvent";
import { validateRoomMemberEvent } from "./validateRoomMemberEvent";
import { validateRoomPowerLevelsEvent } from "./validateRoomPowerLevelsEvent";

const difference = (a: string[], b: string[]) =>
a.filter((x) => !b.includes(x));
const getMissingEvents = (a: string[]) => [];

// https://spec.matrix.org/v1.2/rooms/v9/#authorization-rules

export const ensureAuthorizationRulesBatch = function* (
events: EventBase[],
roomId: string,
size = 100,
) {
const array = [...events];
while (array.length) {
yield ensureAuthorizationRules(array.splice(0, size), roomId);

Check warning on line 30 in packages/homeserver/src/procedures/autorization/ensureAuthorizationRules.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

23-30 lines are not covered with tests
}
};

export const ensureAuthorizationRules = async (
events: EventBase[],
roomId: string,
) => {
const eventMap = new Map(events.map((event) => [generateId(event), event]));
// const eventKeys = Array.from(eventMap.keys());

const seenRemoteEvents = new Set<EventStore>(); // get from database

for (const seen of seenRemoteEvents) {
eventMap.delete(seen._id);

Check warning on line 44 in packages/homeserver/src/procedures/autorization/ensureAuthorizationRules.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

44 line is not covered with tests
}

const authGraph = Object.fromEntries(
[...eventMap.entries()].map(([id, event]) => {
return [id, event.auth_events.map((eventId) => eventMap.get(eventId))];
}),
);

const sortedAuthEvents = [...eventMap.values()];

const authEventIds = sortedAuthEvents
.flatMap((event) => event.auth_events.map((eventId) => eventId))
.filter(Boolean);

const authMap = new Map(
sortedAuthEvents
.filter((event) => authEventIds.includes(generateId(event)))
.map((event) => [generateId(event), event]),
);

const missingEventsId = difference(authEventIds, [...authMap.keys()]);

if (!missingEventsId.length) {
const missingEvents = getMissingEvents(missingEventsId);
for (const event of missingEvents) {
eventMap.set(generateId(event), event);

Check warning on line 70 in packages/homeserver/src/procedures/autorization/ensureAuthorizationRules.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

70 line is not covered with tests
}
}

const eventsToBeStored = new Set<string>();
for await (const event of sortedAuthEvents) {
try {
if (await prep(event, authMap)) {
eventsToBeStored.add(generateId(event));
}
} catch (e) {

Check warning on line 80 in packages/homeserver/src/procedures/autorization/ensureAuthorizationRules.ts

View workflow job for this annotation

GitHub Actions / Code Quality Checks(lint, test, tsc)

80 line is not covered with tests
console.log("error", e);
}
}

return eventsToBeStored;
};

async function prep(event: EventBase, authMap: Map<string, EventBase>) {
const authEvents = new Map<string, EventBase>();
for (const authEventId of event.auth_events) {
const ae = authMap.get(authEventId);
if (!ae) {
// The fact we can't find the auth event doesn't mean it doesn't
// exist, which means it is premature to reject `event`. Instead, we
// just ignore it for now.
console.log(
`Dropping event ${generateId(event)}, which relies on auth_event ${authEventId}, which could not be found`,
);
return;
}
authEvents.set(authEventId, ae);
}
// We're not bothering about room state, so flag the event as an outlier.
// event.internalMetadata.outlier = true;
// const context = EventContext.forOutlier(this._storageControllers);

// validateEventForRoomVersion(event);
switch (true) {
case isRoomCreateEvent(event): {
await validateRoomCreateEvent(event, authEvents);
return true;
}
case isRoomMemberEvent(event): {
await validateRoomMemberEvent(event, authEvents);
return true;
}
}

const caller = [...authMap.values()].find((authEvent) => {
if (isRoomMemberEvent(authEvent)) {
return authEvent.state_key === event.sender;
}
return false;
}) as RoomMemberEvent | undefined;

const callerInRoom = caller?.content.membership === "join";

const callerPowerLevel = getUserPowerLevel(event.sender, authEvents);

const inviteLevel = getNamedPowerLevel("invite", authEvents) ?? 0;

// 5 If the sender’s current membership state is not join, reject.
if (!callerInRoom) {
throw new Error("Invalid sender");
}
// 6 If type is m.room.third_party_invite:
if (event.type === "m.room.third_party_invite") {
// 6.1 Allow if and only if sender’s current power level is greater than or equal to the invite level.
if (callerPowerLevel >= inviteLevel) {
return;
}
throw new Error("Invalid sender");
}

// TODO: 7 If the event type’s required power level is greater than the sender’s power level, reject.

// TODO: 8 If the event has a state_key that starts with an @ and does not match the sender, reject.
if (
"state_key" in event &&
event.state_key &&
event.state_key.startsWith("@") &&
event.state_key !== event.sender
) {
throw new Error("Invalid state_key");
}

// 9 If type is m.room.power_levels
if (isRoomPowerLevelsEvent(event)) {
await validateRoomPowerLevelsEvent(event, authEvents);
}
// 10 otherwise, allow.
return true;
}

export const getNamedPowerLevel = (
name: PowerLevelNames,
authEvents: Map<string, EventBase>,
) => {
const powerLevelEvent = getEventPowerLevel(authEvents);
if (!powerLevelEvent) {
return;
}
return powerLevelEvent.content[name];
};

const getEventPowerLevel = (authEvents: Map<string, EventBase>) =>
[...authEvents.values()].find(isRoomPowerLevelsEvent);

export function getUserPowerLevel(
userId: string,
authEvents: Map<string, EventBase>,
): number {
/**
* Get a user's power level.
*
* @param userId - User's ID to look up in power levels.
* @param authEvents - State in force at this point in the room (or rather, a subset
* of it including at least the create event and power levels event).
* @returns The user's power level in this room.
*/

const powerLevelEvent = getEventPowerLevel(authEvents);

if (powerLevelEvent) {
const powerLevelDefault = powerLevelEvent.content?.users_default ?? 0;

return Number(
powerLevelEvent.content?.users?.[userId] ?? powerLevelDefault,
);
}
// If there is no power levels event, the creator gets 100 and everyone else gets 0.

// Some things which call this don't pass the create event: hack around that.

const createEvent = [...authEvents.values()].find(isRoomCreateEvent);

if (createEvent) {
// TODO: const creator = createEvent.roomVersion?.implicitRoomCreator
// ? createEvent.sender
// : createEvent.content?.[EventContentFields.ROOM_CREATOR];

if (createEvent.sender === userId) {
return 100;
}
}

return 0;
}
Loading
Loading