Skip to main content

tnnl/
protocol.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
2use futures::AsyncReadExt;
3use hmac::{Hmac, Mac};
4use serde::{Deserialize, Serialize};
5use sha2::Sha256;
6
7const MAX_CONTROL_MSG: usize = 4096;
8
9#[derive(Debug, Serialize, Deserialize)]
10#[serde(tag = "type")]
11pub enum ControlMsg {
12    /// Client → Server: subdomain request + HMAC proof.
13    /// Client generates `nonce`; `hmac` = HMAC-SHA256(secret, nonce) if secret is set.
14    Auth {
15        subdomain: Option<String>,
16        nonce: String,
17        hmac: Option<String>,
18    },
19    AuthOk {
20        subdomain: String,
21        url: String,
22    },
23    Error {
24        message: String,
25    },
26}
27
28impl ControlMsg {
29    pub fn encode(&self) -> anyhow::Result<Vec<u8>> {
30        let mut buf = serde_json::to_vec(self)?;
31        buf.push(b'\n');
32        Ok(buf)
33    }
34
35    fn decode(line: &[u8]) -> anyhow::Result<Self> {
36        Ok(serde_json::from_slice(line)?)
37    }
38}
39
40/// HMAC-SHA256(secret, nonce), base64-encoded.
41pub fn compute_hmac(secret: &str, nonce: &str) -> String {
42    type HmacSha256 = Hmac<Sha256>;
43    let mut mac =
44        HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC accepts any key length");
45    mac.update(nonce.as_bytes());
46    B64.encode(mac.finalize().into_bytes())
47}
48
49/// Read a newline-delimited control message from a yamux stream.
50pub async fn read_msg(stream: &mut yamux::Stream) -> anyhow::Result<ControlMsg> {
51    let mut buf = Vec::with_capacity(256);
52    let mut byte = [0u8; 1];
53    loop {
54        let n = stream.read(&mut byte).await?;
55        if n == 0 {
56            anyhow::bail!("stream closed");
57        }
58        if byte[0] == b'\n' {
59            break;
60        }
61        buf.push(byte[0]);
62        if buf.len() > MAX_CONTROL_MSG {
63            anyhow::bail!("control message too large");
64        }
65    }
66    ControlMsg::decode(&buf)
67}