every.channel: sanitized baseline

This commit is contained in:
every.channel 2026-02-15 16:17:27 -05:00
commit 897e556bea
No known key found for this signature in database
258 changed files with 74298 additions and 0 deletions

View file

@ -0,0 +1,319 @@
use std::{
collections::HashMap,
fmt,
net::{Ipv4Addr, SocketAddrV4},
str::FromStr,
};
use bytes::Bytes;
use clap::Parser;
use futures_lite::StreamExt;
use iroh::{
address_lookup::memory::MemoryLookup, Endpoint, EndpointAddr, PublicKey, RelayMode, RelayUrl,
SecretKey,
};
use iroh_gossip::{
api::{Event, GossipReceiver},
net::{Gossip, GOSSIP_ALPN},
proto::TopicId,
};
use n0_error::{bail_any, AnyError, Result, StdResultExt};
use n0_future::task;
use serde::{Deserialize, Serialize};
use serde_byte_array::ByteArray;
/// Chat over iroh-gossip
///
/// This broadcasts signed messages over iroh-gossip and verifies signatures
/// on received messages.
///
/// By default a new endpoint id is created when starting the example. To reuse your identity,
/// set the `--secret-key` flag with the secret key printed on a previous invocation.
///
/// By default, the relay server run by n0 is used. To use a local relay server, run
/// cargo run --bin iroh-relay --features iroh-relay -- --dev
/// in another terminal and then set the `-d http://localhost:3340` flag on this example.
#[derive(Parser, Debug)]
struct Args {
/// secret key to derive our endpoint id from.
#[clap(long)]
secret_key: Option<String>,
/// Set a custom relay server. By default, the relay server hosted by n0 will be used.
#[clap(short, long)]
relay: Option<RelayUrl>,
/// Disable relay completely.
#[clap(long)]
no_relay: bool,
/// Set your nickname.
#[clap(short, long)]
name: Option<String>,
/// Set the bind port for our socket. By default, a random port will be used.
#[clap(short, long, default_value = "0")]
bind_port: u16,
#[clap(subcommand)]
command: Command,
}
#[derive(Parser, Debug)]
enum Command {
/// Open a chat room for a topic and print a ticket for others to join.
///
/// If no topic is provided, a new topic will be created.
Open {
/// Optionally set the topic id (64 bytes, as hex string).
topic: Option<TopicId>,
},
/// Join a chat room from a ticket.
Join {
/// The ticket, as base32 string.
ticket: String,
},
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse();
// parse the cli command
let (topic, peers) = match &args.command {
Command::Open { topic } => {
let topic = topic.unwrap_or_else(|| TopicId::from_bytes(rand::random()));
println!("> opening chat room for topic {topic}");
(topic, vec![])
}
Command::Join { ticket } => {
let Ticket { topic, peers } = Ticket::from_str(ticket)?;
println!("> joining chat room for topic {topic}");
(topic, peers)
}
};
// parse or generate our secret key
let secret_key = match args.secret_key {
None => SecretKey::generate(&mut rand::rng()),
Some(key) => key.parse()?,
};
println!(
"> our secret key: {}",
data_encoding::HEXLOWER.encode(&secret_key.to_bytes())
);
// configure our relay map
let relay_mode = match (args.no_relay, args.relay) {
(false, None) => RelayMode::Default,
(false, Some(url)) => RelayMode::Custom(url.into()),
(true, None) => RelayMode::Disabled,
(true, Some(_)) => bail_any!("You cannot set --no-relay and --relay at the same time"),
};
println!("> using relay servers: {}", fmt_relay_mode(&relay_mode));
// create a memory lookup to pass in endpoint addresses to
let memory_lookup = MemoryLookup::new();
// build our magic endpoint
let endpoint = Endpoint::builder()
.secret_key(secret_key)
.address_lookup(memory_lookup.clone())
.relay_mode(relay_mode.clone())
.bind_addr(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, args.bind_port))?
.bind()
.await?;
println!("> our endpoint id: {}", endpoint.id());
// create the gossip protocol
let gossip = Gossip::builder().spawn(endpoint.clone());
// print a ticket that includes our own endpoint id and endpoint addresses
if !matches!(relay_mode, RelayMode::Disabled) {
// if we are expecting a relay, wait until we get a home relay
// before moving on
endpoint.online().await;
}
let ticket = {
let me = endpoint.addr();
let peers = peers.iter().cloned().chain([me]).collect();
Ticket { topic, peers }
};
println!("> ticket to join us: {ticket}");
// setup router
let router = iroh::protocol::Router::builder(endpoint.clone())
.accept(GOSSIP_ALPN, gossip.clone())
.spawn();
// join the gossip topic by connecting to known peers, if any
let peer_ids = peers.iter().map(|p| p.id).collect();
if peers.is_empty() {
println!("> waiting for peers to join us...");
} else {
println!("> trying to connect to {} peers...", peers.len());
// add the peer addrs from the ticket to our endpoint's addressbook so that they can be dialed
for peer in peers.into_iter() {
memory_lookup.add_endpoint_info(peer);
}
};
let (sender, receiver) = gossip.subscribe_and_join(topic, peer_ids).await?.split();
println!("> connected!");
// broadcast our name, if set
if let Some(name) = args.name {
let message = Message::AboutMe { name };
let encoded_message = SignedMessage::sign_and_encode(endpoint.secret_key(), &message)?;
sender.broadcast(encoded_message).await?;
}
// subscribe and print loop
task::spawn(subscribe_loop(receiver));
// spawn an input thread that reads stdin
// not using tokio here because they recommend this for "technical reasons"
let (line_tx, mut line_rx) = tokio::sync::mpsc::channel(1);
std::thread::spawn(move || input_loop(line_tx));
// broadcast each line we type
println!("> type a message and hit enter to broadcast...");
while let Some(text) = line_rx.recv().await {
let message = Message::Message { text: text.clone() };
let encoded_message = SignedMessage::sign_and_encode(endpoint.secret_key(), &message)?;
sender.broadcast(encoded_message).await?;
println!("> sent: {text}");
}
// shutdown
router.shutdown().await.anyerr()?;
Ok(())
}
async fn subscribe_loop(mut receiver: GossipReceiver) -> Result<()> {
// init a peerid -> name hashmap
let mut names = HashMap::new();
while let Some(event) = receiver.try_next().await? {
if let Event::Received(msg) = event {
let (from, message) = SignedMessage::verify_and_decode(&msg.content)?;
match message {
Message::AboutMe { name } => {
names.insert(from, name.clone());
println!("> {} is now known as {}", from.fmt_short(), name);
}
Message::Message { text } => {
let name = names
.get(&from)
.map_or_else(|| from.fmt_short().to_string(), String::to_string);
println!("{name}: {text}");
}
}
}
}
Ok(())
}
fn input_loop(line_tx: tokio::sync::mpsc::Sender<String>) -> Result<()> {
let mut buffer = String::new();
let stdin = std::io::stdin(); // We get `Stdin` here.
loop {
stdin.read_line(&mut buffer).anyerr()?;
line_tx.blocking_send(buffer.clone()).anyerr()?;
buffer.clear();
}
}
const SIGNATURE_LENGTH: usize = iroh::Signature::LENGTH;
type Signature = ByteArray<SIGNATURE_LENGTH>;
#[derive(Debug, Serialize, Deserialize)]
struct SignedMessage {
from: PublicKey,
data: Bytes,
signature: Signature,
}
impl SignedMessage {
pub fn verify_and_decode(bytes: &[u8]) -> Result<(PublicKey, Message)> {
let signed_message: Self =
postcard::from_bytes(bytes).std_context("decode signed message")?;
let key: PublicKey = signed_message.from;
key.verify(
&signed_message.data,
&iroh::Signature::from_bytes(&signed_message.signature),
)
.std_context("verify signature")?;
let message: Message =
postcard::from_bytes(&signed_message.data).std_context("decode message")?;
Ok((signed_message.from, message))
}
pub fn sign_and_encode(secret_key: &SecretKey, message: &Message) -> Result<Bytes> {
let data: Bytes = postcard::to_stdvec(&message)
.std_context("encode message")?
.into();
let signature = secret_key.sign(&data);
let from: PublicKey = secret_key.public();
let signed_message = Self {
from,
data,
signature: ByteArray::new(signature.to_bytes()),
};
let encoded = postcard::to_stdvec(&signed_message).std_context("encode signed message")?;
Ok(encoded.into())
}
}
#[derive(Debug, Serialize, Deserialize)]
enum Message {
AboutMe { name: String },
Message { text: String },
}
#[derive(Debug, Serialize, Deserialize)]
struct Ticket {
topic: TopicId,
peers: Vec<EndpointAddr>,
}
impl Ticket {
/// Deserializes from bytes.
fn from_bytes(bytes: &[u8]) -> Result<Self> {
postcard::from_bytes(bytes).std_context("decode ticket")
}
/// Serializes to bytes.
pub fn to_bytes(&self) -> Vec<u8> {
postcard::to_stdvec(self).expect("postcard::to_stdvec is infallible")
}
}
/// Serializes to base32.
impl fmt::Display for Ticket {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut text = data_encoding::BASE32_NOPAD.encode(&self.to_bytes()[..]);
text.make_ascii_lowercase();
write!(f, "{text}")
}
}
/// Deserializes from base32.
impl FromStr for Ticket {
type Err = AnyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = data_encoding::BASE32_NOPAD
.decode(s.to_ascii_uppercase().as_bytes())
.std_context("decode ticket base32")?;
Self::from_bytes(&bytes)
}
}
// helpers
fn fmt_relay_mode(relay_mode: &RelayMode) -> String {
match relay_mode {
RelayMode::Disabled => "None".to_string(),
RelayMode::Default => "Default Relay (production) servers".to_string(),
RelayMode::Staging => "Default Relay (staging) servers".to_string(),
RelayMode::Custom(map) => map
.urls::<Vec<_>>()
.into_iter()
.map(|url| url.to_string())
.collect::<Vec<_>>()
.join(", "),
}
}