| 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 | |
| 21 | pub mod code; |
| 22 | pub mod server; |
| 23 | pub mod site; |
| 24 | pub mod ws; |
| 25 | |
| 26 | use 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. |
| 34 | pub 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. |
| 55 | pub const SLOT: usize = 4; |
| 56 | /// Marks a group message's slot: see the crate's documentation. |
| 57 | pub const GROUP: u32 = 1 << 31; |
| 58 | pub const BROADCAST: u32 = u32::MAX; |
| 59 | |
| 60 | /// What the relay tells a peer. |
| 61 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 62 | pub 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)] |
| 79 | pub 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 | |
| 86 | impl 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 | |
| 101 | impl 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 | |
| 124 | impl 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 | |
| 133 | impl 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)] |
| 148 | mod 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 | } |