You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
4.3 KiB
174 lines
4.3 KiB
use std::fmt; |
|
|
|
use openssl::ssl::SslStream; |
|
use openssl::x509::X509; |
|
|
|
use url::Url; |
|
use std::{io::Write, net::TcpStream, str::FromStr}; |
|
use crate::error::{GempressError, GempressResult}; |
|
|
|
/// Gemini status codes as defined in Appendix 1 |
|
/// TODO: fill out remaining codes |
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)] |
|
pub enum StatusCode { |
|
Success = 20, |
|
NotFound = 51, |
|
} |
|
|
|
impl fmt::Display for StatusCode { |
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
write!(f, "{}", *self as u8) |
|
} |
|
} |
|
|
|
/// A gemini request |
|
pub struct Request { |
|
/// The requested resource path |
|
pub url: Url, |
|
pub certificate: Option<X509>, |
|
} |
|
|
|
impl Request { |
|
/// Parse a request object from text |
|
pub fn parse(request: &str) -> GempressResult<Self> { |
|
Self::from_str(request) |
|
} |
|
|
|
/// Get file path |
|
pub fn file_path(&self) -> &str { |
|
self.url |
|
.path() |
|
.chars() |
|
.next() |
|
.map_or("", |c| &self.url.path()[c.len_utf8()..]) |
|
} |
|
} |
|
|
|
impl FromStr for Request { |
|
type Err = GempressError; |
|
|
|
fn from_str(s: &str) -> GempressResult<Self> { |
|
let mut s = s.to_string(); |
|
|
|
// Add gemini: scheme if not explicitly set |
|
if s.starts_with("//") { |
|
s = format!("gemini:{}", s); |
|
} |
|
|
|
// Check protocol |
|
if let Some(proto_end) = s.find("://") { |
|
// If set, check if it's allowed |
|
let protocol = &s[..proto_end]; |
|
|
|
if protocol != "gemini" { |
|
// TODO: return 53 error instead of dropping |
|
return Err(GempressError::InvalidRequest("invalid protocol".into())); |
|
} |
|
} else { |
|
// If no protocol is found, gemini: is implied |
|
s = format!("gemini://{}", s); |
|
} |
|
|
|
// Extract and parse the url from the request. |
|
let raw = s |
|
.trim_end_matches(0x0 as char) |
|
.strip_suffix("\r\n") |
|
.ok_or_else(|| GempressError::InvalidRequest("malformed request".into()))?; |
|
let url = Url::parse(&raw) |
|
.map_err(|e| GempressError::InvalidRequest(format!("invalid url: {}", e)))?; |
|
|
|
let certificate: Option<X509> = None; |
|
|
|
Ok(Self { url, certificate }) |
|
} |
|
} |
|
|
|
/// A gemini response |
|
pub struct Response { |
|
pub status_code: StatusCode, |
|
pub meta: Vec<u8>, |
|
pub body: Vec<u8>, |
|
stream: SslStream<TcpStream>, |
|
} |
|
|
|
impl Response { |
|
pub(crate) fn new(stream: SslStream<TcpStream>) -> Self { |
|
Self { |
|
status_code: StatusCode::Success, |
|
meta: "text/gemini".into(), |
|
body: Vec::new(), |
|
stream, |
|
} |
|
} |
|
|
|
/// Set the status code of a response |
|
pub fn status(&mut self, code: StatusCode) -> GempressResult<&mut Self> { |
|
self.status_code = code; |
|
return Ok(self); |
|
} |
|
|
|
/// "Finish" the response and write it to the steeam |
|
pub fn send(&mut self, text: &[u8]) -> GempressResult<usize> { |
|
let mut buf: Vec<u8> = Vec::new(); |
|
|
|
// <Status> |
|
buf.extend(self.status_code.to_string().as_bytes()); |
|
|
|
// <Space> |
|
buf.push(0x20); |
|
|
|
// <Meta> |
|
buf.extend(&self.meta); |
|
|
|
buf.extend(b"\r\n"); |
|
|
|
buf.extend(self.body.clone()); |
|
|
|
buf.extend(text); |
|
|
|
self.stream |
|
.write(&buf) |
|
.map_err(GempressError::StreamWriteFailed) |
|
} |
|
} |
|
|
|
#[cfg(test)] |
|
mod tests { |
|
use super::*; |
|
|
|
fn check_request(raw: &str, expected_url: &str) { |
|
let req = Request::parse(raw).unwrap(); |
|
|
|
assert_eq!( |
|
req, |
|
Request { |
|
url: Url::parse(expected_url).unwrap() |
|
} |
|
); |
|
} |
|
|
|
#[test] |
|
fn parse_request() { |
|
check_request("gemini://example.space\r\n", "gemini://example.space"); |
|
} |
|
|
|
#[test] |
|
fn parse_without_scheme() { |
|
check_request("example.space\r\n", "gemini://example.space"); |
|
} |
|
|
|
#[test] |
|
fn parse_without_scheme_double_slash() { |
|
check_request("//example.space\r\n", "gemini://example.space"); |
|
} |
|
|
|
#[test] |
|
fn parse_malformed_request() { |
|
let raw = "gemini://example.space"; |
|
|
|
match Request::parse(raw) { |
|
Err(GempressError::InvalidRequest(_)) => {} |
|
x => panic!("expected GempressError::InvalidRequest, got: {:?}", x), |
|
} |
|
} |
|
}
|
|
|