-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implements update rollback mechanism #3
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
base: poc-updates
Are you sure you want to change the base?
Conversation
// TODO, we can close their connection or just let them continue, since we've already undone their changes (and our changes are newer than theirs) | ||
const error = { | ||
reason: `Modification of a restricted type: ${this.threadsMapKey} was rejected`, | ||
} satisfies Partial<CloseEvent>; | ||
throw error; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can close their connection or let them continue. Closing their connection does have the benefit that it forces them to re-sync so I went with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you are undoing, the client will receive a message that reverses their changes (Assuming Hocuspocus distributes all changes that were applied to a Y.Doc).
But it would be good to know somehow when this happens, because it shouldn't!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry I don't fully understand what you mean by:
But it would be good to know somehow when this happens, because it shouldn't!
But, to address your first point: hocuspocus would distribute this change if it was modified. But, with this implementation, it bails out early and closes the connection with that client. The hocuspocus provider notices that the connection has been closed, and will automatically retry to open a new connection. I think that what happens here is that they attempt to sync again, and the document on the hocuspocus server already includes that "invalid" update and so they can continue to sync from there.
So, either way, the connection seems to continue, so maybe we should not kill the connection? Though, I don't really like just taking the update and not letting the user know that it was invalid. At least closing the connection makes it clear that the client did something invalid.
Happy to take opinions on this though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can close their connection or let them continue.
I don't think we can let them continue because HocusPocus will apply the update again as per https://github.com/TypeCellOS/BlockNote-demo-nextjs-hocuspocus/pull/3/files#r1990990264
Update: I'm probably wrong here, because the second update will be a noop as it's already been applied (even though it has been "undone")
useEffect(() => { | ||
provider.document.on("update", (update) => { | ||
console.log(provider.document.getMap("threads").toJSON()); | ||
}); | ||
}, [provider.document]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This just shows the current client state in an easy to inspect way
onClick={() => { | ||
const comments = provider.document.getMap("threads"); | ||
comments.set("unauthorized-thread", { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simulating a malicious client
private getYUpdate(message: Uint8Array) { | ||
/** | ||
* The messages we are interested in are of the following format: | ||
* [docIdLength: number, ...docIdString: string, hocuspocusMessageType: number, ySyncMessageType: number, ...yjsUpdate: Uint8Array] | ||
* | ||
* We check that the hocuspocusMessageType is Sync and that the ySyncMessageType is messageYjsUpdate. | ||
*/ | ||
const incomingMessage = new IncomingMessage(message); | ||
// Read the docID string, but don't use it | ||
incomingMessage.readVarString(); | ||
|
||
// Read the hocuspocusMessageType | ||
const hocuspocusMessageType = incomingMessage.readVarUint(); | ||
// If the hocuspocusMessageType is not Sync, we don't handle the message, since it is not an update | ||
if (hocuspocusMessageType !== MessageType.Sync) { | ||
return; | ||
} | ||
|
||
// Read the ySyncMessageType | ||
const ySyncMessageType = incomingMessage.readVarUint(); | ||
|
||
// If the ySyncMessageType is not messageYjsUpdate, we don't handle the message, since it is not an update | ||
if (ySyncMessageType !== syncProtocol.messageYjsUpdate) { | ||
// not an update | ||
return; | ||
} | ||
|
||
// Read the yjsUpdate | ||
const yUpdate = incomingMessage.readVarUint8Array(); | ||
|
||
return yUpdate; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This took forever to figure out 😮💨
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice you got this to work. Not sure yet whether this approach is feasible though:
- see my comment in
beforeHandleMessage
; if we're applying updates twice that would be a problem - if we're also missing other messages (see comments about
SyncReply
andmessageYjsSyncStep1
) this approach will get quite error-prone
let's discuss
// Read the hocuspocusMessageType | ||
const hocuspocusMessageType = incomingMessage.readVarUint(); | ||
// If the hocuspocusMessageType is not Sync, we don't handle the message, since it is not an update | ||
if (hocuspocusMessageType !== MessageType.Sync) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe? I'm not familiar with what this bit is doing totally
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks specific to the Redis integration, but doesn't hurt to add: https://github.com/ueberdosis/hocuspocus/blob/05f7980ef8840c20be9145805c466f14b4dd3939/packages/server/src/types.ts#L16
update, | ||
document: ydoc, | ||
}: beforeHandleMessagePayload) { | ||
const yUpdate = this.getYUpdate(update); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we process the update here (and in rollbackUpdateIfNeeded
manually). Wouldn't we now apply the update twice? i.e.: the regular hocuspocus code will still process the message, right? so everything would be processed twice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only way that I could think of not applying the update twice would be to make a whole copy of the ydoc.
Applying updates twice should be fine, no? It is a CRDT 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine to apply the update twice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to be clear; this means all updates will be applied twice. Wouldn't that double the load?
@nperez0111 wdyt of something like this? https://github.com/ueberdosis/hocuspocus/compare/main...YousefED:hocuspocus:feat/custom-messagereceiver?expand=1 Not sure it will land of course, but I think this should make it possible to customize the messagereceiver and wrap / overload the |
Yea, I think that something like that would work, and probably worthwhile to try to get in. |
Nice. @nperez0111 do you want to try first whether it works before I (or you) fire up a PR? |
This resolves #1 by implementing a rollback mechanism for hocuspocus servers.
This will inspect a hocuspocus sync, which contains a ysync update, and check that there are no modifications to the
threads
map.This will ensure that a malicious client cannot modify the
threads
map and it can only ever be manipulated through the REST server.