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::{
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 = sp_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			sp_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			sp_io::offchain::http_request_write_body(id, chunk.as_ref(), self.deadline)?;
224		}
225
226		// finalize the request
227		sp_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 = sp_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 = Some(Headers { raw: sp_io::offchain::http_response_headers(self.id) });
331		}
332		self.headers.as_ref().expect("Headers were just set; qed")
333	}
334
335	/// Retrieve the body of this response.
336	pub fn body(&self) -> ResponseBody {
337		ResponseBody::new(self.id)
338	}
339}
340
341/// A buffered byte iterator over response body.
342///
343/// Note that reading the body may return `None` in following cases:
344/// 1. Either the deadline you've set is reached (check via `#error`; In such case you can resume
345///    the reader by setting a new deadline)
346/// 2. Or because of IOError. In such case the reader is not resumable and will keep returning
347///    `None`.
348/// 3. The body has been returned. The reader will keep returning `None`.
349#[derive(Clone)]
350pub struct ResponseBody {
351	id: RequestId,
352	error: Option<HttpError>,
353	buffer: [u8; 4096],
354	filled_up_to: Option<usize>,
355	position: usize,
356	deadline: Option<Timestamp>,
357}
358
359#[cfg(feature = "std")]
360impl std::fmt::Debug for ResponseBody {
361	fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
362		fmt.debug_struct("ResponseBody")
363			.field("id", &self.id)
364			.field("error", &self.error)
365			.field("buffer", &self.buffer.len())
366			.field("filled_up_to", &self.filled_up_to)
367			.field("position", &self.position)
368			.field("deadline", &self.deadline)
369			.finish()
370	}
371}
372
373impl ResponseBody {
374	fn new(id: RequestId) -> Self {
375		ResponseBody {
376			id,
377			error: None,
378			buffer: [0_u8; 4096],
379			filled_up_to: None,
380			position: 0,
381			deadline: None,
382		}
383	}
384
385	/// Set the deadline for reading the body.
386	pub fn deadline(&mut self, deadline: impl Into<Option<Timestamp>>) {
387		self.deadline = deadline.into();
388		self.error = None;
389	}
390
391	/// Return an error that caused the iterator to return `None`.
392	///
393	/// If the error is `DeadlineReached` you can resume the iterator by setting
394	/// a new deadline.
395	pub fn error(&self) -> &Option<HttpError> {
396		&self.error
397	}
398}
399
400impl Iterator for ResponseBody {
401	type Item = u8;
402
403	fn next(&mut self) -> Option<Self::Item> {
404		if self.error.is_some() {
405			return None
406		}
407
408		if self.filled_up_to.is_none() {
409			let result =
410				sp_io::offchain::http_response_read_body(self.id, &mut self.buffer, self.deadline);
411			match result {
412				Err(e) => {
413					self.error = Some(e);
414					return None
415				},
416				Ok(0) => return None,
417				Ok(size) => {
418					self.position = 0;
419					self.filled_up_to = Some(size as usize);
420				},
421			}
422		}
423
424		if Some(self.position) == self.filled_up_to {
425			self.filled_up_to = None;
426			return self.next()
427		}
428
429		let result = self.buffer[self.position];
430		self.position += 1;
431		Some(result)
432	}
433}
434
435/// A collection of Headers in the response.
436#[derive(Clone, PartialEq, Eq, RuntimeDebug)]
437pub struct Headers {
438	/// Raw headers
439	pub raw: Vec<(Vec<u8>, Vec<u8>)>,
440}
441
442impl Headers {
443	/// Retrieve a single header from the list of headers.
444	///
445	/// Note this method is linearly looking from all the headers
446	/// comparing them with the needle byte-by-byte.
447	/// If you want to consume multiple headers it's better to iterate
448	/// and collect them on your own.
449	pub fn find(&self, name: &str) -> Option<&str> {
450		let raw = name.as_bytes();
451		for (key, val) in &self.raw {
452			if &**key == raw {
453				return str::from_utf8(val).ok()
454			}
455		}
456		None
457	}
458
459	/// Convert this headers into an iterator.
460	pub fn into_iter(&self) -> HeadersIterator {
461		HeadersIterator { collection: &self.raw, index: None }
462	}
463}
464
465/// A custom iterator traversing all the headers.
466#[derive(Clone, RuntimeDebug)]
467pub struct HeadersIterator<'a> {
468	collection: &'a [(Vec<u8>, Vec<u8>)],
469	index: Option<usize>,
470}
471
472impl<'a> HeadersIterator<'a> {
473	/// Move the iterator to the next position.
474	///
475	/// Returns `true` is `current` has been set by this call.
476	pub fn next(&mut self) -> bool {
477		let index = self.index.map(|x| x + 1).unwrap_or(0);
478		self.index = Some(index);
479		index < self.collection.len()
480	}
481
482	/// Returns current element (if any).
483	///
484	/// Note that you have to call `next` prior to calling this
485	pub fn current(&self) -> Option<(&str, &str)> {
486		self.collection
487			.get(self.index?)
488			.map(|val| (str::from_utf8(&val.0).unwrap_or(""), str::from_utf8(&val.1).unwrap_or("")))
489	}
490}
491
492#[cfg(test)]
493mod tests {
494	use super::*;
495	use sp_core::offchain::{testing, OffchainWorkerExt};
496	use sp_io::TestExternalities;
497
498	#[test]
499	fn should_send_a_basic_request_and_get_response() {
500		let (offchain, state) = testing::TestOffchainExt::new();
501		let mut t = TestExternalities::default();
502		t.register_extension(OffchainWorkerExt::new(offchain));
503
504		t.execute_with(|| {
505			let request: Request = Request::get("http://localhost:1234");
506			let pending = request.add_header("X-Auth", "hunter2").send().unwrap();
507			// make sure it's sent correctly
508			state.write().fulfill_pending_request(
509				0,
510				testing::PendingRequest {
511					method: "GET".into(),
512					uri: "http://localhost:1234".into(),
513					headers: vec![("X-Auth".into(), "hunter2".into())],
514					sent: true,
515					..Default::default()
516				},
517				b"1234".to_vec(),
518				None,
519			);
520
521			// wait
522			let mut response = pending.wait().unwrap();
523
524			// then check the response
525			let mut headers = response.headers().into_iter();
526			assert_eq!(headers.current(), None);
527			assert_eq!(headers.next(), false);
528			assert_eq!(headers.current(), None);
529
530			let body = response.body();
531			assert_eq!(body.clone().collect::<Vec<_>>(), b"1234".to_vec());
532			assert_eq!(body.error(), &None);
533		})
534	}
535
536	#[test]
537	fn should_send_huge_response() {
538		let (offchain, state) = testing::TestOffchainExt::new();
539		let mut t = TestExternalities::default();
540		t.register_extension(OffchainWorkerExt::new(offchain));
541
542		t.execute_with(|| {
543			let request: Request = Request::get("http://localhost:1234");
544			let pending = request.add_header("X-Auth", "hunter2").send().unwrap();
545			// make sure it's sent correctly
546			state.write().fulfill_pending_request(
547				0,
548				testing::PendingRequest {
549					method: "GET".into(),
550					uri: "http://localhost:1234".into(),
551					headers: vec![("X-Auth".into(), "hunter2".into())],
552					sent: true,
553					..Default::default()
554				},
555				vec![0; 5923],
556				None,
557			);
558
559			// wait
560			let response = pending.wait().unwrap();
561
562			let body = response.body();
563			assert_eq!(body.clone().collect::<Vec<_>>(), vec![0; 5923]);
564			assert_eq!(body.error(), &None);
565		})
566	}
567
568	#[test]
569	fn should_send_a_post_request() {
570		let (offchain, state) = testing::TestOffchainExt::new();
571		let mut t = TestExternalities::default();
572		t.register_extension(OffchainWorkerExt::new(offchain));
573
574		t.execute_with(|| {
575			let pending = Request::default()
576				.method(Method::Post)
577				.url("http://localhost:1234")
578				.body(vec![b"1234"])
579				.send()
580				.unwrap();
581			// make sure it's sent correctly
582			state.write().fulfill_pending_request(
583				0,
584				testing::PendingRequest {
585					method: "POST".into(),
586					uri: "http://localhost:1234".into(),
587					body: b"1234".to_vec(),
588					sent: true,
589					..Default::default()
590				},
591				b"1234".to_vec(),
592				Some(("Test".to_owned(), "Header".to_owned())),
593			);
594
595			// wait
596			let mut response = pending.wait().unwrap();
597
598			// then check the response
599			let mut headers = response.headers().into_iter();
600			assert_eq!(headers.current(), None);
601			assert_eq!(headers.next(), true);
602			assert_eq!(headers.current(), Some(("Test", "Header")));
603
604			let body = response.body();
605			assert_eq!(body.clone().collect::<Vec<_>>(), b"1234".to_vec());
606			assert_eq!(body.error(), &None);
607		})
608	}
609}