brandon1024 / gitchat

:speech_balloon: A Git-Based Command-Line Messaging Application
5 stars 0 forks source link

Push Notifications #78

Open brandon1024 opened 3 years ago

brandon1024 commented 3 years ago

In the time I've been using git-chat, I've found that the solution is in desperately need of push notifications. Whenever someone git-pushes a new message, I want to be notified so I can git-pull and read the message.

Implementing a pull-based notification scheme is pretty easy, and I've been doing that as a solution in the interim. The basic idea is to create a post-merge hook that creates the notification, and then periodically git-pull in a separate shell:

$ cat .git/hooks/post-merge
#!/usr/bin/bash
zenity --notification --window-icon="info" --text "New git-chat message received!"

$ while true; do pushd ~/Downloads/gc-repo/; git pull --ff-only; popd; sleep 60; done

While this is fine for now, but it's not that elegant. I'd like to instead setup some kind of push-based model where the client no longer needs to poll for new messages. Of course, this will only be usable when git-chat is used against a self-hosted git server (since we need full control on the server side).

Here's what I'm thinking.

Client Side

Introduce a new daemon application that runs as a systemd service. The service periodically sends a heartbeat message to the server so that the git server knows of this particular client. The service also listens for push notification UDP messages from the server.

The heartbeat message should include the following:

In pseudocode, the client daemon will work as follows

thread2 1:
    while true:
        ssh -x git@server "git-chat-client-subscribe --space <space>.git --listen-port <port> --client-id <64-digit-hex-string>"
        sleep 60

thread 2:
    listen udp:<port>
    accept:
        notify

Unknowns:

Server Side

git-shell supports custom commands under a ~/git-shell-commands directory on the server. We will implement a new custom (non-interactive) shell command here that accepts a client heartbeat signal and updates an internal store of recipients. The client's address can be determined by the SSH_CONNECTION environment variable, since the connection happens over SSH.

We will also install a server-side post-receive hook that pushes a notification to all registered clients. Contents of the notification message are still not fleshed out, but I'd like to include the following information:

The internal recipient store should be located under /var/git-chat-notif-db/, and organized similar to how objects are stored in the git db. Each object key will be the client-provided identifier (in hex), and the value will be a file containing the client information in a strictly-structured format:

<unix epoch of last heartbeat> <NUL> <IP Address> <NUL> <PORT>

Notes

I decided to go the UDP-based push notification route because it should offer great server-side performance. No long-running processes will need to be deployed, just a new git shell command and a server-side hook. UDP is nice because it supports multicast, so we should be able to quickly and efficiently broadcast to hundreds of users efficiently. I considered doing long-lived TCP connections instead, but this would require a long-lived server-side process and I'd prefer to avoid that; that solution would be a pain to implement, and would be difficult to scale.

The biggest drawback of the UDP route is dealing with firewalls. It looks like clients will have to open a port on their router firewall. That kinda sucks.

It's going to be tricky to get security right with the client daemon.

When it's time to take this on, I think I'll implement it in a new project repository. I'd like for this to be supported as an optional package that the client (and server) can install.

brandon1024 commented 1 year ago

After a bit of research, NAT hole punching would be the way to get through the firewall issues. This is how peer-to-peer communication typically works.