use std::{sync::Arc, collections::HashMap, io::Read}; use async_trait::async_trait; use tokio::{net::{TcpListener, tcp::OwnedWriteHalf}, sync::RwLock}; use tokio_fastcgi::{Requests, RequestResult, Request}; // This is a little example of a REST API server implemeted in FastCGI. // // To include it into your Apache setup you can use the `proxy_fcgi` module. // The following configuration will pass all requests to the path `/api` to the // server application listening on port 8080. // // ``` conf // // ProxyPass "fcgi://127.0.0.1:8080/" enablereuse=on // // ``` // // To try out this example, configure Apache accordingly and start the example // by running `cargo run --example apiserver`. // // Now you can use curl to interact with it: // // # Adding a quote // // To add a quote execute `curl -X POST --fail-with-body -d 'My quote' http://localhost/api/quote`. // This will return the number of the quote. If no quotes are already there it // will return `1`. // // # Retrieving a quote // // After adding a quote you can execute `curl --fail-with-body http://localhost/api/quote/1` to // fetch the quote. If the quote does not exist a 404 error will be returned. // // # Update a quote // // Quotes can be updated by PUTing a new one. To try this out execute // `curl -X PUT --fail-with-body -d 'My updated quote' http://localhost/api/quote/1`. // If you try to PUT a non existent quote, it will be created with the given id. // You can check that the new quote was saved by GETing it. // // # Deleting a quote // // To delete a quote the DELTE HTTP method is used. A quote can be deleted // by calling `curl -X DELETE --fail-with-body http://localhost/api/quote/1`. // The DELETE request will return the deleted quote. // // # Having a second path // // To demonstrate how multiple paths can be handled, there is a `/api/ping` path // that only knows the `GET` method and will alwys return the string `pong`. /// Define some response codes to use. struct HttpResponse { code: u16, message: &'static str } impl HttpResponse { fn ok() -> Self { Self { code: 200, message: "Ok" } } fn e400() -> Self { Self { code: 400, message: "Bad Request" } } fn e404() -> Self { Self { code: 404, message: "Not Found" } } fn e405() -> Self { Self { code: 405, message: "Method Not Allowed" } } fn e500() -> Self { Self { code: 500, message: "Internal Server Error" } } } /// Request handler trait. All request handlers have to implement this async trait. /// The default implementation for every method returns the 405 (Method Not Allowed) error code. #[async_trait] trait RequestHandler { async fn get(_store: Arc>, _request: &Request, _selector: Option) -> Result { Err(HttpResponse::e405()) } async fn put(_store: Arc>, _request: &Request, _selector: Option) -> Result { Err(HttpResponse::e405()) } async fn post(_store: Arc>, _request: &Request, _selector: Option) -> Result { Err(HttpResponse::e405()) } async fn delete(_store: Arc>, _request: &Request, _selector: Option) -> Result { Err(HttpResponse::e405()) } } /// A simple data store implementation using a HashMap. struct Store { pub quotes: HashMap } impl Store { fn new() -> Self { Self { quotes: HashMap::default() } } } /// Handles the REST API for quotes. struct Quotes {} #[async_trait] impl RequestHandler for Quotes { /// Get returns the quote stored for the given selector u32 or 404 (Not Found). async fn get(store: Arc>, _request: &Request, selector: Option) -> Result { if let Some(selector) = selector { if let Some(quote) = store.read().await.quotes.get(&selector) { Ok(quote.clone()) } else { Err(HttpResponse::e404()) } } else { Err(HttpResponse::e404()) } } /// Put puts a quote into the given selector. /// If the selector is missing we return 405 (Method Not Allowed) because you can not call PUT on the root resource. async fn put(store: Arc>, request: &Request, selector: Option) -> Result { if let Some(selector) = selector { let mut quote = String::default(); if request.get_stdin().read_to_string(&mut quote).is_ok() { store.write().await.quotes.insert(selector, quote); Ok("".to_string()) } else { Err(HttpResponse::e500()) } } else { Err(HttpResponse::e405()) } } /// Post findes the next free selector u32 and puts the quote there. /// It returns the selector where the quote was stored. async fn post(store: Arc>, request: &Request, selector: Option) -> Result { // A post can only be done on the root resource not with a selector. // If a selector was passed return 405 - Method Not Allowed if selector.is_some() { return Err(HttpResponse::e405()); } let mut quote = String::default(); if request.get_stdin().read_to_string(&mut quote).is_ok() { let mut store = store.write().await; let next_free_selector = store.quotes.keys().max().unwrap_or(&0) + 1; store.quotes.insert(next_free_selector, quote); Ok(next_free_selector.to_string()) } else { Err(HttpResponse::e500()) } } /// Delete removes the quote described by the selector u32. /// If no selector is passed we return 405 (Method Not Allowed) because the root resource can not be deleted. async fn delete(store: Arc>, _request: &Request, selector: Option) -> Result { if let Some(selector) = selector { if let Some(quote) = store.write().await.quotes.remove(&selector) { Ok(quote) } else { Err(HttpResponse::e404()) } } else { Err(HttpResponse::e405()) } } } /// Handle ping requests on /api/ping struct Ping {} #[async_trait] impl RequestHandler for Ping { async fn get(_store: Arc>, _request: &Request, _selector: Option) -> Result { Ok("pong".to_string()) } } /// Encodes the HTTP status code and the response string and sends it back to the webserver. async fn send_response(request: Arc>, response_code: HttpResponse, data: Option<&str>) -> Result { request.get_stdout().write(format!("Status: {} {}\n\n", response_code.code, response_code.message).as_bytes()).await?; if let Some(data) = data { request.get_stdout().write(data.as_bytes()).await?; } Ok(RequestResult::Complete(0)) } /// Calles the appropriate method handler on a request handler. async fn method_handler(store: Arc>, request: &Request, selector: Option) -> Result { let method = request.get_str_param("request_method"); println!("Calling method {} on endpoint", method.unwrap_or_default()); match method { Some("GET") => H::get(store, request, selector).await, Some("PUT") => H::put(store, request, selector).await, Some("POST") => H::post(store, request, selector).await, Some("DELETE") => H::delete(store, request, selector).await, _ => Err(HttpResponse::e405()) } } /// Dispatcher that uses the endpoint name extracted from the URI path component to call the matching method handler. /// Every call is done via the generic function `method_hanlder` that does the dispatching of the method to a function /// implemented by the `RequestHandler` trait. async fn process_endpoint(store: Arc>, request: Arc>, endpoint: &str, selector: Option<&str>) -> Result { println!("Processing endpoint '{}' with selector '{}'", endpoint, selector.unwrap_or_default()); let result = match endpoint { "quote" => method_handler::(store, &request, selector.map(|s| s.parse().unwrap() )).await, "ping" => method_handler::(store, &request, selector.map(|s| s.parse().unwrap() )).await, _ => Err(HttpResponse::e404()) }; match result { Ok(result) => send_response(request, HttpResponse::ok(), Some(&result)).await, Err(http_response) => send_response(request, http_response, None).await } } /// Called by the `main` function if a new request as arrived via FastCGI. This function parses the request, /// extracts the URI path component and calls `process_endpoint` passing the different path elements. async fn process_request(store: Arc>, request: Arc>) -> Result { // Check that a `request_uri` parameter was passed by the webserver. If this is not the case, // fail with a HTTP 400 (Bad Request) error code. if let Some(request_uri) = request.get_str_param("request_uri").map(String::from) { // Split the request URI into the different path componets. // The following match is used to extract and verify the path compontens. let mut request_parts = request_uri.split_terminator('/').fuse(); match (request_parts.next(), request_parts.next(), request_parts.next(), request_parts.next(), request_parts.next()) { // Process /api/[/] (Some(""), Some("api"), Some(endpoint), selector, None) => process_endpoint(store, request, endpoint, selector).await, // Verything else will return HTTP 404 (Not Found) _ => send_response(request, HttpResponse::e404(), None).await } } else { send_response(request, HttpResponse::e400(), None).await } } #[tokio::main] async fn main() { let addr = "127.0.0.1:8080"; let listener = TcpListener::bind(addr).await.unwrap(); let store = Arc::new(RwLock::new(Store::new())); loop { let connection = listener.accept().await; // Accept new connections match connection { Err(err) => { println!("Establishing connection failed: {}", err); break; }, Ok((stream, address)) => { println!("Connection from {}", address); let conn_store = store.clone(); // If the socket connection was established successfully spawn a new task to handle // the requests that the webserver will send us. tokio::spawn(async move { // Create a new requests handler it will collect the requests from the server and // supply a streaming interface. let mut requests = Requests::from_split_socket(stream.into_split(), 10, 10); // Loop over the requests via the next method and process them. while let Ok(Some(request)) = requests.next().await { let req_store = conn_store.clone(); if let Err(err) = request.process(|request| async move { process_request(req_store.clone(), request).await.unwrap() }).await { // This is the error handler that is called if the process call returns an error. println!("Processing request failed: {}", err); } } }); } } } }