wascc_httpclient/
lib.rs

1///!
2///! # http-client-provider
3///! This library exposes the HTTP client capability to waSCC-compliant actors
4mod 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
31/// An implementation HTTP client provider using reqwest.
32pub 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    /// Create a new HTTP client provider.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Configure the HTTP client for a particular actor.
45    /// Each actor gets a dedicated client so that we can take advantage of connection pooling.
46    /// TODO: This needs to set things like timeouts, redirects, etc.
47    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    /// Clean up resources when a actor disconnects.
75    /// This removes the HTTP client associated with an actor.
76    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    /// Make a HTTP request.
94    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
135/// Implements the CapabilityProvider interface.
136impl 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    /// Handle all calls from actors.
146    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}