1mod http_client;
5
6#[macro_use]
7extern crate wascc_codec as codec;
8
9#[macro_use]
10extern crate log;
11
12use codec::capabilities::{
13 CapabilityDescriptor, CapabilityProvider, Dispatcher, NullDispatcher, OperationDirection,
14 OP_GET_CAPABILITY_DESCRIPTOR,
15};
16use codec::core::{CapabilityConfiguration, OP_BIND_ACTOR, OP_REMOVE_ACTOR};
17use codec::http::{Request, OP_PERFORM_REQUEST};
18use codec::{deserialize, serialize, SYSTEM_ACTOR};
19use std::collections::HashMap;
20use std::error::Error;
21use std::sync::{Arc, RwLock};
22use std::time::Duration;
23
24const CAPABILITY_ID: &str = "wascc:http_client";
25const VERSION: &str = env!("CARGO_PKG_VERSION");
26const REVISION: u32 = 0;
27
28#[cfg(not(feature = "static_plugin"))]
29capability_provider!(HttpClientProvider, HttpClientProvider::new);
30
31pub struct HttpClientProvider {
33 dispatcher: Arc<RwLock<Box<dyn Dispatcher>>>,
34 clients: Arc<RwLock<HashMap<String, reqwest::Client>>>,
35 runtime: tokio::runtime::Runtime,
36}
37
38impl HttpClientProvider {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 fn configure(&self, config: CapabilityConfiguration) -> Result<Vec<u8>, Box<dyn Error>> {
48 let timeout = match config.values.get("timeout") {
49 Some(v) => {
50 let parsed: u64 = v.parse()?;
51 Duration::new(parsed, 0)
52 }
53 None => Duration::new(30, 0),
54 };
55
56 let redirect_policy = match config.values.get("max_redirects") {
57 Some(v) => {
58 let parsed: usize = v.parse()?;
59 reqwest::redirect::Policy::limited(parsed)
60 }
61 None => reqwest::redirect::Policy::default(),
62 };
63
64 self.clients.write().unwrap().insert(
65 config.module.clone(),
66 reqwest::Client::builder()
67 .timeout(timeout)
68 .redirect(redirect_policy)
69 .build()?,
70 );
71 Ok(vec![])
72 }
73
74 fn deconfigure(&self, config: CapabilityConfiguration) -> Result<Vec<u8>, Box<dyn Error>> {
77 if self
78 .clients
79 .write()
80 .unwrap()
81 .remove(&config.module)
82 .is_none()
83 {
84 warn!(
85 "attempted to remove non-existent actor: {}",
86 config.module.as_str()
87 );
88 }
89
90 Ok(vec![])
91 }
92
93 fn request(&self, actor: &str, msg: Request) -> Result<Vec<u8>, Box<dyn Error>> {
95 let lock = self.clients.read().unwrap();
96 let client = lock.get(actor).unwrap();
97 self.runtime
98 .handle()
99 .block_on(async { http_client::request(&client, msg).await })
100 }
101
102 fn get_descriptor(&self) -> Result<Vec<u8>, Box<dyn Error>> {
103 use OperationDirection::ToProvider;
104 Ok(serialize(
105 CapabilityDescriptor::builder()
106 .id(CAPABILITY_ID)
107 .name("wasCC HTTP Client Provider")
108 .long_description("A http client provider")
109 .version(VERSION)
110 .revision(REVISION)
111 .with_operation(OP_PERFORM_REQUEST, ToProvider, "Perform a http request")
112 .build(),
113 )?)
114 }
115}
116
117impl Default for HttpClientProvider {
118 fn default() -> Self {
119 let _ = env_logger::builder().format_module_path(false).try_init();
120
121 let r = tokio::runtime::Builder::new()
122 .threaded_scheduler()
123 .enable_all()
124 .build()
125 .unwrap();
126
127 HttpClientProvider {
128 dispatcher: Arc::new(RwLock::new(Box::new(NullDispatcher::new()))),
129 clients: Arc::new(RwLock::new(HashMap::new())),
130 runtime: r,
131 }
132 }
133}
134
135impl CapabilityProvider for HttpClientProvider {
137 fn configure_dispatch(&self, dispatcher: Box<dyn Dispatcher>) -> Result<(), Box<dyn Error>> {
138 info!("Dispatcher configured");
139
140 let mut lock = self.dispatcher.write().unwrap();
141 *lock = dispatcher;
142 Ok(())
143 }
144
145 fn handle_call(&self, actor: &str, op: &str, msg: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
147 match op {
148 OP_BIND_ACTOR if actor == SYSTEM_ACTOR => self.configure(deserialize(msg)?),
149 OP_REMOVE_ACTOR if actor == SYSTEM_ACTOR => self.deconfigure(deserialize(msg)?),
150 OP_PERFORM_REQUEST => self.request(actor, deserialize(msg)?),
151 OP_GET_CAPABILITY_DESCRIPTOR => self.get_descriptor(),
152 _ => Err(format!("Unknown operation: {}", op).into()),
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use codec::deserialize;
161 use codec::http::Response;
162 use mockito::mock;
163
164 #[test]
165 fn test_request() {
166 let _ = env_logger::try_init();
167 let request = Request {
168 method: "GET".to_string(),
169 path: mockito::server_url(),
170 header: HashMap::new(),
171 body: vec![],
172 query_string: String::new(),
173 };
174
175 let _m = mock("GET", "/")
176 .with_header("content-type", "text/plain")
177 .with_body("ohai")
178 .create();
179
180 let hp = HttpClientProvider::new();
181 hp.configure(CapabilityConfiguration {
182 module: "test".to_string(),
183 values: HashMap::new(),
184 })
185 .unwrap();
186
187 let result = hp.request("test", request).unwrap();
188 let response: Response = deserialize(result.as_slice()).unwrap();
189
190 assert_eq!(response.status_code, 200);
191 }
192}