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