drogue_http_client/
lib.rs

1#![no_std]
2
3//! `drogue-http-client` aims to provide an HTTP client, in constrained `no_std` environment.
4//! Making use of the `drogue-network` API, and its network stack implementations.
5//!
6//! An example could be to use an ESP-01 connected via UART, interfacing with the TCP stack via
7//! `AT` commands, wrapping that stack with a TLS layer from `drogue-tls`, and executing HTTPS
8//! requests on top of that stack.
9//!
10//! # Example
11//!
12//! ~~~no_run
13//! use core::str::from_utf8;
14//!
15//! use heapless::consts;
16//!
17//! use drogue_network::tcp::TcpStack;
18//!
19//! use drogue_http_client::tcp;
20//! use drogue_http_client::*;
21//!
22//! const ENDPOINT_HOST: &'static str = "my-server";
23//! const ENDPOINT_PORT: u16 = 8080;
24//!
25//! # use drogue_http_client::mock;
26//! # fn connect_to_server(host: &str, port: u16) -> (mock::MockStack, mock::MockSocket) {
27//! #     mock::mock_connection()
28//! # }
29//!
30//! fn publish() -> Result<(),()> {
31//!     let (mut network, mut socket) = connect_to_server(ENDPOINT_HOST, ENDPOINT_PORT);
32//!     let mut tcp = tcp::TcpSocketSinkSource::from(&mut network, &mut socket);
33//!
34//!     let con = HttpConnection::<consts::U1024>::new();
35//!
36//!     let handler = BufferResponseHandler::<consts::U512>::new();
37//!
38//!     let mut req = con.post("/my/path")
39//!         .headers(&[
40//!             ("Content-Type", "text/plain"),
41//!             ("Host", ENDPOINT_HOST),
42//!         ])
43//!         .handler(handler)
44//!         .execute_with::<_, consts::U256>(&mut tcp, Some(b"payload"));
45//!
46//!     tcp.pipe_data(&mut req)?;
47//!
48//!     let (con, handler) = req.complete();
49//!
50//!     println!("Response: {} {}", handler.code(), handler.reason());
51//!     println!("{:?}", from_utf8(handler.payload()));
52//!
53//!     Ok(())
54//! }
55//!
56//! ~~~
57
58mod con;
59mod handler;
60#[doc(hidden)]
61pub mod mock;
62mod sink;
63mod source;
64pub mod tcp;
65
66pub use con::*;
67pub use handler::*;
68pub use sink::*;
69pub use source::*;
70
71#[cfg(test)]
72mod test {
73    use super::*;
74    use core::str::from_utf8;
75    use heapless::consts::*;
76    use heapless::{ArrayLength, String, Vec};
77
78    fn init() {
79        let _ = env_logger::builder().is_test(true).try_init();
80    }
81
82    #[test]
83    fn idea() -> Result<(), ()> {
84        init();
85
86        let mut sink_buffer = Vec::<u8, U1024>::new();
87        let con = HttpConnection::<U1024>::new();
88
89        let headers = [("Content-Type", "text/json")];
90
91        let handler = BufferResponseHandler::<U1024>::new();
92
93        let mut req = {
94            con.post("/foo.bar")
95                .headers(&headers)
96                .handler(handler)
97                .execute::<_, U128>(&mut sink_buffer)
98        };
99
100        // mock response
101
102        req.push_data(b"HTTP/1.1 ");
103        req.push_data(b"200 OK\r\n");
104        req.push_data(b"\r\n");
105        req.push_data(b"123");
106        req.push_close();
107
108        let (_, handler) = req.complete();
109
110        // sink
111
112        assert_eq!(
113            String::from_utf8(sink_buffer).unwrap().as_str(),
114            "POST /foo.bar HTTP/1.1\r\nContent-Type: text/json\r\n\r\n",
115        );
116
117        // result
118
119        assert_eq!(200, handler.code());
120        assert_eq!("OK", handler.reason());
121        assert_eq!(core::str::from_utf8(handler.payload()), Ok("123"));
122
123        assert!(handler.is_complete());
124
125        // done
126
127        Ok(())
128    }
129
130    #[test]
131    fn simple() {
132        assert_http(
133            "POST",
134            "/",
135            &[],
136            None,
137            b"POST / HTTP/1.1\r\n\r\n",
138            &[b"HTTP/1.1 200 OK\r\n\r\n0123456789"],
139            200,
140            "OK",
141            b"0123456789",
142        );
143    }
144
145    #[test]
146    fn simple_split_1() {
147        assert_http(
148            "POST",
149            "/",
150            &[],
151            None,
152            b"POST / HTTP/1.1\r\n\r\n",
153            &[b"HTTP/1.1 200 OK\r\n\r\n01234", b"56789"],
154            200,
155            "OK",
156            b"0123456789",
157        );
158    }
159
160    #[test]
161    fn simple_split_2() {
162        assert_http(
163            "POST",
164            "/",
165            &[],
166            None,
167            b"POST / HTTP/1.1\r\n\r\n",
168            &[b"HTTP/1.1 200 ", b"OK\r\n\r\n01234", b"56789"],
169            200,
170            "OK",
171            b"0123456789",
172        );
173    }
174
175    #[test]
176    fn simple_header() {
177        assert_http(
178            "POST",
179            "/",
180            &[("Content-Type", "text/json")],
181            None,
182            b"POST / HTTP/1.1\r\nContent-Type: text/json\r\n\r\n",
183            &[b"HTTP/1.1 200 OK\r\n\r\n0123456789"],
184            200,
185            "OK",
186            b"0123456789",
187        );
188    }
189
190    #[test]
191    fn simple_send_payload() {
192        assert_http(
193            "POST",
194            "/",
195            &[("Content-Type", "text/json")],
196            Some(b"0123456789"),
197            b"POST / HTTP/1.1\r\nContent-Length: 10\r\nContent-Type: text/json\r\n\r\n0123456789",
198            &[b"HTTP/1.1 200 OK\r\n\r\n0123456789"],
199            200,
200            "OK",
201            b"0123456789",
202        );
203    }
204
205    #[test]
206    fn multiple() {
207        let expected = &[
208            &b"POST / HTTP/1.1\r\nContent-Type: text/plain\r\n\r\n"[..],
209            &b"POST / HTTP/1.1\r\nContent-Type: text/plain\r\n\r\n"[..],
210        ];
211        let mut mock_sink = MockSinkImpl::<U1024>::new(expected);
212
213        let con = HttpConnection::<U1024>::new();
214
215        let con = assert_request(
216            con,
217            &mut mock_sink,
218            "POST",
219            "/",
220            &[("Content-Type", "text/plain")],
221            None,
222            &[b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n0123456789"],
223            false,
224            200,
225            "OK",
226            b"0123456789",
227        );
228
229        assert_request(
230            con,
231            &mut mock_sink,
232            "POST",
233            "/",
234            &[("Content-Type", "text/plain")],
235            None,
236            &[b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n0123456789"],
237            true,
238            200,
239            "OK",
240            b"0123456789",
241        );
242    }
243
244    fn assert_request<IN, S>(
245        con: HttpConnection<IN>,
246        sink: &mut S,
247        method: &'static str,
248        path: &'static str,
249        headers: &[(&str, &str)],
250        payload: Option<&[u8]>,
251        push: &[&[u8]],
252        close_after_push: bool,
253        code: u16,
254        reason: &str,
255        expected_payload: &[u8],
256    ) -> HttpConnection<IN>
257    where
258        IN: ArrayLength<u8>,
259        S: Sink + MockSink,
260    {
261        // capture response output
262
263        let handler = BufferResponseHandler::<U1024>::new();
264
265        // begin request
266
267        let mut req = {
268            con.begin(method, path)
269                .headers(&headers)
270                .handler(handler)
271                .execute_with::<_, U1024>(sink, payload)
272        };
273
274        // mock response
275
276        for p in push {
277            req.push_data(p);
278        }
279
280        if close_after_push {
281            req.push_close();
282        }
283
284        // close request
285
286        let (con, handler) = req.complete();
287
288        // assert sink
289
290        sink.assert();
291
292        // assert response
293
294        assert_eq!(code, handler.code());
295        assert_eq!(reason, handler.reason());
296
297        assert_eq!(
298            core::str::from_utf8(handler.payload()),
299            core::str::from_utf8(expected_payload)
300        );
301
302        assert!(handler.is_complete());
303
304        con
305    }
306
307    fn assert_http<'m>(
308        method: &'static str,
309        path: &'static str,
310        headers: &[(&str, &str)],
311        payload: Option<&[u8]>,
312        expected_sink: &'m [u8],
313        push: &[&[u8]],
314        code: u16,
315        reason: &str,
316        expected_payload: &[u8],
317    ) {
318        // capture sink output
319
320        let expected = &[expected_sink];
321        let mut mock_sink = MockSinkImpl::<U1024>::new(expected);
322
323        let con = HttpConnection::<U1024>::new();
324
325        assert_request(
326            con,
327            &mut mock_sink,
328            method,
329            path,
330            headers,
331            payload,
332            push,
333            true,
334            code,
335            reason,
336            expected_payload,
337        );
338    }
339
340    pub(crate) struct MockSinkImpl<'m, N>
341    where
342        N: ArrayLength<u8>,
343    {
344        buffer: Vec<u8, N>,
345        iter: core::slice::Iter<'m, &'m [u8]>,
346    }
347
348    impl<'m, N> MockSinkImpl<'m, N>
349    where
350        N: ArrayLength<u8>,
351    {
352        pub fn new(expected: &'m [&'m [u8]]) -> Self {
353            let i = expected.iter();
354            MockSinkImpl {
355                buffer: Vec::<u8, N>::new(),
356                iter: i,
357            }
358        }
359    }
360
361    impl<'m, N> Sink for MockSinkImpl<'m, N>
362    where
363        N: ArrayLength<u8>,
364    {
365        fn send(&mut self, data: &[u8]) -> Result<usize, ()> {
366            (&mut self.buffer).send(data)
367        }
368    }
369
370    pub trait MockSink {
371        fn assert(&mut self);
372    }
373
374    impl<'m, N> MockSink for MockSinkImpl<'m, N>
375    where
376        N: ArrayLength<u8>,
377    {
378        fn assert(&mut self) {
379            let expected = self.iter.next();
380
381            // assert
382
383            assert_eq!(
384                expected.and_then(|b| from_utf8(b).ok()),
385                from_utf8(self.buffer.as_ref()).ok(),
386            );
387
388            // now clear the buffer
389            self.buffer.clear();
390        }
391    }
392}