referrerpolicy=no-referrer-when-downgrade

sp_runtime/offchain/
http.rs

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