pezsp_runtime/offchain/
http.rs

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