Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/devcert/** -linguist-detectable
17 changes: 12 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ members = [
"websock-proto",
"websock-tungstenite",
"websock-wasm",
"websock-wasm-demo"
"websock-wasm-demo",
"websock-mux",
"websock-tungstenite-mux",
"websock-wasm-mux",
"websock-mux-proto"
]

[workspace.package]
version = "0.1.0"
version = "0.2.0"
edition = "2024"
authors = ["shellrow <shellrow@foctal.com>"]

[workspace.dependencies]
websock-proto = { path = "websock-proto", version = "0.1.0" }
websock-tungstenite = { path = "websock-tungstenite", version = "0.1.0" }
websock-wasm = { path = "websock-wasm", version = "0.1.0" }
websock-proto = { path = "websock-proto", version = "0.2.0" }
websock-tungstenite = { path = "websock-tungstenite", version = "0.2.0" }
websock-wasm = { path = "websock-wasm", version = "0.2.0" }
websock-mux-proto = { path = "websock-mux-proto", version = "0.2.0" }
websock-tungstenite-mux = { path = "websock-tungstenite-mux", version = "0.2.0" }
websock-wasm-mux = { path = "websock-wasm-mux", version = "0.2.0" }
bytes = "1"
4 changes: 4 additions & 0 deletions devcert/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.crt
*.hex
*.key
*.fingerprint
50 changes: 50 additions & 0 deletions devcert/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail

cd "$(dirname "${BASH_SOURCE[0]}")"

CERT_NAME="localhost"
DAYS=10
CONF="openssl.conf"

echo "==> Generating self-signed certificate (${CERT_NAME})"
echo " validity: ${DAYS} days"
echo " config: ${CONF}"

# Generate ECDSA P-256 private key
openssl ecparam \
-genkey \
-name prime256v1 \
-out "${CERT_NAME}.key"

# Generate self-signed certificate
openssl req \
-x509 \
-sha256 \
-nodes \
-days "${DAYS}" \
-key "${CERT_NAME}.key" \
-out "${CERT_NAME}.crt" \
-config "${CONF}" \
-extensions v3_req

# Generate raw SHA-256 hash (DER -> hex, no colons)
openssl x509 \
-in "${CERT_NAME}.crt" \
-outform der \
| openssl dgst -sha256 -binary \
| xxd -p -c 256 \
> "${CERT_NAME}.hex"

# Also print human-readable fingerprint
openssl x509 \
-in "${CERT_NAME}.crt" \
-noout \
-fingerprint -sha256 \
> "${CERT_NAME}.fingerprint"

echo "==> Done"
echo " - ${CERT_NAME}.crt"
echo " - ${CERT_NAME}.key"
echo " - ${CERT_NAME}.hex"
echo " - ${CERT_NAME}.fingerprint"
22 changes: 22 additions & 0 deletions devcert/openssl.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[req]
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = localhost

[req_ext]
subjectAltName = @alt_names

[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = ::1
19 changes: 19 additions & 0 deletions websock-mux-proto/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "websock-mux-proto"
version.workspace = true
edition.workspace = true
authors.workspace = true
description = "Protocol for multiplexing WebSocket logical streams"
repository = "https://github.com/foctal/websock"
readme = "../README.md"
keywords = ["network", "websocket", "multiplex"]
categories = ["network-programming", "web-programming"]
license = "MIT"

[dependencies]
bytes = { workspace = true }
thiserror = "2"
websock-proto = { workspace = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["io-util"] }
7 changes: 7 additions & 0 deletions websock-mux-proto/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub mod stream;
pub mod varint;

pub use stream::{Frame, FrameDecodeError, StreamDir, StreamId};
pub use varint::{VarInt, VarIntBoundsExceeded, VarIntUnexpectedEnd};

pub const SUBPROTOCOL: &str = "websock-mux-1";
173 changes: 173 additions & 0 deletions websock-mux-proto/src/stream.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use bytes::{Buf, BufMut, Bytes, BytesMut};

use crate::varint::{VarInt, VarIntBoundsExceeded, VarIntUnexpectedEnd};

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum StreamDir {
Bi,
Uni,
}

#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StreamId(pub u64);

impl StreamId {
pub fn new(
counter: u64,
is_server: bool,
dir: StreamDir,
) -> Result<Self, VarIntBoundsExceeded> {
let initiator = if is_server { 1 } else { 0 };
let dir_bit = match dir {
StreamDir::Bi => 0,
StreamDir::Uni => 1,
};
let value = counter.checked_shl(2).ok_or(VarIntBoundsExceeded)?
| ((dir_bit as u64) << 1)
| (initiator as u64);
VarInt::from_u64(value)?;
Ok(Self(value))
}

pub fn dir(self) -> StreamDir {
if (self.0 >> 1) & 1 == 1 {
StreamDir::Uni
} else {
StreamDir::Bi
}
}

pub fn initiator_is_server(self) -> bool {
self.0 & 1 == 1
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Frame {
OpenUni {
id: StreamId,
},
OpenBi {
id: StreamId,
},
Stream {
id: StreamId,
data: Bytes,
fin: bool,
},
ResetStream {
id: StreamId,
code: u64,
},
StopSending {
id: StreamId,
code: u64,
},
ConnectionClose {
code: u64,
reason: String,
},
}

impl Frame {
pub fn encode(&self) -> BytesMut {
let mut buf = BytesMut::new();
match self {
Frame::OpenUni { id } => {
VarInt(0).encode(&mut buf);
VarInt(id.0).encode(&mut buf);
}
Frame::OpenBi { id } => {
VarInt(1).encode(&mut buf);
VarInt(id.0).encode(&mut buf);
}
Frame::Stream { id, data, fin } => {
VarInt(2).encode(&mut buf);
VarInt(id.0).encode(&mut buf);
VarInt(u64::from(*fin)).encode(&mut buf);
VarInt(data.len() as u64).encode(&mut buf);
buf.put_slice(data);
}
Frame::ResetStream { id, code } => {
VarInt(3).encode(&mut buf);
VarInt(id.0).encode(&mut buf);
VarInt(*code).encode(&mut buf);
}
Frame::StopSending { id, code } => {
VarInt(4).encode(&mut buf);
VarInt(id.0).encode(&mut buf);
VarInt(*code).encode(&mut buf);
}
Frame::ConnectionClose { code, reason } => {
VarInt(5).encode(&mut buf);
VarInt(*code).encode(&mut buf);
VarInt(reason.len() as u64).encode(&mut buf);
buf.put_slice(reason.as_bytes());
}
}
buf
}

pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, FrameDecodeError> {
let tag = VarInt::decode(buf)?.into_inner();
match tag {
0 => Ok(Frame::OpenUni {
id: StreamId(VarInt::decode(buf)?.into_inner()),
}),
1 => Ok(Frame::OpenBi {
id: StreamId(VarInt::decode(buf)?.into_inner()),
}),
2 => {
let id = StreamId(VarInt::decode(buf)?.into_inner());
let fin = VarInt::decode(buf)?.into_inner() != 0;
let len = VarInt::decode(buf)?.into_inner() as usize;
if buf.remaining() < len {
return Err(FrameDecodeError::UnexpectedEnd);
}
let mut data = vec![0u8; len];
buf.copy_to_slice(&mut data);
Ok(Frame::Stream {
id,
data: Bytes::from(data),
fin,
})
}
3 => Ok(Frame::ResetStream {
id: StreamId(VarInt::decode(buf)?.into_inner()),
code: VarInt::decode(buf)?.into_inner(),
}),
4 => Ok(Frame::StopSending {
id: StreamId(VarInt::decode(buf)?.into_inner()),
code: VarInt::decode(buf)?.into_inner(),
}),
5 => {
let code = VarInt::decode(buf)?.into_inner();
let len = VarInt::decode(buf)?.into_inner() as usize;
if buf.remaining() < len {
return Err(FrameDecodeError::UnexpectedEnd);
}
let mut data = vec![0u8; len];
buf.copy_to_slice(&mut data);
let reason = String::from_utf8(data).map_err(|_| FrameDecodeError::InvalidUtf8)?;
Ok(Frame::ConnectionClose { code, reason })
}
_ => Err(FrameDecodeError::UnknownTag(tag)),
}
}
}

#[derive(Debug, thiserror::Error)]
pub enum FrameDecodeError {
#[error("unexpected end of buffer")]
UnexpectedEnd,
#[error("unknown frame tag {0}")]
UnknownTag(u64),
#[error("invalid utf-8 in reason")]
InvalidUtf8,
}

impl From<VarIntUnexpectedEnd> for FrameDecodeError {
fn from(_: VarIntUnexpectedEnd) -> Self {
FrameDecodeError::UnexpectedEnd
}
}
Loading