tp_runtime/offchain/
http.rs

1// This file is part of Tetcore.
2
3// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! A high-level helpers for making HTTP requests from Offchain Workers.
19//!
20//! `tet-io` crate exposes a low level methods to make and control HTTP requests
21//! available only for Offchain Workers. Those might be hard to use
22//! and usually that level of control is not really necessary.
23//! This module aims to provide high-level wrappers for those APIs
24//! to simplify making HTTP requests.
25//!
26//!
27//! Example:
28//! ```rust,no_run
29//! use tp_runtime::offchain::http::Request;
30//!
31//! // initiate a GET request to localhost:1234
32//! let request: Request = Request::get("http://localhost:1234");
33//! let pending = request
34//! 	.add_header("X-Auth", "hunter2")
35//! 	.send()
36//! 	.unwrap();
37//!
38//! // wait for the response indefinitely
39//! let mut response = pending.wait().unwrap();
40//!
41//! // then check the headers
42//! let mut headers = response.headers().into_iter();
43//! assert_eq!(headers.current(), None);
44//!
45//! // and collect the body
46//! let body = response.body();
47//! assert_eq!(body.clone().collect::<Vec<_>>(), b"1234".to_vec());
48//! assert_eq!(body.error(), &None);
49//! ```
50
51use tetcore_std::str;
52use tetcore_std::prelude::Vec;
53#[cfg(not(feature = "std"))]
54use tetcore_std::prelude::vec;
55use tet_core::RuntimeDebug;
56use tet_core::offchain::{
57	Timestamp,
58	HttpRequestId as RequestId,
59	HttpRequestStatus as RequestStatus,
60	HttpError,
61};
62
63/// Request method (HTTP verb)
64#[derive(Clone, PartialEq, Eq, RuntimeDebug)]
65pub enum Method {
66	/// GET request
67	Get,
68	/// POST request
69	Post,
70	/// PUT request
71	Put,
72	/// PATCH request
73	Patch,
74	/// DELETE request
75	Delete,
76	/// Custom verb
77	Other(&'static str),
78}
79
80impl AsRef<str> for Method {
81	fn as_ref(&self) -> &str {
82		match *self {
83			Method::Get => "GET",
84			Method::Post => "POST",
85			Method::Put => "PUT",
86			Method::Patch => "PATCH",
87			Method::Delete => "DELETE",
88			Method::Other(m) => m,
89		}
90	}
91}
92
93mod header {
94	use super::*;
95
96	/// A header type.
97	#[derive(Clone, PartialEq, Eq, RuntimeDebug)]
98	pub struct Header {
99		name: Vec<u8>,
100		value: Vec<u8>,
101	}
102
103	impl Header {
104		/// Creates new header given it's name and value.
105		pub fn new(name: &str, value: &str) -> Self {
106			Header {
107				name: name.as_bytes().to_vec(),
108				value: value.as_bytes().to_vec(),
109			}
110		}
111
112		/// Returns the name of this header.
113		pub fn name(&self) -> &str {
114			// Header keys are always produced from `&str` so this is safe.
115			// we don't store them as `Strings` to avoid bringing `alloc::String` to tetcore-std
116			// or here.
117			unsafe { str::from_utf8_unchecked(&self.name) }
118		}
119
120		/// Returns the value of this header.
121		pub fn value(&self) -> &str {
122			// Header values are always produced from `&str` so this is safe.
123			// we don't store them as `Strings` to avoid bringing `alloc::String` to tetcore-std
124			// or here.
125			unsafe { str::from_utf8_unchecked(&self.value) }
126		}
127	}
128}
129
130/// An HTTP request builder.
131#[derive(Clone, PartialEq, Eq, RuntimeDebug)]
132pub struct Request<'a, T = Vec<&'static [u8]>> {
133	/// Request method
134	pub method: Method,
135	/// Request URL
136	pub url: &'a str,
137	/// Body of the request
138	pub body: T,
139	/// Deadline to finish sending the request
140	pub deadline: Option<Timestamp>,
141	/// Request list of headers.
142	headers: Vec<header::Header>,
143}
144
145impl<T: Default> Default for Request<'static, T> {
146	fn default() -> Self {
147		Request {
148			method: Method::Get,
149			url: "http://localhost",
150			headers: Vec::new(),
151			body: Default::default(),
152			deadline: None,
153		}
154	}
155}
156
157impl<'a> Request<'a> {
158	/// Start a simple GET request
159	pub fn get(url: &'a str) -> Self {
160		Self::new(url)
161	}
162}
163
164impl<'a, T> Request<'a, T> {
165	/// Create new POST request with given body.
166	pub fn post(url: &'a str, body: T) -> Self {
167		let req: Request = Request::default();
168
169		Request {
170			url,
171			body,
172			method: Method::Post,
173			headers: req.headers,
174			deadline: req.deadline,
175		}
176	}
177}
178
179impl<'a, T: Default> Request<'a, T> {
180	/// Create a new Request builder with the given URL.
181	pub fn new(url: &'a str) -> Self {
182		Request::default().url(url)
183	}
184
185	/// Change the method of the request
186	pub fn method(mut self, method: Method) -> Self {
187		self.method = method;
188		self
189	}
190
191	/// Change the URL of the request.
192	pub fn url(mut self, url: &'a str) -> Self {
193		self.url = url;
194		self
195	}
196
197	/// Set the body of the request.
198	pub fn body(mut self, body: T) -> Self {
199		self.body = body;
200		self
201	}
202
203	/// Add a header.
204	pub fn add_header(mut self, name: &str, value: &str) -> Self {
205		self.headers.push(header::Header::new(name, value));
206		self
207	}
208
209	/// Set the deadline of the request.
210	pub fn deadline(mut self, deadline: Timestamp) -> Self {
211		self.deadline = Some(deadline);
212		self
213	}
214}
215
216impl<'a, I: AsRef<[u8]>, T: IntoIterator<Item=I>> Request<'a, T> {
217	/// Send the request and return a handle.
218	///
219	/// Err is returned in case the deadline is reached
220	/// or the request timeouts.
221	pub fn send(self) -> Result<PendingRequest, HttpError> {
222		let meta = &[];
223
224		// start an http request.
225		let id = tet_io::offchain::http_request_start(
226			self.method.as_ref(),
227			self.url,
228			meta,
229		).map_err(|_| HttpError::IoError)?;
230
231		// add custom headers
232		for header in &self.headers {
233			tet_io::offchain::http_request_add_header(
234				id,
235				header.name(),
236				header.value(),
237			).map_err(|_| HttpError::IoError)?
238		}
239
240		// write body
241		for chunk in self.body {
242			tet_io::offchain::http_request_write_body(id, chunk.as_ref(), self.deadline)?;
243		}
244
245		// finalize the request
246		tet_io::offchain::http_request_write_body(id, &[], self.deadline)?;
247
248		Ok(PendingRequest {
249			id,
250		})
251	}
252}
253
254/// A request error
255#[derive(Clone, PartialEq, Eq, RuntimeDebug)]
256pub enum Error {
257	/// Deadline has been reached.
258	DeadlineReached,
259	/// Request had timed out.
260	IoError,
261	/// Unknown error has been encountered.
262	Unknown,
263}
264
265/// A struct representing an uncompleted http request.
266#[derive(PartialEq, Eq, RuntimeDebug)]
267pub struct PendingRequest {
268	/// Request ID
269	pub id: RequestId,
270}
271
272/// A result of waiting for a pending request.
273pub type HttpResult = Result<Response, Error>;
274
275impl PendingRequest {
276	/// Wait for the request to complete.
277	///
278	/// NOTE this waits for the request indefinitely.
279	pub fn wait(self) -> HttpResult {
280		match self.try_wait(None) {
281			Ok(res) => res,
282			Err(_) => panic!("Since `None` is passed we will never get a deadline error; qed"),
283		}
284	}
285
286	/// Attempts to wait for the request to finish,
287	/// but will return `Err` in case the deadline is reached.
288	pub fn try_wait(self, deadline: impl Into<Option<Timestamp>>) -> Result<HttpResult, PendingRequest> {
289		Self::try_wait_all(vec![self], deadline).pop().expect("One request passed, one status received; qed")
290	}
291
292	/// Wait for all provided requests.
293	pub fn wait_all(requests: Vec<PendingRequest>) -> Vec<HttpResult> {
294		Self::try_wait_all(requests, None)
295			.into_iter()
296			.map(|r| match r {
297				Ok(r) => r,
298				Err(_) => panic!("Since `None` is passed we will never get a deadline error; qed"),
299			})
300			.collect()
301	}
302
303	/// Attempt to wait for all provided requests, but up to given deadline.
304	///
305	/// Requests that are complete will resolve to an `Ok` others will return a `DeadlineReached` error.
306	pub fn try_wait_all(
307		requests: Vec<PendingRequest>,
308		deadline: impl Into<Option<Timestamp>>
309	) -> Vec<Result<HttpResult, PendingRequest>> {
310		let ids = requests.iter().map(|r| r.id).collect::<Vec<_>>();
311		let statuses = tet_io::offchain::http_response_wait(&ids, deadline.into());
312
313		statuses
314			.into_iter()
315			.zip(requests.into_iter())
316			.map(|(status, req)| match status {
317				RequestStatus::DeadlineReached => Err(req),
318				RequestStatus::IoError => Ok(Err(Error::IoError)),
319				RequestStatus::Invalid => Ok(Err(Error::Unknown)),
320				RequestStatus::Finished(code) => Ok(Ok(Response::new(req.id, code))),
321			})
322			.collect()
323	}
324}
325
326/// A HTTP response.
327#[derive(RuntimeDebug)]
328pub struct Response {
329	/// Request id
330	pub id: RequestId,
331	/// Response status code
332	pub code: u16,
333	/// A collection of headers.
334	headers: Option<Headers>,
335}
336
337impl Response {
338	fn new(id: RequestId, code: u16) -> Self {
339		Self {
340			id,
341			code,
342			headers: None,
343		}
344	}
345
346	/// Retrieve the headers for this response.
347	pub fn headers(&mut self) -> &Headers {
348		if self.headers.is_none() {
349			self.headers = Some(
350				Headers { raw: tet_io::offchain::http_response_headers(self.id) },
351			);
352		}
353		self.headers.as_ref().expect("Headers were just set; qed")
354	}
355
356	/// Retrieve the body of this response.
357	pub fn body(&self) -> ResponseBody {
358		ResponseBody::new(self.id)
359	}
360}
361
362/// A buffered byte iterator over response body.
363///
364/// Note that reading the body may return `None` in following cases:
365/// 1. Either the deadline you've set is reached (check via `#error`;
366///	   In such case you can resume the reader by setting a new deadline)
367/// 2. Or because of IOError. In such case the reader is not resumable and will keep
368///    returning `None`.
369/// 3. The body has been returned. The reader will keep returning `None`.
370#[derive(Clone)]
371pub struct ResponseBody {
372	id: RequestId,
373	error: Option<HttpError>,
374	buffer: [u8; 4096],
375	filled_up_to: Option<usize>,
376	position: usize,
377	deadline: Option<Timestamp>,
378}
379
380#[cfg(feature = "std")]
381impl std::fmt::Debug for ResponseBody {
382	fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
383		fmt.debug_struct("ResponseBody")
384			.field("id", &self.id)
385			.field("error", &self.error)
386			.field("buffer", &self.buffer.len())
387			.field("filled_up_to", &self.filled_up_to)
388			.field("position", &self.position)
389			.field("deadline", &self.deadline)
390			.finish()
391	}
392}
393
394impl ResponseBody {
395	fn new(id: RequestId) -> Self {
396		ResponseBody {
397			id,
398			error: None,
399			buffer: [0_u8; 4096],
400			filled_up_to: None,
401			position: 0,
402			deadline: None,
403		}
404	}
405
406	/// Set the deadline for reading the body.
407	pub fn deadline(&mut self, deadline: impl Into<Option<Timestamp>>) {
408		self.deadline = deadline.into();
409		self.error = None;
410	}
411
412	/// Return an error that caused the iterator to return `None`.
413	///
414	/// If the error is `DeadlineReached` you can resume the iterator by setting
415	/// a new deadline.
416	pub fn error(&self) -> &Option<HttpError> {
417		&self.error
418	}
419}
420
421impl Iterator for ResponseBody {
422	type Item = u8;
423
424	fn next(&mut self) -> Option<Self::Item> {
425		if self.error.is_some() {
426			return None;
427		}
428
429		if self.filled_up_to.is_none() {
430			let result = tet_io::offchain::http_response_read_body(
431				self.id,
432				&mut self.buffer,
433				self.deadline);
434			match result {
435				Err(e) => {
436					self.error = Some(e);
437					return None;
438				}
439				Ok(0) => {
440					return None;
441				}
442				Ok(size) => {
443					self.position = 0;
444					self.filled_up_to = Some(size as usize);
445				}
446			}
447		}
448
449		if Some(self.position) == self.filled_up_to {
450			self.filled_up_to = None;
451			return self.next();
452		}
453
454		let result = self.buffer[self.position];
455		self.position += 1;
456		Some(result)
457	}
458}
459
460/// A collection of Headers in the response.
461#[derive(Clone, PartialEq, Eq, RuntimeDebug)]
462pub struct Headers {
463	/// Raw headers
464	pub raw: Vec<(Vec<u8>, Vec<u8>)>,
465}
466
467impl Headers {
468	/// Retrieve a single header from the list of headers.
469	///
470	/// Note this method is linearly looking from all the headers
471	/// comparing them with the needle byte-by-byte.
472	/// If you want to consume multiple headers it's better to iterate
473	/// and collect them on your own.
474	pub fn find(&self, name: &str) -> Option<&str> {
475		let raw = name.as_bytes();
476		for &(ref key, ref val) in &self.raw {
477			if &**key == raw {
478				return str::from_utf8(&val).ok()
479			}
480		}
481		None
482	}
483
484	/// Convert this headers into an iterator.
485	pub fn into_iter(&self) -> HeadersIterator {
486		HeadersIterator { collection: &self.raw, index: None }
487	}
488}
489
490/// A custom iterator traversing all the headers.
491#[derive(Clone, RuntimeDebug)]
492pub struct HeadersIterator<'a> {
493	collection: &'a [(Vec<u8>, Vec<u8>)],
494	index: Option<usize>,
495}
496
497impl<'a> HeadersIterator<'a> {
498	/// Move the iterator to the next position.
499	///
500	/// Returns `true` is `current` has been set by this call.
501	pub fn next(&mut self) -> bool {
502		let index = self.index.map(|x| x + 1).unwrap_or(0);
503		self.index = Some(index);
504		index < self.collection.len()
505	}
506
507	/// Returns current element (if any).
508	///
509	/// Note that you have to call `next` prior to calling this
510	pub fn current(&self) -> Option<(&str, &str)> {
511		self.collection.get(self.index?)
512			.map(|val| (str::from_utf8(&val.0).unwrap_or(""), str::from_utf8(&val.1).unwrap_or("")))
513	}
514}
515
516#[cfg(test)]
517mod tests {
518	use super::*;
519	use tet_io::TestExternalities;
520	use tet_core::offchain::{
521		OffchainExt,
522		testing,
523	};
524
525	#[test]
526	fn should_send_a_basic_request_and_get_response() {
527		let (offchain, state) = testing::TestOffchainExt::new();
528		let mut t = TestExternalities::default();
529		t.register_extension(OffchainExt::new(offchain));
530
531		t.execute_with(|| {
532			let request: Request = Request::get("http://localhost:1234");
533			let pending = request
534				.add_header("X-Auth", "hunter2")
535				.send()
536				.unwrap();
537			// make sure it's sent correctly
538			state.write().fulfill_pending_request(
539				0,
540				testing::PendingRequest {
541					method: "GET".into(),
542					uri: "http://localhost:1234".into(),
543					headers: vec![("X-Auth".into(), "hunter2".into())],
544					sent: true,
545					..Default::default()
546				},
547				b"1234".to_vec(),
548				None,
549			);
550
551			// wait
552			let mut response = pending.wait().unwrap();
553
554			// then check the response
555			let mut headers = response.headers().into_iter();
556			assert_eq!(headers.current(), None);
557			assert_eq!(headers.next(), false);
558			assert_eq!(headers.current(), None);
559
560			let body = response.body();
561			assert_eq!(body.clone().collect::<Vec<_>>(), b"1234".to_vec());
562			assert_eq!(body.error(), &None);
563		})
564	}
565
566	#[test]
567	fn should_send_a_post_request() {
568		let (offchain, state) = testing::TestOffchainExt::new();
569		let mut t = TestExternalities::default();
570		t.register_extension(OffchainExt::new(offchain));
571
572		t.execute_with(|| {
573			let pending = Request::default()
574				.method(Method::Post)
575				.url("http://localhost:1234")
576				.body(vec![b"1234"])
577				.send()
578				.unwrap();
579			// make sure it's sent correctly
580			state.write().fulfill_pending_request(
581				0,
582				testing::PendingRequest {
583					method: "POST".into(),
584					uri: "http://localhost:1234".into(),
585					body: b"1234".to_vec(),
586					sent: true,
587					..Default::default()
588				},
589				b"1234".to_vec(),
590				Some(("Test".to_owned(), "Header".to_owned())),
591			);
592
593			// wait
594			let mut response = pending.wait().unwrap();
595
596			// then check the response
597			let mut headers = response.headers().into_iter();
598			assert_eq!(headers.current(), None);
599			assert_eq!(headers.next(), true);
600			assert_eq!(headers.current(), Some(("Test", "Header")));
601
602			let body = response.body();
603			assert_eq!(body.clone().collect::<Vec<_>>(), b"1234".to_vec());
604			assert_eq!(body.error(), &None);
605		})
606	}
607}