use anyhow::{Context, Result, bail};
use clap::Args;
use hifitime::efmt::Format;
use hifitime::efmt::Formatter;
use hypertext::Buffer;
use hypertext::prelude::*;
use smol::io::AsyncBufReadExt;
use smol::io::AsyncWriteExt;
use smol::io::BufReader;
use smol::net::TcpListener;
use smol::net::TcpStream;
use smol::stream::StreamExt;
use std::cmp::Ordering;
use std::cmp::Reverse;
use std::path::PathBuf;
use std::str::FromStr;
use ufotofu::consumer::compat::fn_mut::ClosureConsumer;
use ufotofu::consumer::compat::writer::writer_to_bulk_consumer;
use ufotofu::prelude::*;
use ufotofu::queues::new_unbounded_elastic;
use url::Url;
use willow25::Timestamp;
use willow25::authorisation::AuthorisedEntry;
use willow25::entry::Entrylike;
use willow25::entry::{NamespaceId, SubspaceId};
use willow25::groupings::Area;
use willow25::groupings::Coordinatelike;
use willow25::groupings::TimeRange;
use willow25::path;
use willow25::paths::Path;
use willow25::storage::PersistentStoreError;
use willow25::storage::StoreOrConsumerError;
use willow25::storage::{PersistentStore, Store};
use crate::logo::SNEAKER_FRIEND;
use crate::util::BASE32_ALPHABET;
use crate::util::emph_style;
use crate::util::oh_no;
use crate::util::sneakerweb_dir;
use crate::util::warn_deprecated_domain_encoding;
use crate::util::yay;
use crate::util::{SNEAKERWEB_NAMESPACE_ID_BYTES, get_domain};
#[derive(Args)]
pub struct ServeArgs {
pub collection: Option<PathBuf>,
}
pub async fn domain_is_empty(
store: &mut PersistentStore,
subspace_id: &SubspaceId,
) -> Result<bool, PersistentStoreError> {
let namespace = NamespaceId::from_bytes(&SNEAKERWEB_NAMESPACE_ID_BYTES);
let domain_area = Area::new(Some(subspace_id.clone()), Path::new(), TimeRange::full());
let mut entry_count = 0;
let mut counting_consumer = ClosureConsumer::new(
async |item_or_final: Either<AuthorisedEntry, ()>| match item_or_final {
Left(_) => {
entry_count += 1;
if entry_count > 1 { Err(()) } else { Ok(()) }
}
Right(()) => Ok(()),
},
);
match store
.get_area(&namespace, &domain_area, &mut counting_consumer)
.await
{
Err(err) => {
match err {
StoreOrConsumerError::ConsumerError(()) => {
Ok(false)
}
StoreOrConsumerError::StoreError(store_err) => Err(store_err),
}
}
Ok(()) => Ok(true),
}
}
pub async fn start_server(args: &ServeArgs) -> Result<()> {
let listener = TcpListener::bind("0.0.0.0:1312").await.context("failed to bind port 1312 to serve the sneakerweb. Is it in use, or do you need to set permissions?")?;
yay(&format!(
"serving your sneakerweb at {}",
emph_style("http://sneakerweb.localhost:1312/")
))
.await;
let mut incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let mut stream = stream?;
if let Err(err) = handle_connection(stream.clone(), args.collection.as_ref()).await {
stream
.write_all(format!("HTTP/1.1 404 NOT FOUND\r\n\r\n{err}\r\n").as_bytes())
.await?;
}
}
Ok(())
}
enum FrontPageOrder {
Domain,
Newest,
Oldest,
Sneakiest,
}
enum Domain {
Homepage(FrontPageOrder),
Subspace(SubspaceId),
}
const FRONT_CSS: &str = include_str!("frontpage.css");
async fn handle_connection(stream: TcpStream, collection: Option<&PathBuf>) -> Result<()> {
let mut buf_reader = BufReader::new(stream.clone());
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut req = httparse::Request::new(&mut headers);
buf_reader.fill_buf().await?;
req.parse(buf_reader.buffer())?;
let mut domain = Domain::Homepage(FrontPageOrder::Domain);
let mut parsed_hostname = false;
let mut hostname = "sneakerweb".to_string();
for header in req.headers {
if header.name == "Host" {
let hostname_str = std::str::from_utf8(header.value).unwrap();
let mut host_iterator = hostname_str.split('.');
let host_fst = match host_iterator.next() {
Some(host_fst) => host_fst,
None => {
bail!("somehow, the request URL had no hostname");
}
};
if let Ok((subspace_id, _encoded)) = get_domain(Some(host_fst), "N/A") {
warn_deprecated_domain_encoding(host_fst, &subspace_id).await;
domain = Domain::Subspace(subspace_id);
hostname = host_fst.to_string();
} else if host_fst != "sneakerweb" {
bail!("hostname {} wasn't a Subspace ID or the homepage", host_fst)
} else {
match req.path {
Some("/?sort=oldest") => domain = Domain::Homepage(FrontPageOrder::Oldest),
Some("/?sort=newest") => domain = Domain::Homepage(FrontPageOrder::Newest),
Some("/?sort=sneakiest") => {
domain = Domain::Homepage(FrontPageOrder::Sneakiest)
}
_ => {}
}
}
let tld = host_iterator.next();
let Some("localhost:1312") = tld else {
bail!("TLD {tld:?} was not .localhost:1312")
};
if let Some(_whatever) = host_iterator.next() {
bail!("too many labels in the hostname")
}
parsed_hostname = true;
}
}
if !parsed_hostname {
bail!("did not find a Host header in the request")
}
let mut url_to_parse = "http://".to_string();
url_to_parse.push_str(&hostname);
url_to_parse.push_str(".localhost:1312");
if let Some(path) = req.path {
url_to_parse.push_str(path);
}
let url = Url::parse(&url_to_parse).unwrap();
match domain {
Domain::Homepage(order) => {
let mut buffer = Buffer::new();
let sneakerweb_fs_path = sneakerweb_dir(collection).await.context(
"could not retrieve the filesystem path to sneakerweb configuration and storage",
)?;
let mut store = PersistentStore::new(&sneakerweb_fs_path)
.await
.context("could not open sneakerweb storage")?;
render_frontpage(&mut store, order, &mut buffer).await?;
let mut http_consumer =
writer_to_bulk_consumer(stream.clone(), new_unbounded_elastic());
let response = "HTTP/1.1 200 OK\r\n\r\n";
if http_consumer
.consume_full_slice(response.as_bytes())
.await
.is_err()
{
oh_no("failed to write http headers").await;
return Ok(());
}
let frontpage_rendered = buffer.rendered();
if http_consumer
.consume_full_slice(frontpage_rendered.as_inner().as_bytes())
.await
.is_err()
{
oh_no("failed to write frontpage HTML").await;
return Ok(());
}
Ok(())
}
Domain::Subspace(subspace_id) => {
let path = match url.path_segments() {
Some(segments) => {
let segment_bytes: Vec<&[u8]> = segments
.map(|segment| {
if segment.is_empty() {
"index.html".as_bytes()
} else {
segment.as_bytes()
}
})
.collect();
Path::from_slices(&segment_bytes)
}
None => Ok(Path::new()),
};
let valid_path = match path {
Ok(p) => p,
Err(_) => bail!("the request's path was not a valid sneakerweb storage path"),
};
let fallback_path = if url
.path_segments()
.and_then(|mut segments| segments.next_back())
.is_some_and(|segment| !segment.is_empty() && !segment.contains('.'))
{
let mut segment_bytes: Vec<&[u8]> =
url.path_segments().unwrap().map(str::as_bytes).collect();
segment_bytes.push("index.html".as_bytes());
Some(Path::from_slices(&segment_bytes))
} else {
None
};
let valid_fallback_path = match fallback_path {
Some(Ok(path)) => Some(path),
Some(Err(_)) => {
bail!("the request's fallback path was not a valid sneakerweb storage path")
}
None => None,
};
let sneakerweb_fs_path = sneakerweb_dir(collection).await.context(
"could not retrieve the filesystem path to sneakerweb configuration and storage",
)?;
let mut store = PersistentStore::new(&sneakerweb_fs_path)
.await
.context("could not open sneakerweb storage")?;
let namespace = NamespaceId::from_bytes(&SNEAKERWEB_NAMESPACE_ID_BYTES);
let key = (subspace_id.clone(), valid_path.clone());
let (resolved_path, resolved_key, entry) =
match store.get_entry(&namespace, &key, None).await? {
Some(entry) => (valid_path.clone(), key, entry),
None => match valid_fallback_path {
Some(fallback_path) => {
let fallback_key = (subspace_id, fallback_path.clone());
match store.get_entry(&namespace, &fallback_key, None).await? {
Some(entry) => (fallback_path, fallback_key, entry),
None => bail!("no entry found at {valid_path} or {fallback_path}"),
}
}
None => bail!("no entry found at {valid_path}"),
},
};
let resolved_path_str = resolved_path.to_string();
let etag = base32::encode(BASE32_ALPHABET, entry.payload_digest().as_bytes());
let mut http_consumer =
writer_to_bulk_consumer(stream.clone(), new_unbounded_elastic());
let mut response = "HTTP/1.1 200 OK\r\n".to_string();
let mime_guesses = mime_guess2::from_path(&resolved_path_str);
if let Some(mime_guess) = mime_guesses.first() {
response.push_str("content-type: ");
response.push_str(mime_guess.essence_str());
response.push_str("\r\n");
}
response.push_str(&format!("etag: \"{etag}\"\r\n"));
response.push_str("\r\n");
if http_consumer
.consume_full_slice(response.as_bytes())
.await
.is_err()
{
oh_no("failed to write http headers").await;
return Ok(());
}
if let Err(err) = store
.get_payload_slice(
&namespace,
&resolved_key,
None,
0,
u64::MAX,
&mut http_consumer,
)
.await
{
oh_no(&format!("failed to write response body: {}", err)).await;
}
if http_consumer.flush().await.is_err() {
oh_no("failed to flush data to http stream").await;
}
Ok(())
}
}
}
struct DomainListing {
id: String,
last_updated: Timestamp,
has_index: bool,
has_preview: bool,
}
impl DomainListing {
fn sneakier(&self, other: &Self) -> Ordering {
match (
(self.has_index, self.has_preview),
(other.has_index, other.has_preview),
) {
((false, false), (false, true)) => Ordering::Less,
((false, false), (true, false)) => Ordering::Less,
((false, false), (true, true)) => Ordering::Less,
((false, true), (false, false)) => Ordering::Greater,
((false, true), (true, false)) => Ordering::Greater,
((false, true), (true, true)) => Ordering::Less,
((true, false), (false, false)) => Ordering::Greater,
((true, false), (false, true)) => Ordering::Less,
((true, false), (true, true)) => Ordering::Less,
((true, true), (false, false)) => Ordering::Greater,
((true, true), (false, true)) => Ordering::Greater,
((true, true), (true, false)) => Ordering::Greater,
_ => self.last_updated.cmp(&other.last_updated),
}
}
fn root_url(&self) -> String {
format!("http://{}.localhost:1312", self.id)
}
fn preview_url(&self) -> Option<String> {
if self.has_preview {
Some(format!("{}/sneakerweb.html", self.root_url()))
} else {
None
}
}
fn format_last_updated(&self) -> String {
let fmt = Formatter::new(
self.last_updated.into(),
Format::from_str("%Y-%m-%d").unwrap(),
);
format!("{}", fmt)
}
}
#[component]
fn site_metadata<'a>(listing: &'a DomainListing) -> impl Renderable {
rsx! {
<div class="metadata">
<span class="public-key">
@if listing.has_index {
<a href={(listing.root_url())}>(listing.id)</a>
} @else {
(listing.id)
}
</span>
<span class="last-updated">(&listing.format_last_updated())</span>
</div>
}
}
impl Renderable for DomainListing {
fn render_to(&self, buffer: &mut Buffer<hypertext::context::Node>) {
match self.preview_url() {
Some(url) => {
rsx! {
<li>
<iframe class="preview" loading="lazy" sandbox="allow-popups allow-popups-to-escape-sandbox" src={url}></iframe>
(SiteMetadata { listing: self })
</li>
}
.render_to(buffer);
}
None => {
rsx! {
<li>
<div class="previewless">
"i forgot to include a sneakerweb.html"
@if !self.has_index {
" (or even an index.html)"
}
</div>
(SiteMetadata { listing: self })
</li>
}
.render_to(buffer);
}
}
}
}
async fn render_frontpage(
store: &mut PersistentStore,
order: FrontPageOrder,
buffer: &mut Buffer,
) -> Result<()> {
let namespace = NamespaceId::from_bytes(&SNEAKERWEB_NAMESPACE_ID_BYTES);
let mut subspace_consumer = vec![].into_consumer();
store
.subspaces(&namespace, &mut subspace_consumer)
.await
.context("could not get known subspaces from the store")?;
let subspaces_vec: Vec<SubspaceId> = subspace_consumer.into();
let index_path = path!("/index.html");
let sneakerweb_path = path!("/sneakerweb.html");
let mut listing_vec: Vec<DomainListing> = vec![];
for subspace_id in subspaces_vec.iter() {
let b16_id = base32::encode(BASE32_ALPHABET, subspace_id.as_bytes());
let index_entry = store
.get_entry(&namespace, &(subspace_id.clone(), index_path.clone()), None)
.await
.context("could not get index.html from subspace")?;
let sneakerweb_entry = store
.get_entry(
&namespace,
&(subspace_id.clone(), sneakerweb_path.clone()),
None,
)
.await
.context("could not get index.html from subspace")?;
let last_updated = match (&index_entry, &sneakerweb_entry) {
(Some(index), Some(sneakerweb)) => index.timestamp().max(sneakerweb.timestamp()),
(Some(index), None) => index.timestamp(),
(None, Some(sneakerweb)) => sneakerweb.timestamp(),
(None, None) => Timestamp::from(0),
};
if !domain_is_empty(store, subspace_id).await? {
let listing = DomainListing {
id: b16_id,
last_updated,
has_index: index_entry.is_some(),
has_preview: sneakerweb_entry.is_some(),
};
listing_vec.push(listing);
}
}
match order {
FrontPageOrder::Domain => listing_vec.sort_by(|a, b| a.id.cmp(&b.id)),
FrontPageOrder::Newest => listing_vec.sort_by_key(|a| Reverse(a.last_updated)),
FrontPageOrder::Oldest => listing_vec.sort_by_key(|a| a.last_updated),
FrontPageOrder::Sneakiest => listing_vec.sort_by(|a, b| b.sneakier(a)),
}
let listing_rsx = rsx! {
<!DOCTYPE html>
<html>
<head>
<title>sneakerweb</title>
<meta charset="utf-8"/>
<style>(FRONT_CSS)</style>
</head>
<body>
<div>
<header><img alt="" src={SNEAKER_FRIEND} />
<nav>Sort by
<a href="/">Domain</a>
<a href="/?sort=newest">Newest</a>
<a href="/?sort=oldest">Oldest</a>
<a href="/?sort=sneakiest">Sneakiest</a>
</nav>
</header>
<ul id="sites">
@for listing in listing_vec.iter() {
(listing)
}
</ul>
</div>
</body>
</html>
};
listing_rsx.render_to(buffer);
Ok(())
}