6LABQWDWWQUEDTXZBKETGLG65FZ66FJLLY5IW3IFCMYJUO65Q4BAC
use zhur_common::*;
use serde::{Deserialize, Serialize};
pub mod err;
pub use err::InvocationError;
pub mod http;
pub use http::*;
/// Struct representing a Zhur app invocation.
#[derive(Deserialize, Serialize, Debug)]
pub struct Invocation {
/// The name of the user/org to whom the app belongs.
pub owner: String,
/// The name of the app itself.
pub app_name: String,
/// The input for the app.
pub payload: Vec<u8>
}
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Deserialize, Serialize)]
/// A Serde-friendly representation of a HTTP request.
pub struct HttpReq {
/// The method of the request.
pub method: String,
/// The path of the request.
pub path: String,
/// Headers represented as pairs of `String`s.
pub headers: BTreeMap<String, String>,
/// Query strings represented as pairs of `String`s.
pub query_params: BTreeMap<String, String>,
/// Cookies represented as pairs of `String`s.
pub cookies: BTreeMap<String, String>,
/// The IP address of the requester.
pub ip_addr: String,
/// The body of the request, as bytes.
pub body: Vec<u8>
}
#[derive(Debug, Deserialize, Serialize)]
/// A Serde-friendly representation of an HTTP response.
pub struct HttpRes {
/// Headers represented as pairs of `String`s.
pub headers: BTreeMap<String, String>,
/// An optional `Set-Cookie` header. Setting multiple cookies is not supported and additional attributes aren't supported yet either.
pub set_cookie: Option<(String, String)>,
/// The body of the response, as bytes.
pub body: Vec<u8>,
/// The status code.
pub status: u16,
}
impl Default for HttpRes {
fn default() -> Self {
Self {
headers: BTreeMap::new(),
set_cookie: None,
body: vec![],
status: 200
}
}
}
use std::fmt::Display;
use super::*;
/// Everything that can go wrong with an invocation. Can be generated by the gateway or the core.
#[derive(Deserialize, Serialize, Debug)]
pub enum InvocationError {
// The variants below are gateway-side:
/// The HTTP request from Hyper could not be converted into a Serde-friendly form.
MalformedRequest,
/// No app was specified.
NoId,
/// No app could be identified with the information given.
MalformedId(String),
/// The gateway can't reach the core.
NoCore,
/// The core did not reply correctly.
MalformedReply,
/// The core did not respond within the timeout period.
TimedOut,
// The variants below are core-side:
/// No app identified by the two ID substrings exists, or it is disabled/hidden.
NoSuchApp(String, String),
/// An internal problem occurred within the core.
OtherInternal
}
impl Display for InvocationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self {
Self::MalformedRequest => "The HTTP request from Hyper could not be converted into a Serde-friendly form.".to_owned(),
Self::NoId => "No app was specified at all.".to_owned(),
Self::MalformedId(id) => format!("The ID \"{}\" is not a valid Zhur app ID.", id),
Self::NoCore => "The Zhur gateway could not connect to the Zhur core.".to_owned(),
Self::MalformedReply => "The reply from the Zhur core server could not be properly deserialized as an app's output.".to_owned(),
Self::TimedOut => "The Zhur core could be reached, but timed out before responding.".to_owned(),
Self::NoSuchApp(owner, app_name) => format!("The Zhur core could not find an app named {}:{}. It may have been disabled.", owner, app_name),
Self::OtherInternal => "The core encountered an internal error that prevented it from returning a proper reply.".to_owned()
};
f.write_str(&text)
}
}
# zhur_invk
Types common to `zhur_core` and `zhur_gate`.
[package]
name = "zhur_invk"
version = "0.1.0"
authors = ["oreganoli <3611916+oreganoli@users.noreply.github.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
zhur_common = { path = "../zhur_common" }
serde = { version = "1.0.118", features = ["derive"] }
let text = format!("Hi there, {}!", req.ip);
Ok(Response::new(text.into()))
let invocation = req.into_invoc().await;
// TODO: actually send the invocation to the core and try to get something back.
// Also TODO: extract all this stuff into its own function so we can ? it.
match &invocation {
Ok(i) => {
let text = format!("Got an OK invocation for {}:{} of length {}", &i.owner, &i.app_name, &i.payload.len());
info!("{}", &text);
Ok(Response::new(text.into()))
},
Err(e) => {
let text = format!("Got an invocation error: {}", e);
warn!("{}", &text);
Ok(Response::new(text.into()))
}
}
use std::collections::BTreeMap;
use super::{FullRequest, HttpReq, InvocationError};
use zhur_common::log::*;
use zhur_invk::Invocation;
/// Simplifies a `FullRequest` into an `HttpReq` of ours.
pub async fn simplify_req(req: FullRequest) -> Result<HttpReq, InvocationError> {
use http::header::COOKIE;
let method = req.req.method().to_string();
let uri = req.req.uri();
let path = uri.path().to_owned();
let query_params = match uri.query() {
Some(s) => parse_query_string(s),
None => BTreeMap::new()
};
// dbg!(&query_params);
let cookie_str = match req.req.headers().get(COOKIE) {
Some(hv) => match hv.to_str() {
Ok(s) => Some(s),
Err(_) => {
warn!("Got a Cookie string that is not a valid ASCII string. Discarding.");
None
}
},
None => None
};
let cookies = match cookie_str {
Some(s) => cookie_map(s),
None => BTreeMap::new()
};
let mut headers = BTreeMap::new();
for (name, val) in req.req.headers().iter() {
if name == "Cookie" {
continue;
}
match val.to_str() {
Ok(s) => {
headers.insert(name.to_string(), s.to_owned());
},
Err(_) => {
warn!("Found a header that could not be parsed as a string. Discarding.");
continue;
}
}
}
let body = match hyper::body::to_bytes(req.req.into_body()).await {
Ok(b) => b.to_vec(),
Err(_) => return Err(InvocationError::MalformedRequest)
};
Ok(HttpReq {
body,
path,
method,
cookies,
headers,
query_params,
ip_addr: req.ip.clone()
})
}
impl FullRequest {
/// Turns a `FullRequest` into an `Invocation`.
pub async fn into_invoc(self) -> Result<Invocation, InvocationError> {
use zhur_common::bincode::serialize;
use http::header::HOST;
let host = match self.req.headers().get(HOST) {
Some(s) => match s.to_str() {
Ok(s) => s,
Err(_) => {
warn!("Received an HTTP request with a non-UTF-text Host header, returning malformed ID error.");
return Err(InvocationError::MalformedId("(not valid UTF-8 text)".into()))
}
},
None => {
warn!("Received an HTTP request with no Host header, returning a no ID error.");
return Err(InvocationError::NoId)
}
}.to_owned();
let segments = host.split('.').collect::<Vec<_>>();
if segments.is_empty() {
warn!("Received an HTTP request with an empty Host header, returning a no ID error.");
return Err(InvocationError::NoId);
} else if segments.len() < 2 {
warn!("Received an HTTP request with a Host header that could not be transformed into an app ID: \"{}\"", host);
return Err(InvocationError::MalformedId(host.into()));
}
let req_simple = simplify_req(self).await?;
let req_bytes = match serialize(&req_simple) {
Ok(b) => b,
Err(_) => return Err(InvocationError::MalformedRequest)
};
let result = Invocation {
owner: segments[0].to_owned(),
app_name: segments[1].to_owned(),
payload: req_bytes
};
Ok(result)
}
}
/// Parses a query string of the form "a=b&c=d" into params. Note: Hyper takes care of extracting said string out of a URI already, so no need to worry about ?.
fn parse_query_string(s: &str) -> BTreeMap<String, String> {
let mut output = BTreeMap::new();
let pairs = s.split("&");
for pair in pairs {
let mut param_val = pair.split("=");
let param = match param_val.next() {
Some(p) => p,
None => continue
};
let val = match param_val.next() {
Some(v) => v,
None => continue
};
output.insert(param.to_owned(), val.to_owned());
}
output
}
/// Produces a map of cookies to their values given a cookie string.
fn cookie_map(cookie_str: &str) -> BTreeMap<String, String> {
let mut output = BTreeMap::new();
for cookie in cookie_str.split("; ") {
let mut name_val = cookie.split("=");
let name = match name_val.next() {
Some(n) => n,
None => continue
};
let val = match name_val.next() {
Some(v) => v,
None => continue
};
output.insert(name.to_owned(), val.to_owned());
}
output
}