use crate::{api::*, layers::*}; use hyper::{ header, service::{make_service_fn, service_fn}, Body, HeaderMap, Method, Server, }; use std::error::Error; use tokio::sync::OnceCell; use uuid::Uuid; pub type HttpRequest = hyper::Request; pub type HttpResponse = hyper::Response; pub type HttpResult = ResultOrAnyErr; pub type HttpResultOrErr = Result; // keep the server alive statically // because we need it for the lifetime of the program static S3D: OnceCell = OnceCell::const_new(); #[derive(Debug)] pub struct HttpServer { api: Box, } impl HttpServer { /// Starts http server with s3 service. /// This should be called only once at the start of the program. pub async fn run() -> ResultOrAnyErr<()> { let addr = ([127, 0, 0, 1], 3000).into(); S3D.set(Self::new()).unwrap(); let server = Server::bind(&addr).serve(make_service_fn(|_| async { Ok::<_, AnyError>(service_fn(|req: HttpRequest| async { S3D.get().unwrap().handle_request(req).await })) })); println!("Listening on http://{}", addr); server.await?; Ok(()) } pub fn new() -> Self { let api = Box::new(MockLayer::new()); Self { api } } pub async fn handle_request(&self, mut req: HttpRequest) -> HttpResult { // generate request uuid let reqid = Uuid::new_v4().to_string(); println!( "==> HTTP {} {} {:?} [{}]", req.method(), req.uri(), &req.headers(), reqid, ); self.set_headers_reqid(req.headers_mut(), &reqid); let req = self.parse_request(req); let mut res = { self.handle_authorization(&req, &bucket, &key).await?; // ????????????????????????????????????????????????????????????????????????????????? // ??? does this '?' above return from the entire fn or just the current closure ??? // ????????????????????????????????????????????????????????????????????????????????? // call an op handler if req.method() == Method::OPTIONS { Ok(HttpResponse::new(Body::empty())) } else if bucket.is_empty() { self.handle_service_ops(&req).await } else if key.is_empty() { self.handle_bucket_ops(&req, &bucket).await } else { self.handle_object_ops(&req, &bucket, &key).await } } // handle errors .unwrap_or_else(|err| self.handle_error(&req, err)); // set response headers self.set_headers_reqid(res.headers_mut(), &reqid); self.set_headers_cors(&req, &mut res); println!( "<== HTTP {} {} {} {} {:?} [{}]", res.status().as_u16(), res.status(), req.method(), req.uri(), &res.headers(), reqid, ); Ok(res) } fn parse_request(&self, req: HttpRequest) -> (String, String) { // parse path style addressing for bucket names (TODO: host style addressing) assert!(req.uri().path().starts_with("/")); let path_items: Vec<_> = req.uri().path()[1..].splitn(2, "/").collect(); let (bucket, key) = match path_items.len() { 0 => ("", ""), 1 => (path_items[0], ""), 2 => (path_items[0], path_items[1]), _ => panic!("invalid path"), }; (bucket.to_owned(), key.to_owned()) } async fn handle_service_ops(&self, req: &HttpRequest) -> HttpResultOrErr { match *req.method() { // Method::GET => self // .api // .list_buckets(ListBucketsParams::from_request(req, "", "")) // .await // .into(), _ => Err(ApiError::from(ApiErrors::BadRequest)), } } async fn handle_bucket_ops( &self, req: &HttpRequest, _bucket: &str, ) -> HttpResultOrErr { match *req.method() { // Method::GET => self // .api // .list_objects(ListObjectsParams::parse(req, bucket, "")) // .await // .reply(), _ => Err(ApiError::from(ApiErrors::BadRequest)), } } async fn handle_object_ops( &self, req: &HttpRequest, _bucket: &str, _key: &str, ) -> HttpResultOrErr { match *req.method() { // Method::GET => self // .api // .get_object(GetObjectParams::parse(req, bucket, key)) // .await // .reply(), _ => Err(ApiError::from(ApiErrors::BadRequest)), } } fn set_headers_reqid(&self, h: &mut HeaderMap, reqid: &str) { let x_amz_request_id = header::HeaderName::from_static("x-amz-request-id"); let x_amz_id_2 = header::HeaderName::from_static("x-amz-id-2"); let reqid_val = header::HeaderValue::from_str(reqid).unwrap(); h.insert(x_amz_request_id, reqid_val.clone()); h.insert(x_amz_id_2, reqid_val.clone()); } fn set_headers_cors(&self, _req: &HttpRequest, res: &mut HttpResponse) { // note that browsers will not allow origin=* with credentials // but anyway we allow it by the agent server. let h = res.headers_mut(); h.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); h.insert( header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap(), ); h.insert( header::ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,PUT,DELETE,OPTIONS".parse().unwrap(), ); h.insert(header::ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type,Content-MD5,Authorization,X-Amz-User-Agent,X-Amz-Date,ETag,X-Amz-Content-Sha256".parse().unwrap()); h.insert( header::ACCESS_CONTROL_EXPOSE_HEADERS, "ETag,X-Amz-Version-Id".parse().unwrap(), ); } pub fn handle_error(&self, _req: &HttpRequest, _err: ApiError) -> HttpResponse { todo!() } pub async fn handle_authorization( &self, _req: &HttpRequest, _bucket: &str, _key: &str, ) -> HttpResultOrErr { todo!() } } // let res: HttpResult = match op_match { // LIST_BUCKETS => self // .api // .list_buckets(ListBucketsParams::parse(req, bucket, key)) // .await // .write(), // GET_BUCKET => self // .api // .get_bucket(get_bucket::Req::parse(req, bucket, key)) // .await // .write(), // PUT_BUCKET => self // .api // .put_bucket(put_bucket::Req::parse(req, bucket, key)) // .await // .write(), // DELETE_BUCKET => self // .api // .delete_bucket(delete_bucket::Req::parse(req, bucket, key)) // .await // .write(), // // LIST_OBJECTS => self // .api // .resolve_object_api(bucket) // .await // .list_objects(list_objects::Req::parse(req, bucket, key)) // .await // .write(), // GET_OBJECT | HEAD_OBJECT => self // .api // .resolve_object_api(bucket) // .await // .get_object(get_object::Req::parse(req, bucket, key)) // .await // .write(), // PUT_OBJECT => self // .api // .resolve_object_api(bucket) // .await // .put_object(put_object::Req::parse(req, bucket, key)) // .await // .write(), // DELETE_OBJECT => self // .api // .resolve_object_api(bucket) // .await // .delete_object(delete_object::Req::parse(req, bucket, key)) // .await // .write(), // // _ => Ok(ApiError::BadRequest).write(), // /// OpMatch is a tuple for choosing the requested op based on: // /// 1. the http method // /// 2. the existence of a bucket name in the host or path // /// 3. the existence of a key in the path // type OpMatch = (Method, bool, bool); // const LIST_BUCKETS: OpMatch = (Method::GET, false, false); // const LIST_OBJECTS: OpMatch = (Method::GET, true, false); // const GET_BUCKET: OpMatch = (Method::HEAD, true, false); // const GET_OBJECT: OpMatch = (Method::GET, true, true); // const HEAD_OBJECT: OpMatch = (Method::HEAD, true, true); // const PUT_BUCKET: OpMatch = (Method::PUT, true, false); // const PUT_OBJECT: OpMatch = (Method::PUT, true, true); // const DELETE_BUCKET: OpMatch = (Method::DELETE, true, false); // const DELETE_OBJECT: OpMatch = (Method::DELETE, true, true);