1use std::borrow::Cow;
2use std::io::Write;
3use std::{
4 fmt::Display,
5 str::FromStr,
6 time::{Duration, SystemTime},
7};
8
9use pink::{
10 chain_extension::{
11 self as ext, HttpRequest, HttpRequestError, HttpResponse, JsCode, JsValue, PinkExtBackend,
12 SigType, StorageQuotaExceeded,
13 },
14 types::sgx::SgxQuote,
15 Balance, EcdhPublicKey, EcdsaPublicKey, EcdsaSignature, Hash,
16};
17use reqwest::{
18 header::{HeaderMap, HeaderName, HeaderValue},
19 Method,
20};
21use reqwest_env_proxy::EnvProxyBuilder;
22use sp_core::{ByteArray as _, Pair};
23
24pub mod local_cache;
25pub mod mock_ext;
26
27pub trait PinkRuntimeEnv {
28 type AccountId: AsRef<[u8]> + Display;
29
30 fn address(&self) -> &Self::AccountId;
31}
32
33pub struct DefaultPinkExtension<'a, T, Error> {
34 pub env: &'a T,
35 _e: std::marker::PhantomData<Error>,
36}
37
38impl<'a, T, E> DefaultPinkExtension<'a, T, E> {
39 pub fn new(env: &'a T) -> Self {
40 Self {
41 env,
42 _e: std::marker::PhantomData,
43 }
44 }
45}
46
47fn block_on<F: core::future::Future>(f: F) -> F::Output {
48 match tokio::runtime::Handle::try_current() {
49 Ok(handle) => handle.block_on(f),
50 Err(_) => tokio::runtime::Runtime::new()
51 .expect("Failed to create tokio runtime")
52 .block_on(f),
53 }
54}
55
56pub fn batch_http_request(requests: Vec<HttpRequest>, timeout_ms: u64) -> ext::BatchHttpResult {
57 const MAX_CONCURRENT_REQUESTS: usize = 5;
58 if requests.len() > MAX_CONCURRENT_REQUESTS {
59 return Err(ext::HttpRequestError::TooManyRequests);
60 }
61 block_on(async move {
62 let futs = requests
63 .into_iter()
64 .map(|request| async_http_request(request, timeout_ms));
65 tokio::time::timeout(
66 Duration::from_millis(timeout_ms + 200),
67 futures::future::join_all(futs),
68 )
69 .await
70 })
71 .or(Err(ext::HttpRequestError::Timeout))
72}
73
74pub fn http_request(
75 request: HttpRequest,
76 timeout_ms: u64,
77) -> Result<HttpResponse, HttpRequestError> {
78 use HttpRequestError::*;
79 match block_on(async_http_request(request, timeout_ms)) {
80 Ok(resp) => Ok(resp),
81 Err(err) => match err {
82 InvalidUrl | InvalidMethod | InvalidHeaderName | InvalidHeaderValue
84 | FailedToCreateClient | Timeout => Err(err),
85 _ => {
86 log::error!("chain_ext: http request failed: {}", err.display());
89 Ok(HttpResponse {
90 status_code: 524,
91 reason_phrase: "IO Error".into(),
92 body: format!("{err:?}").into_bytes(),
93 headers: vec![],
94 })
95 }
96 },
97 }
98}
99
100async fn async_http_request(
101 request: HttpRequest,
102 timeout_ms: u64,
103) -> Result<HttpResponse, HttpRequestError> {
104 if timeout_ms == 0 {
105 return Err(HttpRequestError::Timeout);
106 }
107 let timeout = Duration::from_millis(timeout_ms);
108 let url: reqwest::Url = request.url.parse().or(Err(HttpRequestError::InvalidUrl))?;
109 let client = reqwest::Client::builder()
110 .trust_dns(true)
111 .timeout(timeout)
112 .env_proxy(url.host_str().unwrap_or_default())
113 .build()
114 .or(Err(HttpRequestError::FailedToCreateClient))?;
115
116 let method: Method =
117 FromStr::from_str(request.method.as_str()).or(Err(HttpRequestError::InvalidMethod))?;
118 let mut headers = HeaderMap::new();
119 for (key, value) in &request.headers {
120 let key =
121 HeaderName::from_str(key.as_str()).or(Err(HttpRequestError::InvalidHeaderName))?;
122 let value = HeaderValue::from_str(value).or(Err(HttpRequestError::InvalidHeaderValue))?;
123 headers.insert(key, value);
124 }
125
126 let result = client
127 .request(method, url)
128 .headers(headers)
129 .body(request.body)
130 .send()
131 .await;
132
133 let mut response = match result {
134 Ok(response) => response,
135 Err(err) => {
136 return Ok(HttpResponse {
139 status_code: 523,
140 reason_phrase: "Unreachable".into(),
141 body: format!("{err:?}").into_bytes(),
142 headers: vec![],
143 });
144 }
145 };
146
147 let headers: Vec<_> = response
148 .headers()
149 .iter()
150 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().into()))
151 .collect();
152
153 const MAX_BODY_SIZE: usize = 1024 * 1024 * 2; let mut body = Vec::new();
156 let mut writer = LimitedWriter::new(&mut body, MAX_BODY_SIZE);
157
158 while let Some(chunk) = response
159 .chunk()
160 .await
161 .or(Err(HttpRequestError::NetworkError))?
162 {
163 writer
164 .write_all(&chunk)
165 .or(Err(HttpRequestError::ResponseTooLarge))?;
166 }
167
168 let response = HttpResponse {
169 status_code: response.status().as_u16(),
170 reason_phrase: response
171 .status()
172 .canonical_reason()
173 .unwrap_or_default()
174 .into(),
175 body,
176 headers,
177 };
178 Ok(response)
179}
180
181impl<T: PinkRuntimeEnv, E: From<&'static str>> PinkExtBackend for DefaultPinkExtension<'_, T, E> {
182 type Error = E;
183 fn http_request(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error> {
184 http_request(request, 10 * 1000).map_err(|err| err.display().into())
185 }
186
187 fn batch_http_request(
188 &self,
189 requests: Vec<HttpRequest>,
190 timeout_ms: u64,
191 ) -> Result<ext::BatchHttpResult, Self::Error> {
192 Ok(batch_http_request(requests, timeout_ms))
193 }
194
195 fn sign(
196 &self,
197 sigtype: SigType,
198 key: Cow<[u8]>,
199 message: Cow<[u8]>,
200 ) -> Result<Vec<u8>, Self::Error> {
201 macro_rules! sign_with {
202 ($sigtype:ident) => {{
203 let pair = sp_core::$sigtype::Pair::from_seed_slice(&key).or(Err("Invalid key"))?;
204 let signature = pair.sign(&message);
205 let signature: &[u8] = signature.as_ref();
206 signature.to_vec()
207 }};
208 }
209
210 Ok(match sigtype {
211 SigType::Sr25519 => sign_with!(sr25519),
212 SigType::Ed25519 => sign_with!(ed25519),
213 SigType::Ecdsa => sign_with!(ecdsa),
214 })
215 }
216
217 fn verify(
218 &self,
219 sigtype: SigType,
220 pubkey: Cow<[u8]>,
221 message: Cow<[u8]>,
222 signature: Cow<[u8]>,
223 ) -> Result<bool, Self::Error> {
224 macro_rules! verify_with {
225 ($sigtype:ident) => {{
226 let pubkey = sp_core::$sigtype::Public::from_slice(&pubkey)
227 .map_err(|_| "Invalid public key")?;
228 let signature = sp_core::$sigtype::Signature::from_slice(&signature)
229 .ok_or("Invalid signature")?;
230 Ok(sp_core::$sigtype::Pair::verify(
231 &signature, message, &pubkey,
232 ))
233 }};
234 }
235 match sigtype {
236 SigType::Sr25519 => verify_with!(sr25519),
237 SigType::Ed25519 => verify_with!(ed25519),
238 SigType::Ecdsa => verify_with!(ecdsa),
239 }
240 }
241
242 fn derive_sr25519_key(&self, salt: Cow<[u8]>) -> Result<Vec<u8>, Self::Error> {
243 let mut seed: <sp_core::sr25519::Pair as Pair>::Seed = Default::default();
245 let len = seed.len().min(salt.len());
246 seed[..len].copy_from_slice(&salt[..len]);
247 let key = sp_core::sr25519::Pair::from_seed(&seed);
248
249 Ok(key.as_ref().secret.to_bytes().to_vec())
250 }
251
252 fn get_public_key(&self, sigtype: SigType, key: Cow<[u8]>) -> Result<Vec<u8>, Self::Error> {
253 macro_rules! public_key_with {
254 ($sigtype:ident) => {{
255 sp_core::$sigtype::Pair::from_seed_slice(&key)
256 .or(Err("Invalid key"))?
257 .public()
258 .to_raw_vec()
259 }};
260 }
261 let pubkey = match sigtype {
262 SigType::Ed25519 => public_key_with!(ed25519),
263 SigType::Sr25519 => public_key_with!(sr25519),
264 SigType::Ecdsa => public_key_with!(ecdsa),
265 };
266 Ok(pubkey)
267 }
268
269 fn cache_set(
270 &self,
271 _key: Cow<[u8]>,
272 _value: Cow<[u8]>,
273 ) -> Result<Result<(), StorageQuotaExceeded>, Self::Error> {
274 Ok(Ok(()))
275 }
276
277 fn cache_set_expiration(&self, _key: Cow<[u8]>, _expire: u64) -> Result<(), Self::Error> {
278 Ok(())
279 }
280
281 fn cache_get(&self, _key: Cow<'_, [u8]>) -> Result<Option<Vec<u8>>, Self::Error> {
282 Ok(None)
283 }
284
285 fn cache_remove(&self, _key: Cow<'_, [u8]>) -> Result<Option<Vec<u8>>, Self::Error> {
286 Ok(None)
287 }
288
289 fn log(&self, level: u8, message: Cow<str>) -> Result<(), Self::Error> {
290 let address = self.env.address();
291 let level = match level {
292 1 => log::Level::Error,
293 2 => log::Level::Warn,
294 3 => log::Level::Info,
295 4 => log::Level::Debug,
296 5 => log::Level::Trace,
297 _ => log::Level::Error,
298 };
299 log::log!(target: "pink", level, "[{}] {}", address, message);
300 Ok(())
301 }
302
303 fn getrandom(&self, length: u8) -> Result<Vec<u8>, Self::Error> {
304 let mut buf = vec![0u8; length as _];
305 getrandom::getrandom(&mut buf[..]).or(Err("Failed to get random bytes"))?;
306 Ok(buf)
307 }
308
309 fn is_in_transaction(&self) -> Result<bool, Self::Error> {
310 Ok(false)
311 }
312
313 fn ecdsa_sign_prehashed(
314 &self,
315 key: Cow<[u8]>,
316 message_hash: Hash,
317 ) -> Result<EcdsaSignature, Self::Error> {
318 let pair = sp_core::ecdsa::Pair::from_seed_slice(&key).or(Err("Invalid key"))?;
319 let signature = pair.sign_prehashed(&message_hash);
320 Ok(signature.0)
321 }
322
323 fn ecdsa_verify_prehashed(
324 &self,
325 signature: EcdsaSignature,
326 message_hash: Hash,
327 pubkey: EcdsaPublicKey,
328 ) -> Result<bool, Self::Error> {
329 let public = sp_core::ecdsa::Public(pubkey);
330 let sig = sp_core::ecdsa::Signature(signature);
331 Ok(sp_core::ecdsa::Pair::verify_prehashed(
332 &sig,
333 &message_hash,
334 &public,
335 ))
336 }
337
338 fn system_contract_id(&self) -> Result<ext::AccountId, Self::Error> {
339 Err("No default system contract id".into())
340 }
341
342 fn balance_of(&self, _account: ext::AccountId) -> Result<(Balance, Balance), Self::Error> {
343 Ok((0, 0))
344 }
345
346 fn untrusted_millis_since_unix_epoch(&self) -> Result<u64, Self::Error> {
347 let duration = SystemTime::now()
348 .duration_since(SystemTime::UNIX_EPOCH)
349 .or(Err("The system time is earlier than UNIX_EPOCH"))?;
350 Ok(duration.as_millis() as u64)
351 }
352
353 fn worker_pubkey(&self) -> Result<EcdhPublicKey, Self::Error> {
354 Ok(Default::default())
355 }
356
357 fn code_exists(&self, _code_hash: Hash, _sidevm: bool) -> Result<bool, Self::Error> {
358 Ok(false)
359 }
360
361 fn import_latest_system_code(
362 &self,
363 _payer: ext::AccountId,
364 ) -> Result<Option<Hash>, Self::Error> {
365 Ok(None)
366 }
367
368 fn runtime_version(&self) -> Result<(u32, u32), Self::Error> {
369 Ok((1, 0))
370 }
371
372 fn current_event_chain_head(&self) -> Result<(u64, Hash), Self::Error> {
373 Ok((0, Default::default()))
374 }
375
376 fn js_eval(&self, _codes: Vec<JsCode>, _args: Vec<String>) -> Result<JsValue, Self::Error> {
377 Ok(JsValue::Exception("No Js Runtime".into()))
378 }
379
380 fn worker_sgx_quote(&self) -> Result<Option<SgxQuote>, Self::Error> {
381 Ok(None)
382 }
383}
384
385struct LimitedWriter<W> {
386 writer: W,
387 written: usize,
388 limit: usize,
389}
390
391impl<W> LimitedWriter<W> {
392 fn new(writer: W, limit: usize) -> Self {
393 Self {
394 writer,
395 written: 0,
396 limit,
397 }
398 }
399}
400
401impl<W: std::io::Write> std::io::Write for LimitedWriter<W> {
402 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
403 if self.written + buf.len() > self.limit {
404 return Err(std::io::Error::new(
405 std::io::ErrorKind::Other,
406 "Buffer limit exceeded",
407 ));
408 }
409 let wlen = self.writer.write(buf)?;
410 self.written += wlen;
411 Ok(wlen)
412 }
413
414 fn flush(&mut self) -> std::io::Result<()> {
415 self.writer.flush()
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 use pink::chain_extension::HttpRequest;
424
425 #[test]
426 fn default_impl_works() {
427 use mock_ext::MockExtension;
428 use pink::chain_extension::PinkExtBackend;
429
430 let mock = MockExtension;
431 let ext = DefaultPinkExtension::<_, String>::new(&mock);
432
433 let key = b"key";
434 let value = b"value";
435 assert!(ext
436 .cache_set(Cow::Borrowed(key), Cow::Borrowed(value))
437 .is_ok());
438 assert!(ext.cache_set_expiration(Cow::Borrowed(key), 100).is_ok());
439 assert!(ext.cache_get(Cow::Borrowed(key)).is_ok());
440 assert!(ext.cache_remove(Cow::Borrowed(key)).is_ok());
441
442 ext.log(1, "error".into()).unwrap();
443 ext.log(2, "warn".into()).unwrap();
444 ext.log(3, "info".into()).unwrap();
445 ext.log(4, "debug".into()).unwrap();
446 ext.log(5, "trace".into()).unwrap();
447 ext.log(6, "unknown".into()).unwrap();
448
449 assert!(ext.is_in_transaction().is_ok());
450 assert!(ext.system_contract_id().is_err());
451 assert!(ext.balance_of([0u8; 32].into()).is_ok());
452 assert!(ext.worker_pubkey().is_ok());
453 assert!(ext.code_exists(Default::default(), false).is_ok());
454 assert!(ext.import_latest_system_code([0u8; 32].into()).is_ok());
455 assert!(ext.runtime_version().is_ok());
456 assert!(ext.current_event_chain_head().is_ok());
457 }
458
459 #[test]
460 fn test_too_large_batch_http_req() {
461 let requests = (0..10)
462 .map(|_| HttpRequest {
463 url: "https://www.google.com".into(),
464 method: "GET".into(),
465 headers: vec![],
466 body: vec![],
467 })
468 .collect::<Vec<_>>();
469 let responses = batch_http_request(requests, 10 * 1000);
470 assert!(responses.is_err());
471 }
472
473 #[cfg(coverage)]
474 #[tokio::test]
475 async fn test_http_req() {
476 let response = tokio::task::spawn_blocking(|| {
477 http_request(
478 HttpRequest {
479 url: "https://httpbin.org/get".into(),
480 method: "GET".into(),
481 headers: vec![("X-Foo".to_string(), "bar".to_string())],
482 body: vec![],
483 },
484 1000 * 10,
485 )
486 })
487 .await;
488 assert!(response.is_ok());
489 }
490
491 #[tokio::test]
492 async fn test_http_req_net_error() {
493 let response = tokio::task::spawn_blocking(|| {
494 http_request(
495 HttpRequest {
496 url: "http://127.0.0.1:54321/get".into(),
497 method: "GET".into(),
498 headers: vec![],
499 body: vec![],
500 },
501 1000 * 10,
502 )
503 })
504 .await;
505 assert_eq!(response.unwrap().unwrap().status_code, 523);
506 }
507
508 #[tokio::test]
509 async fn test_http_req_zero_timeout() {
510 let response = tokio::task::spawn_blocking(|| {
511 http_request(
512 HttpRequest {
513 url: "https://httpbin.org/get".into(),
514 method: "GET".into(),
515 headers: vec![],
516 body: vec![],
517 },
518 0,
519 )
520 })
521 .await;
522 assert!(response.unwrap().is_err());
523 }
524}