1//! Snowbound's Live Share relay, and what its clients share with it.
2//!
3//! A peer opens a WebSocket to `/v1/room/<tag>`, or to `/v1/claim` for a code's room, whose
4//! number the relay chooses. Every peer in a room has a slot, a number the relay gives it
5//! that is never reused in that room. A binary message is a slot (`u32`, big-endian) and
6//! bytes: sent, the slot it goes to; received, the slot it came from. What the bytes are
7//! the relay never knows; peers seal them end to end. Text messages are [`Notice`]s from the
8//! relay and [`Verdict`]s to it.
9//!
10//! A group message is sent once and copied by the relay: to `BROADCAST`, every other peer
11//! in the room the sender may reach; to `GROUP | n` followed by `n` slots, those peers.
12//! Received, its slot is the sender's marked with `GROUP`. Peers seal group messages under a
13//! key the room's secret gives them all.
14//!
15//! Of each pair of peers in a code's room the one with the higher slot opens the stream
16//! between them.
17//! A peer joining a code's room waits, hearing only the room's owner, until the owner says
18//! its opening `met`; a peer that `failed`, left first, or stayed silent too long counts a
19//! wrong code against its address and the code. See `resources/live-share.md`.
20
21pub mod code;
22pub mod server;
23pub mod site;
24pub mod ws;
25
26use std::{
27 fmt,
28 net::{IpAddr, Ipv4Addr, Ipv6Addr, TcpStream},
29 str::FromStr,
30};
31
32/// Where a request counts against limits: the address it connected from, or with
33/// `trust_forwarded` the one its proxy says, an IPv6 address by its /64.
34pub fn peer(stream: &TcpStream, head: &str, trust_forwarded: bool) -> IpAddr {
35 let forwarded = ws::header(head, "X-Forwarded-For")
36 .filter(|_| trust_forwarded)
37 .and_then(|value| value.rsplit(',').next()?.trim().parse().ok());
38 let ip = forwarded
39 .or_else(|| stream.peer_addr().ok().map(|address| address.ip()))
40 .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
41 match ip {
42 IpAddr::V6(v6) => match v6.to_ipv4_mapped() {
43 Some(v4) => IpAddr::V4(v4),
44 // One subscriber is usually given a whole /64.
45 None => {
46 let [a, b, c, d, ..] = v6.segments();
47 IpAddr::V6(Ipv6Addr::new(a, b, c, d, 0, 0, 0, 0))
48 }
49 },
50 v4 => v4,
51 }
52}
53
54/// The bytes before a binary message's payload: the slot it goes to or came from.
55pub const SLOT: usize = 4;
56/// Marks a group message's slot: see the crate's documentation.
57pub const GROUP: u32 = 1 << 31;
58pub const BROADCAST: u32 = u32::MAX;
59
60/// What the relay tells a peer.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub enum Notice {
63 /// The number of the code whose room this peer claimed; sent before `Welcome`.
64 Nameplate(u32),
65 /// This peer's slot, and the slots of those already in the room it may talk to.
66 Welcome {
67 you: u32,
68 members: Vec<u32>,
69 },
70 /// Someone this peer may now talk to.
71 Joined(u32),
72 Left(u32),
73 /// The code had too many wrong tries and admits no one new; sent to its owner.
74 Burned,
75}
76
77/// What a code's owner tells the relay of the peer in a slot.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum Verdict {
80 /// The peer knew the code.
81 Met(u32),
82 /// The peer's opening failed: a wrong code.
83 Failed(u32),
84}
85
86impl fmt::Display for Notice {
87 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
88 match self {
89 Notice::Nameplate(number) => write!(f, "nameplate {number}"),
90 Notice::Welcome { you, members } => {
91 write!(f, "welcome {you}")?;
92 members.iter().try_for_each(|slot| write!(f, " {slot}"))
93 }
94 Notice::Joined(slot) => write!(f, "joined {slot}"),
95 Notice::Left(slot) => write!(f, "left {slot}"),
96 Notice::Burned => f.write_str("burned"),
97 }
98 }
99}
100
101impl FromStr for Notice {
102 type Err = ();
103
104 fn from_str(text: &str) -> Result<Self, ()> {
105 let mut words = text.split(' ');
106 let name = words.next().ok_or(())?;
107 let numbers: Vec<u32> = words
108 .map(|word| word.parse().map_err(|_| ()))
109 .collect::<Result<_, _>>()?;
110 Ok(match (name, &numbers[..]) {
111 ("nameplate", [number]) => Notice::Nameplate(*number),
112 ("welcome", [you, members @ ..]) => Notice::Welcome {
113 you: *you,
114 members: members.to_vec(),
115 },
116 ("joined", [slot]) => Notice::Joined(*slot),
117 ("left", [slot]) => Notice::Left(*slot),
118 ("burned", []) => Notice::Burned,
119 _ => return Err(()),
120 })
121 }
122}
123
124impl fmt::Display for Verdict {
125 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126 match self {
127 Verdict::Met(slot) => write!(f, "met {slot}"),
128 Verdict::Failed(slot) => write!(f, "failed {slot}"),
129 }
130 }
131}
132
133impl FromStr for Verdict {
134 type Err = ();
135
136 fn from_str(text: &str) -> Result<Self, ()> {
137 let (name, slot) = text.split_once(' ').ok_or(())?;
138 let slot = slot.parse().map_err(|_| ())?;
139 match name {
140 "met" => Ok(Verdict::Met(slot)),
141 "failed" => Ok(Verdict::Failed(slot)),
142 _ => Err(()),
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn notices_and_verdicts_read_back() {
153 for notice in [
154 Notice::Nameplate(412),
155 Notice::Welcome {
156 you: 3,
157 members: vec![1, 2],
158 },
159 Notice::Welcome {
160 you: 1,
161 members: vec![],
162 },
163 Notice::Joined(4),
164 Notice::Left(4),
165 Notice::Burned,
166 ] {
167 assert_eq!(notice.to_string().parse(), Ok(notice));
168 }
169 for verdict in [Verdict::Met(2), Verdict::Failed(9)] {
170 assert_eq!(verdict.to_string().parse(), Ok(verdict));
171 }
172 assert_eq!("shrug 1".parse::<Notice>(), Err(()));
173 }
174}