init
This commit is contained in:
commit
acef45c75c
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
/cert/
|
||||
/public/
|
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "northstar"
|
||||
version = "0.1.0"
|
||||
authors = ["panicbit <panicbit.dev@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.33"
|
||||
tokio-rustls = "0.20.0"
|
||||
tokio = { version = "0.3.1", features = ["full"] }
|
||||
mime = "0.3.16"
|
||||
uriparse = "0.6.3"
|
||||
percent-encoding = "2.1.0"
|
||||
futures = "0.3.7"
|
||||
itertools = "0.9.0"
|
||||
log = "0.4.11"
|
33
README.md
Normal file
33
README.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
```
|
||||
__ __ __
|
||||
____ ____ _____/ /_/ /_ _____/ /_____ ______
|
||||
/ __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/
|
||||
/ / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / /
|
||||
/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/
|
||||
```
|
||||
|
||||
- [Documentation](https://docs.rs/northstar)
|
||||
- [GitHub](https://github.com/panicbit/northstar)
|
||||
|
||||
# Usage
|
||||
|
||||
Add the latest version of northstar to your `Cargo.toml`.
|
||||
|
||||
## Manually
|
||||
|
||||
```toml
|
||||
northstar = "0.1.0" # check crates.io for the latest version
|
||||
```
|
||||
|
||||
## Automatically
|
||||
|
||||
```sh
|
||||
cargo add northstar
|
||||
```
|
||||
|
||||
# Generating a key & certificate
|
||||
|
||||
```sh
|
||||
mkdir cert && cd cert
|
||||
openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365
|
||||
```
|
20
examples/serve_dir.rs
Normal file
20
examples/serve_dir.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use anyhow::*;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use northstar::{Server, Request, Response, GEMINI_PORT};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
Server::bind(("localhost", GEMINI_PORT))
|
||||
.serve(handle_request)
|
||||
.await
|
||||
}
|
||||
|
||||
fn handle_request(request: Request) -> BoxFuture<'static, Result<Response>> {
|
||||
async move {
|
||||
let path = request.path_segments();
|
||||
let response = northstar::util::serve_dir("public", &path).await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
.boxed()
|
||||
}
|
199
src/lib.rs
Normal file
199
src/lib.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
#[macro_use] extern crate log;
|
||||
|
||||
use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc};
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use mime::Mime;
|
||||
use tokio::{
|
||||
prelude::*,
|
||||
io::{self, BufStream},
|
||||
net::{TcpStream, ToSocketAddrs},
|
||||
};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_rustls::{rustls, TlsAcceptor};
|
||||
use rustls::*;
|
||||
use anyhow::*;
|
||||
use uri::URIReference;
|
||||
|
||||
pub mod types;
|
||||
pub mod util;
|
||||
|
||||
pub use mime;
|
||||
pub use uriparse as uri;
|
||||
pub use types::*;
|
||||
|
||||
pub const REQUEST_URI_MAX_LEN: usize = 1024;
|
||||
pub const GEMINI_PORT: u16 = 1965;
|
||||
|
||||
type Handler = Arc<dyn Fn(Request) -> HandlerResponse + Send + Sync>;
|
||||
type HandlerResponse = BoxFuture<'static, Result<Response>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Server {
|
||||
tls_acceptor: TlsAcceptor,
|
||||
listener: Arc<TcpListener>,
|
||||
handler: Handler,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn bind<A: ToSocketAddrs>(addr: A) -> Builder<A> {
|
||||
Builder::bind(addr)
|
||||
}
|
||||
|
||||
async fn serve(self) -> Result<()> {
|
||||
loop {
|
||||
let (stream, _addr) = self.listener.accept().await?;
|
||||
let this = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = this.serve_client(stream).await {
|
||||
error!("{}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_client(self, stream: TcpStream) -> Result<()> {
|
||||
let stream = self.tls_acceptor.accept(stream).await?;
|
||||
let mut stream = BufStream::new(stream);
|
||||
|
||||
let request = receive_request(&mut stream).await?;
|
||||
debug!("Client requested: {}", request.uri());
|
||||
|
||||
let handler = (self.handler)(request);
|
||||
let handler = AssertUnwindSafe(handler);
|
||||
|
||||
let response = handler.catch_unwind().await
|
||||
.unwrap_or_else(|_| Response::server_error(""))
|
||||
.or_else(|err| {
|
||||
error!("Handler: {}", err);
|
||||
Response::server_error("")
|
||||
})?;
|
||||
|
||||
send_response(response, &mut stream).await?;
|
||||
|
||||
stream.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Builder<A> {
|
||||
addr: A,
|
||||
}
|
||||
|
||||
impl<A: ToSocketAddrs> Builder<A> {
|
||||
fn bind(addr: A) -> Self {
|
||||
Self { addr }
|
||||
}
|
||||
|
||||
pub async fn serve<F>(self, handler: F) -> Result<()>
|
||||
where
|
||||
F: Fn(Request) -> HandlerResponse + Send + Sync + 'static,
|
||||
{
|
||||
let config = tls_config()?;
|
||||
|
||||
let server = Server {
|
||||
tls_acceptor: TlsAcceptor::from(config),
|
||||
listener: Arc::new(TcpListener::bind(self.addr).await?),
|
||||
handler: Arc::new(handler),
|
||||
};
|
||||
|
||||
server.serve().await
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_request(stream: &mut (impl AsyncBufRead + Unpin)) -> Result<Request> {
|
||||
let limit = REQUEST_URI_MAX_LEN + "\r\n".len();
|
||||
let mut stream = stream.take(limit as u64);
|
||||
let mut uri = Vec::new();
|
||||
|
||||
stream.read_until(b'\n', &mut uri).await?;
|
||||
|
||||
if !uri.ends_with(b"\r\n") {
|
||||
if uri.len() < REQUEST_URI_MAX_LEN {
|
||||
bail!("Request header not terminated with CRLF")
|
||||
} else {
|
||||
bail!("Request URI too long")
|
||||
}
|
||||
}
|
||||
|
||||
// Strip CRLF
|
||||
uri.pop();
|
||||
uri.pop();
|
||||
|
||||
let uri = URIReference::try_from(&*uri)?.into_owned();
|
||||
let request = Request::from_uri(uri)?;
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
async fn send_response(mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
||||
send_response_header(response.header(), stream).await?;
|
||||
|
||||
if let Some(body) = response.take_body() {
|
||||
send_response_body(body, stream).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
||||
let header = format!(
|
||||
"{status} {meta}\r\n",
|
||||
status = header.status.code(),
|
||||
meta = header.meta.as_str(),
|
||||
);
|
||||
|
||||
stream.write_all(header.as_bytes()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
||||
match body {
|
||||
Body::Bytes(bytes) => stream.write_all(&bytes).await?,
|
||||
Body::Reader(mut reader) => { io::copy(&mut reader, stream).await?; },
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tls_config() -> Result<Arc<ServerConfig>> {
|
||||
let mut config = ServerConfig::new(NoClientAuth::new());
|
||||
|
||||
let cert_chain = load_cert_chain()?;
|
||||
let key = load_key()?;
|
||||
config.set_single_cert(cert_chain, key)?;
|
||||
|
||||
Ok(config.into())
|
||||
}
|
||||
|
||||
fn load_cert_chain() -> Result<Vec<Certificate>> {
|
||||
let certs = std::fs::File::open("cert/cert.pem")?;
|
||||
let mut certs = BufReader::new(certs);
|
||||
let certs = rustls::internal::pemfile::certs(&mut certs)
|
||||
.map_err(|_| anyhow!("failed to load certs"))?;
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
fn load_key() -> Result<PrivateKey> {
|
||||
let mut keys = BufReader::new(std::fs::File::open("cert/key.pem")?);
|
||||
let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys)
|
||||
.map_err(|_| anyhow!("failed to load key"))?;
|
||||
|
||||
ensure!(!keys.is_empty(), "no key found");
|
||||
|
||||
let key = keys.swap_remove(0);
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
const GEMINI_MIME: &str = "text/gemini";
|
||||
|
||||
pub fn gemini_mime() -> Result<Mime> {
|
||||
let mime = GEMINI_MIME.parse()?;
|
||||
Ok(mime)
|
||||
}
|
||||
|
||||
|
287
src/types.rs
Normal file
287
src/types.rs
Normal file
|
@ -0,0 +1,287 @@
|
|||
use std::ops;
|
||||
use anyhow::*;
|
||||
use mime::Mime;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use tokio::{io::AsyncRead, fs::File};
|
||||
use uriparse::URIReference;
|
||||
|
||||
pub struct Request {
|
||||
uri: URIReference<'static>,
|
||||
input: Option<String>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn from_uri(mut uri: URIReference<'static>) -> Result<Self> {
|
||||
uri.normalize();
|
||||
|
||||
let input = match uri.query() {
|
||||
None => None,
|
||||
Some(query) => {
|
||||
let input = percent_decode_str(query.as_str())
|
||||
.decode_utf8()?
|
||||
.into_owned();
|
||||
Some(input)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
uri,
|
||||
input,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> &URIReference {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
pub fn path_segments(&self) -> Vec<String> {
|
||||
self.uri()
|
||||
.path()
|
||||
.segments()
|
||||
.iter()
|
||||
.map(|segment| percent_decode_str(segment.as_str()).decode_utf8_lossy().into_owned())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
pub fn input(&self) -> Option<&str> {
|
||||
self.input.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Request {
|
||||
type Target = URIReference<'static>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.uri
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
pub struct ResponseHeader {
|
||||
pub status: Status,
|
||||
pub meta: Meta,
|
||||
}
|
||||
|
||||
impl ResponseHeader {
|
||||
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
status: Status::INPUT,
|
||||
meta: Meta::new(prompt)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn success(mime: &Mime) -> Result<Self> {
|
||||
Ok(Self {
|
||||
status: Status::SUCCESS,
|
||||
meta: Meta::new(mime.to_string())?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn server_error(reason: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
status: Status::PERMANENT_FAILURE,
|
||||
meta: Meta::new(reason)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn not_found() -> Result<Self> {
|
||||
Ok(Self {
|
||||
status: Status::NOT_FOUND,
|
||||
meta: Meta::new("Not found")?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn status(&self) -> &Status {
|
||||
&self.status
|
||||
}
|
||||
|
||||
pub fn meta(&self) -> &Meta {
|
||||
&self.meta
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Copy,Clone,PartialEq,Eq)]
|
||||
pub struct Status(u8);
|
||||
|
||||
impl Status {
|
||||
pub const INPUT: Self = Self(10);
|
||||
pub const SENSITIVE_INPUT: Self = Self(11);
|
||||
pub const SUCCESS: Self = Self(20);
|
||||
pub const REDIRECT_TEMPORARY: Self = Self(30);
|
||||
pub const REDIRECT_PERMANENT: Self = Self(31);
|
||||
pub const TEMPORARY_FAILURE: Self = Self(40);
|
||||
pub const SERVER_UNAVAILABLE: Self = Self(41);
|
||||
pub const CGI_ERROR: Self = Self(42);
|
||||
pub const PROXY_ERROR: Self = Self(43);
|
||||
pub const SLOW_DOWN: Self = Self(44);
|
||||
pub const PERMANENT_FAILURE: Self = Self(50);
|
||||
pub const NOT_FOUND: Self = Self(51);
|
||||
pub const GONE: Self = Self(52);
|
||||
pub const PROXY_REQUEST_REFUSED: Self = Self(53);
|
||||
pub const BAD_REQUEST: Self = Self(59);
|
||||
pub const CLIENT_CERTIFICATE_REQUIRED: Self = Self(60);
|
||||
|
||||
pub fn code(&self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn is_success(&self) -> bool {
|
||||
self.category().is_success()
|
||||
}
|
||||
|
||||
pub fn category(&self) -> StatusCategory {
|
||||
let class = self.0 / 10;
|
||||
|
||||
match class {
|
||||
1 => StatusCategory::Input,
|
||||
2 => StatusCategory::Success,
|
||||
3 => StatusCategory::Redirect,
|
||||
4 => StatusCategory::TemporaryFailure,
|
||||
5 => StatusCategory::PermanentFailure,
|
||||
6 => StatusCategory::ClientCertificateRequired,
|
||||
_ => StatusCategory::PermanentFailure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy,Clone,PartialEq,Eq)]
|
||||
pub enum StatusCategory {
|
||||
Input,
|
||||
Success,
|
||||
Redirect,
|
||||
TemporaryFailure,
|
||||
PermanentFailure,
|
||||
ClientCertificateRequired,
|
||||
}
|
||||
|
||||
impl StatusCategory {
|
||||
pub fn is_input(&self) -> bool {
|
||||
*self == Self::Input
|
||||
}
|
||||
|
||||
pub fn is_success(&self) -> bool {
|
||||
*self == Self::Success
|
||||
}
|
||||
|
||||
pub fn redirect(&self) -> bool {
|
||||
*self == Self::Redirect
|
||||
}
|
||||
|
||||
pub fn is_temporary_failure(&self) -> bool {
|
||||
*self == Self::TemporaryFailure
|
||||
}
|
||||
|
||||
pub fn is_permanent_failure(&self) -> bool {
|
||||
*self == Self::PermanentFailure
|
||||
}
|
||||
|
||||
pub fn is_client_certificate_required(&self) -> bool {
|
||||
*self == Self::ClientCertificateRequired
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,Clone,PartialEq,Eq,Default)]
|
||||
pub struct Meta(String);
|
||||
|
||||
impl Meta {
|
||||
pub fn new(meta: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines");
|
||||
|
||||
Ok(Self(meta.into()))
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_mime(&self) -> Result<Mime> {
|
||||
let mime = self.as_str().parse::<Mime>()?;
|
||||
Ok(mime)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
header: ResponseHeader,
|
||||
body: Option<Body>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new(header: ResponseHeader) -> Self {
|
||||
Self {
|
||||
header,
|
||||
body: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
let header = ResponseHeader::input(prompt)?;
|
||||
Ok(Self::new(header))
|
||||
}
|
||||
|
||||
pub fn success(mime: &Mime) -> Result<Self> {
|
||||
let header = ResponseHeader::success(&mime)?;
|
||||
Ok(Self::new(header))
|
||||
}
|
||||
|
||||
pub fn server_error(reason: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
let header = ResponseHeader::server_error(reason)?;
|
||||
Ok(Self::new(header))
|
||||
}
|
||||
|
||||
pub fn not_found() -> Result<Self> {
|
||||
let header = ResponseHeader::not_found()?;
|
||||
Ok(Self::new(header))
|
||||
}
|
||||
|
||||
pub fn with_body(mut self, body: impl Into<Body>) -> Self {
|
||||
self.body = Some(body.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &ResponseHeader {
|
||||
&self.header
|
||||
}
|
||||
|
||||
pub fn take_body(&mut self) -> Option<Body> {
|
||||
self.body.take()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Body {
|
||||
Bytes(Vec<u8>),
|
||||
Reader(Box<dyn AsyncRead + Send + Sync + Unpin>),
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Body {
|
||||
fn from(bytes: Vec<u8>) -> Self {
|
||||
Self::Bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for Body {
|
||||
fn from(bytes: &[u8]) -> Self {
|
||||
Self::Bytes(bytes.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Body {
|
||||
fn from(text: String) -> Self {
|
||||
Self::Bytes(text.into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Body {
|
||||
fn from(text: &str) -> Self {
|
||||
Self::Bytes(text.to_owned().into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for Body {
|
||||
fn from(file: File) -> Self {
|
||||
Self::Reader(Box::new(file))
|
||||
}
|
||||
}
|
103
src/util.rs
Normal file
103
src/util.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use std::path::Path;
|
||||
use mime::Mime;
|
||||
use percent_encoding::utf8_percent_encode;
|
||||
use anyhow::*;
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
io,
|
||||
};
|
||||
use crate::{GEMINI_MIME, Response, gemini_mime};
|
||||
use itertools::Itertools;
|
||||
|
||||
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &Mime) -> Result<Response> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let file = match File::open(path).await {
|
||||
Ok(file) => file,
|
||||
Err(err) => match err.kind() {
|
||||
io::ErrorKind::NotFound => return Ok(Response::not_found()?),
|
||||
_ => return Err(err.into()),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Response::success(&mime)?.with_body(file))
|
||||
}
|
||||
|
||||
pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P]) -> Result<Response> {
|
||||
debug!("Dir: {}", dir.as_ref().display());
|
||||
let dir = dir.as_ref().canonicalize()?;
|
||||
let mut path = dir.to_path_buf();
|
||||
|
||||
for segment in virtual_path {
|
||||
path.push(segment);
|
||||
}
|
||||
|
||||
let path = path.canonicalize()?;
|
||||
|
||||
if !path.starts_with(&dir) {
|
||||
return Ok(Response::not_found()?);
|
||||
}
|
||||
|
||||
if !path.is_dir() {
|
||||
let mime = guess_mime_from_path(&path);
|
||||
return serve_file(path, &mime).await;
|
||||
}
|
||||
|
||||
serve_dir_listing(path, virtual_path).await
|
||||
}
|
||||
|
||||
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Result<Response> {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut dir = match fs::read_dir(path).await {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => match err.kind() {
|
||||
io::ErrorKind::NotFound => return Ok(Response::not_found()?),
|
||||
_ => return Err(err.into()),
|
||||
}
|
||||
};
|
||||
|
||||
let breadcrumbs = virtual_path.iter().map(|segment| segment.as_ref().display()).join("/");
|
||||
let mut listing = String::new();
|
||||
|
||||
writeln!(listing, "# Index of /{}", breadcrumbs)?;
|
||||
writeln!(listing)?;
|
||||
|
||||
if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) {
|
||||
writeln!(listing, "=> .. 📁 ../")?;
|
||||
}
|
||||
|
||||
while let Some(entry) = dir.next_entry().await? {
|
||||
let file_name = entry.file_name();
|
||||
let file_name = file_name.to_string_lossy();
|
||||
let is_dir = entry.file_type().await?.is_dir();
|
||||
|
||||
writeln!(
|
||||
listing,
|
||||
"=> {link}{trailing_slash} {icon} {name}{trailing_slash}",
|
||||
icon = if is_dir { '📁' } else { '📄' },
|
||||
link = utf8_percent_encode(&file_name, percent_encoding::NON_ALPHANUMERIC),
|
||||
trailing_slash = if is_dir { "/" } else { "" },
|
||||
name = file_name,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(Response::success(&gemini_mime()?)?.with_body(listing))
|
||||
}
|
||||
|
||||
pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> Mime {
|
||||
let path = path.as_ref();
|
||||
let extension = path.extension().and_then(|s| s.to_str());
|
||||
let mime = match extension {
|
||||
Some(extension) => match extension {
|
||||
"gemini" => GEMINI_MIME,
|
||||
"txt" => "text/plain",
|
||||
"jpeg" | "jpg" | "jpe" => "image/jpeg",
|
||||
"png" => "image/png",
|
||||
_ => "application/octet-stream",
|
||||
},
|
||||
None => "application/octet-stream",
|
||||
};
|
||||
|
||||
mime.parse::<Mime>().unwrap_or(mime::APPLICATION_OCTET_STREAM)
|
||||
}
|
Loading…
Reference in a new issue