use std::path::PathBuf;
use std::time::Duration;
use clap::Parser as _;
use modelpipe::{ConnectOptions, ServeOptions, Ticket, TokenPolicy};
mod cli;
mod diagnostics;
mod interrupt;
mod park;
use cli::{Cli, Command};
use interrupt::Interrupt;
use park::{FIRST_CONTACT, first_contact, park, shut_down};
fn token_policy(
token: Option<String>,
token_file: Option<PathBuf>,
insecure: bool,
) -> anyhow::Result<TokenPolicy> {
Ok(match (token, token_file, insecure) {
(Some(t), _, _) if t.trim().is_empty() => {
anyhow::bail!("the bearer token is empty — unset MODELPIPE_TOKEN or pass a value")
}
(Some(t), _, _) => TokenPolicy::Supplied(t),
(None, Some(path), _) => {
let raw = std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("could not read {}: {e}", path.display()))?;
let trimmed = raw.trim_end_matches(['\n', '\r']).to_owned();
if trimmed.is_empty() {
anyhow::bail!("{} is empty", path.display());
}
TokenPolicy::Supplied(trimmed)
}
(None, None, true) => TokenPolicy::InsecureNoAuth,
(None, None, false) => TokenPolicy::Generate,
})
}
fn token_line(supplied: bool, token: Option<String>) -> Option<String> {
let token = token?;
Some(if supplied {
"token: (supplied)".to_owned()
} else {
format!("token: {token}")
})
}
fn qr(ticket: &Ticket) -> Option<String> {
use qrcode::QrCode;
use qrcode::render::unicode;
let code = QrCode::new(ticket.to_string().to_uppercase()).ok()?;
Some(code.render::<unicode::Dense1x2>().quiet_zone(true).build())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
diagnostics::install(cli.verbose);
let mut interrupt = Interrupt::new()?;
match cli.command {
Command::Serve {
backend_url,
insecure_no_auth,
token,
token_file,
allow_private_backend,
relay,
identity,
no_qr,
no_portmap,
no_discovery,
} => {
let mut opts = ServeOptions::default();
opts.auth = token_policy(token, token_file, insecure_no_auth)?;
let supplied = matches!(opts.auth, TokenPolicy::Supplied(_));
opts.allow_private_backend = allow_private_backend;
opts.relay = relay;
let ephemeral = identity.is_none();
opts.identity = identity;
opts.port_mapping = !no_portmap;
opts.discovery = !no_discovery;
opts.wait_online = Some(Duration::from_secs(10));
eprintln!("finding a relay…");
let mut handle = modelpipe::serve(&backend_url, opts).await?;
let ticket = handle.ticket();
println!("ticket: {ticket}");
match token_line(supplied, handle.token()) {
Some(line) => println!("{line}"),
None => eprintln!(
"WARNING: serving open — anyone holding the ticket can use your backend"
),
}
if ephemeral {
eprintln!(
"note: this ticket dies when serve restarts — \
pass --identity <file> to keep it across restarts"
);
}
if !no_qr && let Some(code) = qr(&ticket) {
println!("\n{code}");
}
park(&mut handle, &mut interrupt).await?;
shut_down(handle.shutdown(), &mut interrupt).await;
}
Command::Connect {
ticket,
bind,
relay,
no_portmap,
no_discovery,
} => {
let ticket: Ticket = ticket.parse()?;
if let Some(addr) = bind
&& !addr.ip().is_loopback()
{
eprintln!(
"WARNING: binding {addr} exposes the pipe beyond this machine — anyone who can reach that port can reach the backend (with the token)"
);
}
let mut opts = ConnectOptions::default();
opts.bind = bind;
opts.relay = relay;
opts.port_mapping = !no_portmap;
opts.discovery = !no_discovery;
let mut handle = modelpipe::connect(&ticket, opts).await?;
eprintln!("reaching the serve side…");
first_contact(&mut handle, FIRST_CONTACT).await?;
println!("{}", handle.base_url());
park(&mut handle, &mut interrupt).await?;
shut_down(handle.shutdown(), &mut interrupt).await;
}
}
Ok(())
}
#[cfg(test)]
#[path = "main_tests.rs"]
mod main_tests;