use clap::Parser;
use core::fmt;
use http_body_util::Full;
use hyper::{
body::{Bytes, Incoming},
header::CONTENT_TYPE,
server::conn::http1,
Request, Response,
};
use hyper_util::rt::TokioIo;
use ssi_claims_core::VerificationParameters;
use ssi_dids::{VerificationMethodDIDResolver, DIDJWK};
use ssi_status::{any::AnyStatusMap, FromBytes, FromBytesOptions};
use std::{
fs,
io::{self, Read},
net::SocketAddr,
path::PathBuf,
process::ExitCode,
sync::Arc,
};
use tokio::net::TcpListener;
#[derive(Parser)]
#[clap(
name = "status-list-server",
bin_name = "cargo run --example status_list_server --"
)]
struct Args {
filename: Option<PathBuf>,
#[clap(short = 't', long)]
media_type: String,
#[clap(short, long)]
path: Option<String>,
#[clap(short, long)]
addr: Option<SocketAddr>,
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode {
let args = Args::parse();
match run(args).await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
async fn run(args: Args) -> Result<(), Error> {
let input = args.filename.map(Source::File).unwrap_or_default();
let bytes = match input.read() {
Ok(bytes) => bytes,
Err(e) => return Err(Error::ReadFile(input, e)),
};
let verifier =
VerificationParameters::from_resolver(VerificationMethodDIDResolver::new(DIDJWK));
let status_list = AnyStatusMap::from_bytes_with(
&bytes,
&args.media_type,
&verifier,
FromBytesOptions::ALLOW_UNSECURED,
)
.await
.map_err(|e| Error::Decode(input, e))?;
let path = status_list
.credential_url()
.map(|url| {
format!(
"{}{}{}",
url.path(),
url.query().map(|q| format!("?{q}")).unwrap_or_default(),
url.fragment().map(|f| format!("#{f}")).unwrap_or_default()
)
})
.or(args.path)
.ok_or(Error::MissingPath)?;
let configuration = Arc::new(Configuration {
path,
bytes,
media_type: args.media_type,
});
let addr = args
.addr
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 3000)));
println!("serving {} at {addr}...", configuration.path);
let listener = TcpListener::bind(addr).await?;
let service = Service { configuration };
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
let service = service.clone();
tokio::task::spawn(async move {
if let Err(e) = http1::Builder::new().serve_connection(io, service).await {
eprintln!("failed to serve connection: {e}")
}
});
}
}
struct Configuration {
path: String,
bytes: Vec<u8>,
media_type: String,
}
#[derive(Clone)]
struct Service {
configuration: Arc<Configuration>,
}
impl hyper::service::Service<Request<Incoming>> for Service {
type Response = Response<Full<Bytes>>;
type Error = hyper::Error;
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
fn call(&self, req: Request<Incoming>) -> Self::Future {
println!("serving `{}`", req.uri());
let response = if req.uri() == self.configuration.path.as_str() {
Response::builder()
.header(CONTENT_TYPE, self.configuration.media_type.as_str())
.body(Full::new(Bytes::from(self.configuration.bytes.clone())))
.unwrap()
} else {
Response::builder()
.status(404)
.body(Full::default())
.unwrap()
};
std::future::ready(Ok(response))
}
}
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
IO(#[from] io::Error),
#[error("unable to read {0}: {1}")]
ReadFile(Source, io::Error),
#[error("unable to decode {0}: {1}")]
Decode(Source, ssi_status::any::FromBytesError),
#[error("missing serve path")]
MissingPath,
}
#[derive(Debug, Default)]
enum Source {
#[default]
Stdin,
File(PathBuf),
}
impl Source {
fn read(&self) -> Result<Vec<u8>, io::Error> {
let mut buffer = Vec::new();
match self {
Self::Stdin => {
io::stdin().read_to_end(&mut buffer)?;
}
Self::File(path) => {
let mut file = fs::File::open(path)?;
file.read_to_end(&mut buffer)?;
}
}
Ok(buffer)
}
}
impl fmt::Display for Source {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Stdin => write!(f, "standard input"),
Self::File(path) => write!(f, "file `{}`", path.display()),
}
}
}