Skip to main content

rainier_framework/
broadcasting.rs

1//! The half of broadcasting that needs HTTP: the authorisation endpoint, the
2//! socket header, and the notification channel.
3//!
4//! `rainier-broadcast` depends on support and nothing else, so it can be used
5//! from a worker with no router. What lives here is what genuinely needs a
6//! request: `/broadcasting/auth`, which is how a browser proves to your
7//! WebSocket server that it may subscribe to a private channel.
8//!
9//! ```text
10//! browser ──subscribe private-orders.7──▶ soketi
11//!    │                                      │
12//!    └──POST /broadcasting/auth─────────────┘  "prove it"
13//!          ▲
14//!          └── this module: is this user allowed, and here is the signature
15//! ```
16
17use std::sync::Arc;
18
19use rainier_auth::AuthenticatedUser;
20use rainier_broadcast::{Broadcasting, Channel, ChannelRegistry};
21use rainier_container::Application;
22use rainier_http::{Request, Response, StatusCode};
23use rainier_notify::{Channel as NotificationChannel, Delivery};
24use rainier_routing::Req;
25use rainier_support::{BoxFuture, Error, Result};
26
27/// The header a Pusher client sends its socket id in.
28///
29/// Laravel reads the same one, and it is what makes `to_others` possible: the
30/// browser tells you which socket it is, so you can leave it out.
31pub const SOCKET_ID_HEADER: &str = "X-Socket-ID";
32
33/// The socket id this request came from, if the client sent one.
34///
35/// Pass it to [`Broadcasting::event_except`] to skip the browser that caused
36/// the change — it has already updated itself, and echoing the change back
37/// makes its own edit flicker.
38pub fn socket_id(request: &Request) -> Option<String> {
39    request.header(SOCKET_ID_HEADER).map(str::to_string)
40}
41
42/// `POST /broadcasting/auth` — decide whether this user may subscribe.
43///
44/// Laravel's `Broadcast::routes()`, as one handler you attach yourself:
45///
46/// ```ignore
47/// router
48///     .post("/broadcasting/auth", broadcasting::authorize::<User>)
49///     .name("broadcasting.auth")
50///     .middleware(kernel::auth("api"));
51/// ```
52///
53/// **Put it behind the auth middleware.** It reads the authenticated user from
54/// the request, and without a guard in front there is none — every private
55/// channel then answers `401`, which looks like a broken client rather than a
56/// missing route.
57///
58/// The request carries `socket_id` and `channel_name`, form-encoded or JSON,
59/// which is what every Pusher client sends.
60pub async fn authorize<U>(request: Req) -> Result<Response>
61where
62    U: Send + Sync + 'static,
63{
64    let socket = required(&request, "socket_id")?;
65    let wire_name = required(&request, "channel_name")?;
66    let channel = Channel::from_wire_name(&wire_name);
67
68    let app = rainier_container::facade_application();
69    let registry = app.resolve::<ChannelRegistry<U>>()?;
70    let broadcasting = app.resolve::<Broadcasting>()?;
71
72    let Some(user) = request.extension::<AuthenticatedUser<U>>() else {
73        return Err(Error::unauthenticated("Unauthenticated."));
74    };
75
76    let access = registry.authorize(user.get(), &channel).await?;
77    if !access.is_allowed() {
78        // A 403 rather than a 404: the client already knows the channel name —
79        // it just sent it — so there is nothing left to conceal, and a Pusher
80        // client shows "denied" for this and hangs on a 404.
81        return Ok(Response::new(StatusCode::FORBIDDEN));
82    }
83
84    // Presence channels carry a roster entry; private ones do not. The driver
85    // decides what proof looks like — an HMAC for a Pusher-protocol server,
86    // nothing for a relay that trusts the application.
87    let body = broadcasting.driver().auth_response(&socket, &channel, access.member())?;
88
89    Ok(Response::json(&body))
90}
91
92/// A field the client must send, from the body or the query.
93fn required(request: &Request, name: &str) -> Result<String> {
94    request
95        .input(name)
96        .map(|value| value.to_string())
97        .filter(|value| !value.is_empty())
98        .ok_or_else(|| Error::bad_request(format!("`{name}` is required.")))
99}
100
101/// Sends notifications to a private channel for the recipient.
102///
103/// Laravel's `broadcast` notification channel. The channel name is
104/// `notifications.{type}.{id}` — for example `private-notifications.User.7` on
105/// the wire — so one subscription receives everything sent to that recipient,
106/// whatever the notification turns out to be.
107///
108/// Uses [`to_data`](rainier_notify::Notification::to_data), falling back to
109/// [`to_text`](rainier_notify::Notification::to_text) wrapped as
110/// `{"message": …}`.
111///
112/// # It is not a delivery
113///
114/// A broadcast reaches whoever is connected at that instant and nobody else.
115/// Pair it with the [database channel](crate::notifications::DatabaseChannel),
116/// which is what
117/// makes the notification survive a reload — this one only makes the bell
118/// menu move without one.
119pub struct BroadcastChannel {
120    broadcasting: Arc<Broadcasting>,
121}
122
123impl BroadcastChannel {
124    /// A channel publishing through `broadcasting`.
125    pub fn new(broadcasting: Arc<Broadcasting>) -> Self {
126        Self { broadcasting }
127    }
128
129    /// The channel a recipient's notifications are published on.
130    ///
131    /// The pattern to authorise is `notifications.{type}.{id}`.
132    pub fn channel_for(notifiable_type: &str, notifiable_id: &str) -> Channel {
133        Channel::private(format!("notifications.{notifiable_type}.{notifiable_id}"))
134    }
135}
136
137impl NotificationChannel for BroadcastChannel {
138    fn name(&self) -> &'static str {
139        "broadcast"
140    }
141
142    fn send<'a>(&'a self, delivery: &'a Delivery) -> BoxFuture<'a, Result<()>> {
143        Box::pin(async move {
144            let Some(payload) = delivery.data_or_text() else {
145                return Err(delivery.nothing_to_send("broadcast", "data or text"));
146            };
147
148            let channel = Self::channel_for(delivery.recipient_type, &delivery.recipient_id);
149
150            self.broadcasting.to(vec![channel], delivery.notification, payload).await
151        })
152    }
153}
154
155/// Register the notification channel's authoriser.
156///
157/// Every recipient may listen to their own notifications and nobody else's.
158/// Without this the channel publishes and no browser is ever allowed to
159/// subscribe, which looks exactly like a broken WebSocket server.
160///
161/// `identify` says which id a user is, so the comparison is against the same
162/// string [`Notifiable::notifiable_id`](rainier_notify::Notifiable) produced.
163pub fn authorize_notifications<U, F>(
164    registry: &mut ChannelRegistry<U>,
165    notifiable_type: &'static str,
166    identify: F,
167) where
168    U: Send + Sync + 'static,
169    F: Fn(&U) -> String + Send + Sync + 'static,
170{
171    registry.channel("notifications.{type}.{id}", move |user: &U, params| {
172        let allowed = params.get("type") == Some(notifiable_type)
173            && params.get("id") == Some(identify(user).as_str());
174
175        Box::pin(async move { Ok(rainier_broadcast::ChannelAccess::allowed_if(allowed)) })
176    });
177}
178
179/// Bind broadcasting into `app` with the log driver and an empty channel table.
180///
181/// What [`Rainier`](crate::Rainier) does at boot unless the application says
182/// otherwise: nothing reaches a browser, and every private channel is denied.
183/// Both are the safe end of their respective mistakes.
184pub fn bind_defaults<U: Send + Sync + 'static>(app: &Application) {
185    app.instance(Broadcasting::log());
186    app.instance(ChannelRegistry::<U>::new());
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use rainier_broadcast::{ChannelAccess, MemoryBroadcaster};
193    use rainier_notify::{Channels, Notifiable, Notification, Notifier};
194
195    struct User(u64);
196
197    impl Notifiable for User {
198        fn notifiable_id(&self) -> String {
199            self.0.to_string()
200        }
201        fn notifiable_type(&self) -> &'static str {
202            "User"
203        }
204        fn route_for(&self, _: &str) -> Option<String> {
205            None
206        }
207    }
208
209    struct Mentioned;
210
211    impl Notification<User> for Mentioned {
212        fn notification_name(&self) -> &'static str {
213            "post.mentioned"
214        }
215        fn via(&self, _: &User) -> Channels {
216            Channels::new().with::<BroadcastChannel>()
217        }
218        fn to_text(&self, _: &User) -> Option<String> {
219            Some("someone mentioned you".into())
220        }
221    }
222
223    #[tokio::test]
224    async fn a_notification_broadcasts_to_the_recipients_own_channel() {
225        let driver = Arc::new(MemoryBroadcaster::new());
226        let broadcasting = Arc::new(Broadcasting::new(driver.clone()));
227        let notifier = Notifier::new().with(BroadcastChannel::new(broadcasting));
228
229        notifier.send(&User(7), &Mentioned).await.unwrap();
230
231        driver.assert_broadcast("post.mentioned", "private-notifications.User.7");
232        assert_eq!(driver.sent()[0].payload["message"], "someone mentioned you");
233    }
234
235    #[tokio::test]
236    async fn a_recipient_may_listen_to_their_own_and_nobody_elses() {
237        let mut registry = ChannelRegistry::<User>::new();
238        authorize_notifications(&mut registry, "User", |user| user.notifiable_id());
239
240        let mine = BroadcastChannel::channel_for("User", "7");
241        assert_eq!(registry.authorize(&User(7), &mine).await.unwrap(), ChannelAccess::Allowed);
242        assert_eq!(registry.authorize(&User(8), &mine).await.unwrap(), ChannelAccess::Denied);
243    }
244
245    #[tokio::test]
246    async fn another_notifiable_type_with_the_same_id_is_denied() {
247        // `Team.7` is not `User.7`, and an authoriser that only compared ids
248        // would let a user read their team's notifications.
249        let mut registry = ChannelRegistry::<User>::new();
250        authorize_notifications(&mut registry, "User", |user| user.notifiable_id());
251
252        let theirs = BroadcastChannel::channel_for("Team", "7");
253        assert_eq!(registry.authorize(&User(7), &theirs).await.unwrap(), ChannelAccess::Denied);
254    }
255}