gatewayd: ws conn, messages, pings, closing and a dead peer
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
//! One WebSocket connection to Mattermost (M4a spec, section 6): open it, send text, and poll for
|
||||
//! the next text message while answering pings, sending our own, and noticing a dead peer.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::config::ServerUrl;
|
||||
use crate::net::{Connector, Stream};
|
||||
use crate::ws::WsError;
|
||||
use crate::ws::frame::{CLOSE, Decoder, Incoming, PING, PONG, TEXT, encode};
|
||||
use crate::ws::handshake::handshake;
|
||||
|
||||
/// Mattermost's WebSocket path.
|
||||
pub const PATH: &str = "/api/v4/websocket";
|
||||
|
||||
/// How often we ping, and how long silence may last.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Timing {
|
||||
pub ping_every: Duration,
|
||||
pub dead_after: Duration,
|
||||
}
|
||||
|
||||
pub struct Ws {
|
||||
stream: Stream,
|
||||
decoder: Decoder,
|
||||
random: Box<dyn Read + Send>,
|
||||
timing: Timing,
|
||||
last_heard: Instant,
|
||||
last_ping: Instant,
|
||||
}
|
||||
|
||||
impl Ws {
|
||||
/// Connect, and complete the handshake with `token`. `random` supplies the key and every mask
|
||||
/// (in `gatewayd`, `/dev/urandom`).
|
||||
pub fn open(
|
||||
connector: &Connector,
|
||||
token: &str,
|
||||
timing: Timing,
|
||||
mut random: Box<dyn Read + Send>,
|
||||
) -> Result<Ws, WsError> {
|
||||
// 1. `connector.connect(timing.dead_after)`; its error becomes
|
||||
// WsError::Handshake(e.to_string()).
|
||||
// 2. `handshake(&mut stream, &host_header(connector.server()), PATH, token, &mut random)?`.
|
||||
// 3. A Ws with a new Decoder, and last_heard and last_ping both now.
|
||||
let mut stream = connector
|
||||
.connect(timing.dead_after)
|
||||
.map_err(|e| WsError::Handshake(e.to_string()))?;
|
||||
handshake(
|
||||
&mut stream,
|
||||
&host_header(connector.server()),
|
||||
PATH,
|
||||
token,
|
||||
&mut random,
|
||||
)?;
|
||||
let now = Instant::now();
|
||||
Ok(Ws {
|
||||
stream,
|
||||
decoder: Decoder::new(),
|
||||
random,
|
||||
timing,
|
||||
last_heard: now,
|
||||
last_ping: now,
|
||||
})
|
||||
}
|
||||
|
||||
fn send(&mut self, opcode: u8, payload: &[u8]) -> Result<(), WsError> {
|
||||
// 4 mask bytes from `random` (read_exact), then write `encode(opcode, payload, mask)` and
|
||||
// flush.
|
||||
let mut mask = [0u8; 4];
|
||||
self.random.read_exact(&mut mask)?;
|
||||
let frame = encode(opcode, payload, mask);
|
||||
self.stream.write_all(&frame)?;
|
||||
self.stream.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_text(&mut self, text: &str) -> Result<(), WsError> {
|
||||
// `send` with TEXT.
|
||||
self.send(TEXT, text.as_bytes())
|
||||
}
|
||||
|
||||
/// The next text message, or `None` after about `wait` with none. Pings are answered and sent
|
||||
/// here; a close frame is answered and ends the connection (`Closed`); silence past the
|
||||
/// dead-after limit is `Dead`. Written for you: it is the glue.
|
||||
pub fn poll(&mut self, wait: Duration) -> Result<Option<String>, WsError> {
|
||||
let until = Instant::now() + wait;
|
||||
loop {
|
||||
if let Some(text) = self.take_messages()? {
|
||||
return Ok(Some(text));
|
||||
}
|
||||
let now = Instant::now();
|
||||
self.keep_alive(now)?;
|
||||
if now >= until {
|
||||
return Ok(None);
|
||||
}
|
||||
let timeout = self.read_timeout(until, now);
|
||||
self.read_some(timeout)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Act on every whole message the decoder holds, until one is text.
|
||||
fn take_messages(&mut self) -> Result<Option<String>, WsError> {
|
||||
// `while let Some(message) = self.decoder.next_message()? { match message { ... } }`:
|
||||
// - Text(text) -> return Ok(Some(text)).
|
||||
// - Ping(payload) -> `self.send(PONG, &payload)?`.
|
||||
// - Pong(_) -> nothing.
|
||||
// - Close(code, _) -> the reply is the code as 2 big-endian bytes, or empty:
|
||||
// `code.map(|c| c.to_be_bytes().to_vec()).unwrap_or_default()`. Then
|
||||
// `let _ = self.send(CLOSE, &reply);` (the peer may be gone) and Err(WsError::Closed).
|
||||
// After the loop: Ok(None).
|
||||
while let Some(message) = self.decoder.next_message()? {
|
||||
match message {
|
||||
Incoming::Text(text) => return Ok(Some(text)),
|
||||
Incoming::Ping(payload) => self.send(PONG, &payload)?,
|
||||
Incoming::Pong(_) => {}
|
||||
Incoming::Close(code, _) => {
|
||||
let reply = code.map(|c| c.to_be_bytes().to_vec()).unwrap_or_default();
|
||||
let _ = self.send(CLOSE, &reply);
|
||||
return Err(WsError::Closed);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// The dead-peer check, and our ping when one is due.
|
||||
fn keep_alive(&mut self, now: Instant) -> Result<(), WsError> {
|
||||
// 1. `now.duration_since(self.last_heard) >= self.timing.dead_after` -> Err(WsError::Dead).
|
||||
// 2. `now.duration_since(self.last_ping) >= self.timing.ping_every` ->
|
||||
// `self.send(PING, b"")?` and `self.last_ping = now`.
|
||||
// 3. Ok(()).
|
||||
if now.duration_since(self.last_heard) >= self.timing.dead_after {
|
||||
return Err(WsError::Dead);
|
||||
}
|
||||
if now.duration_since(self.last_ping) >= self.timing.ping_every {
|
||||
self.send(PING, b"")?;
|
||||
self.last_ping = now;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How long the next read may wait: until the soonest of the end of `wait`, the next ping and
|
||||
/// the dead-after limit, and never less than 1 ms.
|
||||
fn read_timeout(&self, until: Instant, now: Instant) -> Duration {
|
||||
// `(self.last_ping + self.timing.ping_every).saturating_duration_since(now)` is the time to
|
||||
// the next ping; the same with last_heard and dead_after; and
|
||||
// `until.saturating_duration_since(now)`. The least of the three (`.min`), then
|
||||
// `.max(Duration::from_millis(1))`.
|
||||
let next_ping = (self.last_ping + self.timing.ping_every).saturating_duration_since(now);
|
||||
let next_dead = (self.last_heard + self.timing.dead_after).saturating_duration_since(now);
|
||||
let until_left = until.saturating_duration_since(now);
|
||||
next_ping
|
||||
.min(next_dead)
|
||||
.min(until_left)
|
||||
.max(Duration::from_millis(1))
|
||||
}
|
||||
|
||||
/// One read, at most `timeout` long, fed to the decoder.
|
||||
fn read_some(&mut self, timeout: Duration) -> Result<(), WsError> {
|
||||
// 1. `self.stream.set_read_timeout(Some(timeout))?`.
|
||||
// 2. `let mut buf = [0u8; 16 * 1024];` and `match self.stream.read(&mut buf)`:
|
||||
// - Ok(0) -> Err(WsError::Closed).
|
||||
// - Ok(n) -> `self.decoder.feed(buf.get(..n).unwrap_or_default())`,
|
||||
// `self.last_heard = Instant::now()`, Ok(()).
|
||||
// - Err(e) whose `e.kind()` is WouldBlock, TimedOut or Interrupted -> Ok(()): nothing
|
||||
// came, and `poll` goes round.
|
||||
// - any other Err(e) -> Err(WsError::Io(e)).
|
||||
self.stream.set_read_timeout(Some(timeout))?;
|
||||
let mut buf = [0u8; 16 * 1024];
|
||||
match self.stream.read(&mut buf) {
|
||||
Ok(0) => Err(WsError::Closed),
|
||||
Ok(n) => {
|
||||
self.decoder.feed(buf.get(..n).unwrap_or_default());
|
||||
self.last_heard = Instant::now();
|
||||
Ok(())
|
||||
}
|
||||
Err(e)
|
||||
if matches!(
|
||||
e.kind(),
|
||||
std::io::ErrorKind::WouldBlock
|
||||
| std::io::ErrorKind::TimedOut
|
||||
| std::io::ErrorKind::Interrupted
|
||||
) =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(WsError::Io(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a close frame, best effort, and drop the connection.
|
||||
pub fn close(mut self) {
|
||||
// Send CLOSE with 1000 as 2 big-endian bytes; ignore the error.
|
||||
let _ = self.send(CLOSE, &1000u16.to_be_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Host` header for a server: the port is written only when it is not the scheme's default.
|
||||
pub fn host_header(server: &ServerUrl) -> String {
|
||||
// The host alone when the port is the default for the scheme (443 for tls, 80 otherwise), else
|
||||
// "host:port".
|
||||
let default = if server.tls { 443 } else { 80 };
|
||||
if server.port == default {
|
||||
server.host.clone()
|
||||
} else {
|
||||
format!("{}:{}", server.host, server.port)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! The WebSocket client (RFC 6455; M4a spec, section 6): the handshake, the frame codec, and the
|
||||
//! connection that uses them.
|
||||
|
||||
pub mod conn;
|
||||
pub mod frame;
|
||||
pub mod handshake;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user