From 2516fe269bd05cf0bd94472c738b2b14200d5385 Mon Sep 17 00:00:00 2001 From: Mingwei Samuel Date: Fri, 5 Jun 2020 01:51:25 -0700 Subject: [PATCH] Adding example API proxy project --- example_proxy/.gitignore | 1 + example_proxy/Cargo.toml | 12 +++ example_proxy/README.md | 20 +++++ example_proxy/src/main.rs | 149 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+) create mode 100644 example_proxy/.gitignore create mode 100644 example_proxy/Cargo.toml create mode 100644 example_proxy/README.md create mode 100644 example_proxy/src/main.rs diff --git a/example_proxy/.gitignore b/example_proxy/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/example_proxy/.gitignore @@ -0,0 +1 @@ +target diff --git a/example_proxy/Cargo.toml b/example_proxy/Cargo.toml new file mode 100644 index 0000000..4e206ea --- /dev/null +++ b/example_proxy/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "riven_example_proxy" +version = "0.0.0" +authors = [ "Mingwei Samuel " ] +edition = "2018" + +[dependencies] +riven = { path = '..' } + +hyper = "0.13" +lazy_static = "1.4" +tokio = { version = "0.2", features = [ "full" ] } diff --git a/example_proxy/README.md b/example_proxy/README.md new file mode 100644 index 0000000..21f2450 --- /dev/null +++ b/example_proxy/README.md @@ -0,0 +1,20 @@ +# Riven Example Proxy + +This is a simple example implementation of a Riot API proxy server using `hyper`. This adds the API key and forwards +requests to the Riot API, then returns and forwards responses back to the requester. It handles error cases but only +provides minimal failure information. HTTP requests will wait to complete when Riven is waiting on rate limits. + +Set `RGAPI_KEY` env var then run: +```bash +export RGAPI_KEY=RGAPI-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +cargo run +``` + +Test in your browser or using `curl`. The first path segment specifies the region: +```json +$ curl http://localhost:3000/na1/lol/summoner/v4/summoners/by-name/LugnutsK +{"id":"...","accountId":"...","puuid":"...","name":"LugnutsK","profileIconId":4540,"revisionDate":1589704662000,"summonerLevel":111} + +$ curl http://localhost:3000/na1/valorant/v4/players/by-name/LugnutsK # not yet :) +{"error":"Riot API endpoint method not found."} +``` diff --git a/example_proxy/src/main.rs b/example_proxy/src/main.rs new file mode 100644 index 0000000..68134a1 --- /dev/null +++ b/example_proxy/src/main.rs @@ -0,0 +1,149 @@ +// #![deny(warnings)] + +use std::convert::Infallible; + +use hyper::service::{ make_service_fn, service_fn }; +use hyper::{ Body, Method, Request, Response, Server, StatusCode }; +use hyper::header::HeaderValue; +use lazy_static::lazy_static; + +use riven::{ RiotApi, RiotApiConfig }; +use riven::consts::Region; + +lazy_static! { + /// Create lazy static RiotApi instance. + /// Easier than passing it around. + pub static ref RIOT_API: RiotApi = { + let api_key = std::env::var("RGAPI_KEY").ok() + .or_else(|| std::fs::read_to_string("../apikey.txt").ok()) + .expect("Failed to find RGAPI_KEY env var or apikey.txt."); + RiotApi::with_config(RiotApiConfig::with_key(api_key.trim()) + .preconfig_burst()) + }; +} + +/// Helper to create JSON error responses. +fn create_json_response(body: &'static str, status: StatusCode) -> Response { + let mut resp = Response::new(Body::from(body)); + *resp.status_mut() = status; + resp.headers_mut().insert(hyper::header::CONTENT_TYPE, HeaderValue::from_static("application/json")); + return resp; +} + +/// Main request handler service. +async fn handle_request(req: Request) -> Result, Infallible> { + + if Method::GET != req.method() { + return Ok(create_json_response(r#"{"error":"HTTP method must be GET."}"#, StatusCode::METHOD_NOT_ALLOWED)); + } + + // Handle path. + let path_data_opt = parse_path(req.uri().path()); + let ( region, method_id, req_path ) = match path_data_opt { + None => return Ok(create_json_response( + r#"{"error":"Riot API endpoint method not found."}"#, StatusCode::NOT_FOUND)), + Some(path_data) => path_data, + }; + + println!("Request to region {} path {}:\n\t{} {}", region, method_id, region, req_path); + + // Send request to Riot API. + let query = req.uri().query().map(|s| s.to_owned()); + let resp_result = RIOT_API.get_raw_response(method_id, region.into(), req_path.to_owned(), query).await; + let resp_info = match resp_result { + Err(_err) => return Ok(create_json_response( + r#"{"error":"Riot API request failed."}"#, StatusCode::INTERNAL_SERVER_ERROR)), + Ok(resp_info) => resp_info, + }; + + // Write output. + let api_response = resp_info.response; + let mut out_response = Response::default(); + *out_response.headers_mut() = api_response.headers().clone(); + + // If None, write "null" to body to be extra nice. + if resp_info.status_none { + *out_response.body_mut() = Body::from("null"); + } + // Otherwise copy body. + else { + *out_response.status_mut() = api_response.status(); + + // Using streams would be faster. + let bytes_result = api_response.bytes().await; + let bytes = match bytes_result { + Err(_err) => return Ok(create_json_response( + r#"{"error":"Failed to get body from Riot API response."}"#, StatusCode::INTERNAL_SERVER_ERROR)), + Ok(bytes) => bytes, + }; + *out_response.body_mut() = Body::from((&bytes[..]).to_vec()); + } + return Ok(out_response); +} + +/// Gets the region, method_id, and Riot API path based on the given path. +fn parse_path<'a>(req_path: &'a str) -> Option<( Region, &'static str, &'a str )> { + + // Split URI into region and rest of path. + let req_path = req_path.trim_start_matches('/'); + let ( region, req_path ) = req_path.split_at(req_path.find('/')?); + let region: Region = region.to_uppercase().parse().ok()?; + + // Find method_id for given path. + let method_id = find_matching_method_id(req_path)?; + + return Some(( region, method_id, req_path )) +} + +/// Finds the method_id given the request path. +fn find_matching_method_id(req_path: &str) -> Option<&'static str> { + for ( ref_path, method_id ) in &*riven::meta::ENDPOINT_PATH_METHODID { + if paths_match(ref_path, req_path) { + return Some(method_id); + } + } + None +} + +/// Checks if the request path (req_path) matches the reference path (ref_path). +fn paths_match(ref_path: &str, req_path: &str) -> bool { + let mut ref_iter = ref_path.split('/'); + let mut req_iter = req_path.split('/'); + loop { + let ref_seg_opt = ref_iter.next(); + let req_seg_opt = req_iter.next(); + if ref_seg_opt.is_none() != req_seg_opt.is_none() { + return false; + } + if let Some(ref_seg) = ref_seg_opt { + if let Some(req_seg) = req_seg_opt { + if ref_seg.starts_with('{') || ref_seg == req_seg { + continue; + } + return false; + }} + return true; + } +} + +#[tokio::main] +pub async fn main() -> Result<(), Box> { + // For every connection, we must make a `Service` to handle all + // incoming HTTP requests on said connection. + let make_svc = make_service_fn(|_conn| { + // This is the `Service` that will handle the connection. + // `service_fn` is a helper to convert a function that + // returns a Response into a `Service`. + async { Ok::<_, Infallible>(service_fn(handle_request)) } + }); + + let addr = ([ 127, 0, 0, 1 ], 3000).into(); + + let server = Server::bind(&addr).serve(make_svc); + + println!("Listening on http://{}", addr); + + server.await?; + + Ok(()) +}