transport.rs 8.32 KB
Newer Older
Niklas Adolfsson's avatar
Niklas Adolfsson committed
1
2
3
4
5
6
7
8
// Implementation note: hyper's API is not adapted to async/await at all, and there's
// unfortunately a lot of boilerplate here that could be removed once/if it gets reworked.
//
// Additionally, despite the fact that hyper is capable of performing requests to multiple different
// servers through the same `hyper::Client`, we don't use that feature on purpose. The reason is
// that we need to be guaranteed that hyper doesn't re-use an existing connection if we ever reset
// the JSON-RPC request id to a value that might have already been used.

9
use hyper::client::{Client, HttpConnector};
10
use hyper::Uri;
Maciej Hirsz's avatar
Maciej Hirsz committed
11
12
13
use jsonrpsee_core::client::CertificateStore;
use jsonrpsee_core::error::GenericTransportError;
use jsonrpsee_core::http_helpers;
14
use jsonrpsee_core::tracing::{rx_log_from_bytes, tx_log_from_str};
Niklas Adolfsson's avatar
Niklas Adolfsson committed
15
16
17
18
use thiserror::Error;

const CONTENT_TYPE_JSON: &str = "application/json";

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#[derive(Debug, Clone)]
enum HyperClient {
	/// Hyper client with https connector.
	#[cfg(feature = "tls")]
	Https(Client<hyper_rustls::HttpsConnector<HttpConnector>>),
	/// Hyper client with http connector.
	Http(Client<HttpConnector>),
}

impl HyperClient {
	fn request(&self, req: hyper::Request<hyper::Body>) -> hyper::client::ResponseFuture {
		match self {
			Self::Http(client) => client.request(req),
			#[cfg(feature = "tls")]
			Self::Https(client) => client.request(req),
		}
	}
}

Niklas Adolfsson's avatar
Niklas Adolfsson committed
38
39
/// HTTP Transport Client.
#[derive(Debug, Clone)]
40
pub struct HttpTransportClient {
Niklas Adolfsson's avatar
Niklas Adolfsson committed
41
	/// Target to connect to.
42
	target: Uri,
43
	/// HTTP client
44
	client: HyperClient,
Niklas Adolfsson's avatar
Niklas Adolfsson committed
45
	/// Configurable max request body size
Niklas Adolfsson's avatar
Niklas Adolfsson committed
46
	max_request_body_size: u32,
47
48
49
50
	/// Max length for logging for requests and responses
	///
	/// Logs bigger than this limit will be truncated.
	max_log_length: u32,
Niklas Adolfsson's avatar
Niklas Adolfsson committed
51
52
53
54
}

impl HttpTransportClient {
	/// Initializes a new HTTP client.
55
56
57
58
	pub(crate) fn new(
		target: impl AsRef<str>,
		max_request_body_size: u32,
		cert_store: CertificateStore,
59
		max_log_length: u32,
60
	) -> Result<Self, Error> {
61
62
63
		let target: Uri = target.as_ref().parse().map_err(|e| Error::Url(format!("Invalid URL: {}", e)))?;
		if target.port_u16().is_none() {
			return Err(Error::Url("Port number is missing in the URL".into()));
Niklas Adolfsson's avatar
Niklas Adolfsson committed
64
		}
65
66

		let client = match target.scheme_str() {
67
			Some("http") => HyperClient::Http(Client::new()),
68
69
70
			#[cfg(feature = "tls")]
			Some("https") => {
				let connector = match cert_store {
71
72
73
74
					CertificateStore::Native => hyper_rustls::HttpsConnectorBuilder::new()
						.with_native_roots()
						.https_or_http()
						.enable_http1()
75
						.build(),
76
77
78
79
					CertificateStore::WebPki => hyper_rustls::HttpsConnectorBuilder::new()
						.with_webpki_roots()
						.https_or_http()
						.enable_http1()
80
						.build(),
81
82
					_ => return Err(Error::InvalidCertficateStore),
				};
83
				HyperClient::Https(Client::builder().build::<_, hyper::Body>(connector))
84
85
86
87
88
89
90
91
92
			}
			_ => {
				#[cfg(feature = "tls")]
				let err = "URL scheme not supported, expects 'http' or 'https'";
				#[cfg(not(feature = "tls"))]
				let err = "URL scheme not supported, expects 'http'";
				return Err(Error::Url(err.into()));
			}
		};
93
		Ok(Self { target, client, max_request_body_size, max_log_length })
Niklas Adolfsson's avatar
Niklas Adolfsson committed
94
95
	}

96
	async fn inner_send(&self, body: String) -> Result<hyper::Response<hyper::Body>, Error> {
97
		tx_log_from_str(&body, self.max_log_length);
Niklas Adolfsson's avatar
Niklas Adolfsson committed
98

Niklas Adolfsson's avatar
Niklas Adolfsson committed
99
		if body.len() > self.max_request_body_size as usize {
Niklas Adolfsson's avatar
Niklas Adolfsson committed
100
101
102
			return Err(Error::RequestTooLarge);
		}

103
		let req = hyper::Request::post(&self.target)
104
105
106
107
108
109
			.header(hyper::header::CONTENT_TYPE, hyper::header::HeaderValue::from_static(CONTENT_TYPE_JSON))
			.header(hyper::header::ACCEPT, hyper::header::HeaderValue::from_static(CONTENT_TYPE_JSON))
			.body(From::from(body))
			.expect("URI and request headers are valid; qed");

		let response = self.client.request(req).await.map_err(|e| Error::Http(Box::new(e)))?;
Niklas Adolfsson's avatar
Niklas Adolfsson committed
110
111
112
113
114
115
116
		if response.status().is_success() {
			Ok(response)
		} else {
			Err(Error::RequestFailure { status_code: response.status().into() })
		}
	}

117
	/// Send serialized message and wait until all bytes from the HTTP message body have been read.
118
119
	pub(crate) async fn send_and_read_body(&self, body: String) -> Result<Vec<u8>, Error> {
		let response = self.inner_send(body).await?;
120
		let (parts, body) = response.into_parts();
121
		let (body, _) = http_helpers::read_body(&parts.headers, body, self.max_request_body_size).await?;
122
123
124

		rx_log_from_bytes(&body, self.max_log_length);

125
126
		Ok(body)
	}
Niklas Adolfsson's avatar
Niklas Adolfsson committed
127

128
129
130
	/// Send serialized message without reading the HTTP message body.
	pub(crate) async fn send(&self, body: String) -> Result<(), Error> {
		let _ = self.inner_send(body).await?;
131

132
		Ok(())
Niklas Adolfsson's avatar
Niklas Adolfsson committed
133
134
135
136
137
	}
}

/// Error that can happen during a request.
#[derive(Debug, Error)]
138
pub enum Error {
Niklas Adolfsson's avatar
Niklas Adolfsson committed
139
140
141
142
143
	/// Invalid URL.
	#[error("Invalid Url: {0}")]
	Url(String),

	/// Error during the HTTP request, including networking errors and HTTP protocol errors.
144
	#[error("HTTP error: {0}")]
Niklas Adolfsson's avatar
Niklas Adolfsson committed
145
146
147
148
149
150
151
152
153
154
155
156
	Http(Box<dyn std::error::Error + Send + Sync>),

	/// Server returned a non-success status code.
	#[error("Server returned an error status code: {:?}", status_code)]
	RequestFailure {
		/// Status code returned by the server.
		status_code: u16,
	},

	/// Request body too large.
	#[error("The request body was too large")]
	RequestTooLarge,
157
158
159
160

	/// Malformed request.
	#[error("Malformed request")]
	Malformed,
161
162
163
164

	/// Invalid certificate store.
	#[error("Invalid certificate store")]
	InvalidCertficateStore,
Niklas Adolfsson's avatar
Niklas Adolfsson committed
165
166
167
168
169
170
171
172
173
}

impl<T> From<GenericTransportError<T>> for Error
where
	T: std::error::Error + Send + Sync + 'static,
{
	fn from(err: GenericTransportError<T>) -> Self {
		match err {
			GenericTransportError::<T>::TooLarge => Self::RequestTooLarge,
174
			GenericTransportError::<T>::Malformed => Self::Malformed,
Niklas Adolfsson's avatar
Niklas Adolfsson committed
175
176
177
178
179
180
181
			GenericTransportError::<T>::Inner(e) => Self::Http(Box::new(e)),
		}
	}
}

#[cfg(test)]
mod tests {
182
	use super::{CertificateStore, Error, HttpTransportClient};
Niklas Adolfsson's avatar
Niklas Adolfsson committed
183

184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
	fn assert_target(
		client: &HttpTransportClient,
		host: &str,
		scheme: &str,
		path_and_query: &str,
		port: u16,
		max_request_size: u32,
	) {
		assert_eq!(client.target.scheme_str(), Some(scheme));
		assert_eq!(client.target.path_and_query().map(|pq| pq.as_str()), Some(path_and_query));
		assert_eq!(client.target.host(), Some(host));
		assert_eq!(client.target.port_u16(), Some(port));
		assert_eq!(client.max_request_body_size, max_request_size);
	}

Niklas Adolfsson's avatar
Niklas Adolfsson committed
199
200
	#[test]
	fn invalid_http_url_rejected() {
201
		let err = HttpTransportClient::new("ws://localhost:9933", 80, CertificateStore::Native, 80).unwrap_err();
Niklas Adolfsson's avatar
Niklas Adolfsson committed
202
203
204
		assert!(matches!(err, Error::Url(_)));
	}

205
206
207
	#[cfg(feature = "tls")]
	#[test]
	fn https_works() {
208
		let client = HttpTransportClient::new("https://localhost:9933", 80, CertificateStore::Native, 80).unwrap();
209
210
211
212
213
214
		assert_target(&client, "localhost", "https", "/", 9933, 80);
	}

	#[cfg(not(feature = "tls"))]
	#[test]
	fn https_fails_without_tls_feature() {
215
		let err = HttpTransportClient::new("https://localhost:9933", 80, CertificateStore::Native, 80).unwrap_err();
216
217
218
219
220
		assert!(matches!(err, Error::Url(_)));
	}

	#[test]
	fn faulty_port() {
221
		let err = HttpTransportClient::new("http://localhost:-43", 80, CertificateStore::Native, 80).unwrap_err();
222
		assert!(matches!(err, Error::Url(_)));
223
		let err = HttpTransportClient::new("http://localhost:-99999", 80, CertificateStore::Native, 80).unwrap_err();
224
225
226
227
228
229
		assert!(matches!(err, Error::Url(_)));
	}

	#[test]
	fn url_with_path_works() {
		let client =
230
231
			HttpTransportClient::new("http://localhost:9944/my-special-path", 1337, CertificateStore::Native, 80)
				.unwrap();
232
233
234
235
236
237
238
239
240
		assert_target(&client, "localhost", "http", "/my-special-path", 9944, 1337);
	}

	#[test]
	fn url_with_query_works() {
		let client = HttpTransportClient::new(
			"http://127.0.0.1:9999/my?name1=value1&name2=value2",
			u32::MAX,
			CertificateStore::WebPki,
241
			80,
242
243
244
245
246
247
248
249
		)
		.unwrap();
		assert_target(&client, "127.0.0.1", "http", "/my?name1=value1&name2=value2", 9999, u32::MAX);
	}

	#[test]
	fn url_with_fragment_is_ignored() {
		let client =
250
			HttpTransportClient::new("http://127.0.0.1:9944/my.htm#ignore", 999, CertificateStore::Native, 80).unwrap();
251
252
253
		assert_target(&client, "127.0.0.1", "http", "/my.htm", 9944, 999);
	}

Niklas Adolfsson's avatar
Niklas Adolfsson committed
254
255
256
	#[tokio::test]
	async fn request_limit_works() {
		let eighty_bytes_limit = 80;
257
		let client = HttpTransportClient::new("http://localhost:9933", 80, CertificateStore::WebPki, 99).unwrap();
Niklas Adolfsson's avatar
Niklas Adolfsson committed
258
		assert_eq!(client.max_request_body_size, eighty_bytes_limit);
Niklas Adolfsson's avatar
Niklas Adolfsson committed
259

260
261
262
		let body = "a".repeat(81);
		assert_eq!(body.len(), 81);
		let response = client.send(body).await.unwrap_err();
Niklas Adolfsson's avatar
Niklas Adolfsson committed
263
264
265
		assert!(matches!(response, Error::RequestTooLarge));
	}
}