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

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),
}
}
}