Skip to content
This repository has been archived by the owner on Oct 17, 2021. It is now read-only.

Commit

Permalink
Port Skull132's HTTP system (tgstation#23)
Browse files Browse the repository at this point in the history
Adds in a HTTP system for use in a PR over at TG to auto-role discord members when they link their accounts. Could probably replace some world/Export calls in the future.
  • Loading branch information
AffectedArc07 authored Feb 19, 2020
1 parent 43c7181 commit bbc9891
Show file tree
Hide file tree
Showing 8 changed files with 1,277 additions and 17 deletions.
1,050 changes: 1,035 additions & 15 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ hex = { version = "0.3", optional = true }
percent-encoding = { version = "1.0", optional = true }
png = { version = "0.11.0", optional = true }
git2 = { version = "0.7.1", optional = true, default-features = false }
reqwest = { version = "0.9", optional = true, default-features = false, features = ["rustls-tls"] }
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true }
lazy_static = { version = "1.3", optional = true }

[features]
default = ["dmi", "log", "git"]
default = ["dmi", "log", "git", "http"]
dmi = ["png"]
file = []
hash = ["crypto-hash", "hex"]
log = ["chrono"]
url = ["percent-encoding"]
git = ["git2", "chrono"]
http = ["reqwest", "serde", "serde_json", "serde_derive", "lazy_static"]
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ On Windows, the output will be `target/release/rust_g.dll`.
For more advanced configuration, a list of modules may be passed:

```sh
cargo build --release --features dmi,file,log,url
cargo build --release --features dmi,file,log,url,http
```

* **dmi** (default): DMI manipulations which are impossible from within BYOND.
Expand All @@ -87,6 +87,7 @@ cargo build --release --features dmi,file,log,url
* hash: Faster replacement for `md5`, support for SHA-1, SHA-256, and SHA-512. Requires OpenSSL on Linux.
* **log** (default): Faster log output.
* url: Faster replacements for `url_encode` and `url_decode`.
* http: HTTP client to allow `GET`, `POST`, `PUT`, `PATCH`, `DELETE` and `HEAD`.

## Installing

Expand Down
19 changes: 19 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ fn main() {
// header
write!(f, r#"// rust_g.dm - DM API for rust_g extension library
#define RUST_G "rust_g"
#define RUSTG_JOB_NO_RESULTS_YET "NO RESULTS YET"
#define RUSTG_JOB_NO_SUCH_JOB "NO SUCH JOB"
#define RUSTG_JOB_ERROR "JOB PANICKED"
"#).unwrap();

// module: dmi
Expand Down Expand Up @@ -84,4 +88,19 @@ fn main() {
#endif
"#).unwrap();
}

// module: http
if enabled!("HTTP") {
write!(f, r#"
#define RUSTG_HTTP_METHOD_GET "get"
#define RUSTG_HTTP_METHOD_PUT "put"
#define RUSTG_HTTP_METHOD_DELETE "delete"
#define RUSTG_HTTP_METHOD_PATCH "patch"
#define RUSTG_HTTP_METHOD_HEAD "head"
#define RUSTG_HTTP_METHOD_POST "post"
#define rustg_http_request_blocking(method, url, body, headers) call(RUST_G, "http_request_blocking")(method, url, body, headers)
#define rustg_http_request_async(method, url, body, headers) call(RUST_G, "http_request_async")(method, url, body, headers)
#define rustg_http_check_request(req_id) call(RUST_G, "http_check_request")(req_id)
"#).unwrap();
}
}
20 changes: 20 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ pub enum Error {
#[cfg(feature="png")]
#[fail(display = "Invalid png data.")]
InvalidPngDataError,
#[cfg(feature="http")]
#[fail(display = "{}", _0)]
RequestError(#[cause] reqwest::Error),
#[cfg(feature="http")]
#[fail(display = "{}", _0)]
SerializationError(#[cause] serde_json::Error),
}

impl From<io::Error> for Error {
Expand Down Expand Up @@ -67,6 +73,20 @@ impl From<ParseIntError> for Error {
}
}

#[cfg(feature="http")]
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Error {
Error::RequestError(error)
}
}

#[cfg(feature="http")]
impl From<serde_json::Error> for Error {
fn from(error: serde_json::Error) -> Error {
Error::SerializationError(error)
}
}

impl From<Error> for String {
fn from(error: Error) -> String {
error.to_string()
Expand Down
119 changes: 119 additions & 0 deletions src/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use std::collections::hash_map::{ HashMap };
use std::collections::BTreeMap;

use error::Result;
use jobs;

// ----------------------------------------------------------------------------
// Interface

#[derive(Serialize)]
struct Response<'a> {
status_code: u16,
headers: HashMap<&'a str, &'a str>,
body: &'a str,
}

// If the response can be deserialized -> success.
// If the response can't be deserialized -> failure.
byond_fn! { http_request_blocking(method, url, body, headers) {
let req = match construct_request(method, url, body, headers) {
Ok(r) => r,
Err(e) => return Some(e.to_string())
};

match submit_request(req) {
Ok(r) => Some(r),
Err(e) => Some(e.to_string())
}
} }

// Returns new job-id.
byond_fn! { http_request_async(method, url, body, headers) {
let req = match construct_request(method, url, body, headers) {
Ok(r) => r,
Err(e) => return Some(e.to_string())
};

Some(jobs::start(move || {
match submit_request(req) {
Ok(r) => r,
Err(e) => e.to_string()
}
}))
} }

// If the response can be deserialized -> success.
// If the response can't be deserialized -> failure or WIP.
byond_fn! { http_check_request(id) {
Some(jobs::check(id))
} }

// ----------------------------------------------------------------------------
// Shared HTTP client state

const VERSION: &str = env!("CARGO_PKG_VERSION");
const PKG_NAME: &str = env!("CARGO_PKG_NAME");

fn setup_http_client() -> reqwest::Client {
use reqwest::{ Client, header::{ HeaderMap, USER_AGENT } };

let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, format!("{}/{}", PKG_NAME, VERSION).parse().unwrap());

Client::builder()
.default_headers(headers)
.build()
.unwrap()
}

lazy_static! {
static ref HTTP_CLIENT: reqwest::Client = setup_http_client();
}

// ----------------------------------------------------------------------------
// Request construction and execution

fn construct_request(method: &str, url: &str, body: &str, headers: &str) -> Result<reqwest::RequestBuilder> {
let mut req = match method {
"post" => HTTP_CLIENT.post(url),
"put" => HTTP_CLIENT.put(url),
"patch" => HTTP_CLIENT.patch(url),
"delete" => HTTP_CLIENT.delete(url),
"head" => HTTP_CLIENT.head(url),
_ => HTTP_CLIENT.get(url),
};

if !body.is_empty() {
req = req.body(body.to_owned());
}

if !headers.is_empty() {
let headers: BTreeMap<&str, &str> = serde_json::from_str(headers)?;
for (key, value) in headers {
req = req.header(key, value);
}
}

Ok(req)
}

fn submit_request(req: reqwest::RequestBuilder) -> Result<String> {
let mut response = req.send()?;

let body = response.text()?;

let mut resp = Response {
status_code: response.status().as_u16(),
headers: HashMap::new(),
body: &body,
};

for (key, value) in response.headers().iter() {
if let Ok(value) = value.to_str() {
resp.headers.insert(key.as_str(), value);
}
}

Ok(serde_json::to_string(&resp)?)
}
62 changes: 62 additions & 0 deletions src/jobs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Job system
use std::sync::mpsc;
use std::thread;
use std::collections::hash_map::{HashMap, Entry};
use std::cell::RefCell;

struct Job {
rx: mpsc::Receiver<Output>,
handle: thread::JoinHandle<()>,
}

type Output = String;
type JobId = String;

const NO_RESULTS_YET: &str = "NO RESULTS YET";
const NO_SUCH_JOB: &str = "NO SUCH JOB";
const JOB_PANICKED: &str = "JOB PANICKED";

#[derive(Default)]
struct Jobs {
map: HashMap<JobId, Job>,
next_job: usize,
}

impl Jobs {
fn start<F: FnOnce() -> Output + Send + 'static>(&mut self, f: F) -> JobId {
let (tx, rx) = mpsc::channel();
let handle = thread::spawn(move || {
let _ = tx.send(f());
});
let id = self.next_job.to_string();
self.next_job += 1;
self.map.insert(id.clone(), Job { rx, handle });
id
}

fn check(&mut self, id: &str) -> Output {
let entry = match self.map.entry(id.to_owned()) {
Entry::Occupied(occupied) => occupied,
Entry::Vacant(_) => return NO_SUCH_JOB.to_owned(),
};
let result = match entry.get().rx.try_recv() {
Ok(result) => result,
Err(mpsc::TryRecvError::Disconnected) => JOB_PANICKED.to_owned(),
Err(mpsc::TryRecvError::Empty) => return NO_RESULTS_YET.to_owned(),
};
let _ = entry.remove().handle.join();
result
}
}

thread_local! {
static JOBS: RefCell<Jobs> = Default::default();
}

pub fn start<F: FnOnce() -> Output + Send + 'static>(f: F) -> JobId {
JOBS.with(|jobs| jobs.borrow_mut().start(f))
}

pub fn check(id: &str) -> String {
JOBS.with(|jobs| jobs.borrow_mut().check(id))
}
13 changes: 13 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,22 @@ extern crate hex;
extern crate percent_encoding;
#[cfg(feature="png")]
extern crate png;
#[cfg(feature="http")]
extern crate reqwest;
#[cfg(feature="http")]
#[macro_use]
extern crate serde_derive;
#[cfg(feature="http")]
extern crate serde_json;
#[cfg(feature="http")]
#[macro_use]
extern crate lazy_static;

#[macro_use]
mod byond;
#[allow(dead_code)]
mod error;
mod jobs;

#[cfg(feature="dmi")]
pub mod dmi;
Expand All @@ -31,3 +42,5 @@ pub mod hash;
pub mod log;
#[cfg(feature="url")]
pub mod url;
#[cfg(feature="http")]
pub mod http;

0 comments on commit bbc9891

Please sign in to comment.