utils/
netutil.rs

1//! URL encoding や SHA 計算等のユーティリティ。
2//!
3use anyhow::{Context, Result, anyhow};
4use hmac::{KeyInit, Mac, SimpleHmac, digest::CtOutput};
5use percent_encoding::{AsciiSet, utf8_percent_encode};
6use reqwest::{Client, RequestBuilder, Response};
7use serde::Deserialize;
8use sha1::Sha1;
9use sha2::Sha256;
10use std::time::{Duration, Instant};
11use thiserror::Error;
12
13const RETRY_TIMEOUT: Duration = Duration::from_secs(5);
14const RETRY_INTERVAL: Duration = Duration::from_millis(500);
15
16pub async fn send_with_retry(mut build_req: impl FnMut() -> RequestBuilder) -> Result<Response> {
17    let start = Instant::now();
18    loop {
19        let res = build_req().send().await.context("HTTP request failed");
20        if let Err(ref err) = res
21            && Instant::now() - start < RETRY_TIMEOUT
22        {
23            log::error!("{err:#?}");
24            log::warn!("HTTP request failed, retrying...");
25            tokio::time::sleep(RETRY_INTERVAL).await;
26        } else {
27            break res;
28        }
29    }
30}
31
32#[derive(Debug, Error)]
33#[error("Http error {status} {body}")]
34pub struct HttpStatusError {
35    pub status: u16,
36    pub body: String,
37}
38
39/// HTTP status が成功 (200 台) でなければ Err に変換する。
40///
41/// 成功ならば response body を文字列に変換して返す。
42pub async fn check_http_resp(resp: reqwest::Response) -> Result<String> {
43    let status = resp.status();
44    let text = resp.text().await?;
45
46    if status.is_success() {
47        Ok(text)
48    } else {
49        Err(anyhow!(HttpStatusError {
50            status: status.as_u16(),
51            body: text
52        }))
53    }
54}
55
56/// HTTP status が成功 (200 台) でなければ Err に変換する。
57///
58/// 成功ならば response body をバイト列に変換して返す。
59#[allow(unused)]
60pub async fn check_http_resp_bin(resp: reqwest::Response) -> Result<Vec<u8>> {
61    let status = resp.status();
62
63    if status.is_success() {
64        let bin = resp.bytes().await?.to_vec();
65        Ok(bin)
66    } else {
67        let text = resp.text().await?;
68        Err(anyhow!(HttpStatusError {
69            status: status.as_u16(),
70            body: text
71        }))
72    }
73}
74
75/// [send_with_retry] [check_http_resp] 付きの GET。
76pub async fn checked_get_url(client: &Client, url: &str) -> Result<String> {
77    let resp = send_with_retry(|| client.get(url)).await?;
78
79    check_http_resp(resp).await
80}
81
82pub async fn checked_get_url_bin(client: &Client, url: &str) -> Result<Vec<u8>> {
83    let resp = send_with_retry(|| client.get(url)).await?;
84
85    check_http_resp_bin(resp).await
86}
87
88/// 文字列を JSON としてパースし、T 型に変換する。
89///
90/// 変換エラーが発生した場合はエラーにソース文字列を付加する。
91pub fn convert_from_json<'a, T>(json_str: &'a str) -> Result<T>
92where
93    T: Deserialize<'a>,
94{
95    let obj = serde_json::from_str::<T>(json_str)
96        .with_context(|| format!("JSON parse failed: {json_str}"))?;
97
98    Ok(obj)
99}
100
101/// [percent_encode] で変換する文字セット。
102///
103///  curl_easy_escape() と同じ。
104const FRAGMENT: &AsciiSet = &percent_encoding::NON_ALPHANUMERIC
105    .remove(b'-')
106    .remove(b'.')
107    .remove(b'_')
108    .remove(b'~');
109
110pub fn percent_encode(input: &str) -> String {
111    utf8_percent_encode(input, FRAGMENT).to_string()
112}
113
114pub fn html_escape(src: &str) -> String {
115    let mut result = String::new();
116    for c in src.chars() {
117        match c {
118            '&' => result.push_str("&amp;"),
119            '"' => result.push_str("&quot;"),
120            '\'' => result.push_str("&apos;"),
121            '<' => result.push_str("&lt;"),
122            '>' => result.push_str("&gt;"),
123            _ => result.push(c),
124        }
125    }
126
127    result
128}
129
130pub type HmacSha1 = SimpleHmac<Sha1>;
131pub type HmacSha256 = SimpleHmac<Sha256>;
132
133/// HMAC SHA1 を計算する。
134///
135/// 返り値は constant time 比較可能なオブジェクト。
136/// into_bytes() で内部のバイト列を取得できるが、一致検証に用いる場合は
137/// タイミング攻撃回避のため通常の比較をしてはならない。
138pub fn hmac_sha1(key: &[u8], data: &[u8]) -> CtOutput<HmacSha1> {
139    let mut mac = HmacSha1::new_from_slice(key).unwrap();
140    mac.update(data);
141
142    mac.finalize()
143}
144
145/// HMAC SHA2 を計算して検証する。
146pub fn hmac_sha256_verify(key: &[u8], data: &[u8], expected: &[u8]) -> Result<()> {
147    let mut mac = HmacSha256::new_from_slice(key).unwrap();
148    mac.update(data);
149    mac.verify_slice(expected)?;
150
151    Ok(())
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use hex_literal::hex;
158
159    // https://developer.twitter.com/en/docs/authentication/oauth-1-0a/percent-encoding-parameters
160    #[test]
161    fn percent_encode_twitter_1() {
162        let str = "Ladies + Gentlemen";
163        let result = percent_encode(str);
164        let expected = "Ladies%20%2B%20Gentlemen";
165        assert_eq!(result, expected);
166    }
167
168    #[test]
169    fn percent_encode_twitter_2() {
170        let str = "An encoded string!";
171        let result = percent_encode(str);
172        let expected = "An%20encoded%20string%21";
173        assert_eq!(result, expected);
174    }
175
176    #[test]
177    fn percent_encode_twitter_3() {
178        let str = "Dogs, Cats & Mice";
179        let result = percent_encode(str);
180        let expected = "Dogs%2C%20Cats%20%26%20Mice";
181        assert_eq!(result, expected);
182    }
183
184    #[test]
185    fn percent_encode_twitter_4() {
186        let str = "☃";
187        let result = percent_encode(str);
188        let expected = "%E2%98%83";
189        assert_eq!(result, expected);
190    }
191
192    #[test]
193    fn html_escape_1() {
194        let str = "\"<a href='test'>Test&Test</a>\"";
195        let result = html_escape(str);
196        let expected = "&quot;&lt;a href=&apos;test&apos;&gt;Test&amp;Test&lt;/a&gt;&quot;";
197        assert_eq!(result, expected);
198    }
199
200    #[test]
201    fn hmac_sha1_rfc2202_1() {
202        let key = &hex!("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
203        let data = b"Hi There";
204        let result = hmac_sha1(key, data).into_bytes();
205        let expected = &hex!("b617318655057264e28bc0b6fb378c8ef146be00");
206        assert_eq!(result[..], expected[..]);
207    }
208
209    #[test]
210    fn hmac_sha1_rfc2202_2() {
211        let key = b"Jefe";
212        let data = b"what do ya want for nothing?";
213        let result = hmac_sha1(key, data).into_bytes();
214        let expected = &hex!("effcdf6ae5eb2fa2d27416d5f184df9c259a7c79");
215        assert_eq!(result[..], expected[..]);
216    }
217
218    #[test]
219    fn hmac_sha1_rfc2202_3() {
220        let key = &hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
221        // 0xdd repeated 50 times
222        let data = &[0xdd_u8; 50];
223        let result = hmac_sha1(key, data).into_bytes();
224        let expected = &hex!("125d7342b9ac11cd91a39af48aa17b4f63f175d3");
225        assert_eq!(result[..], expected[..]);
226    }
227
228    #[test]
229    fn hmac_sha1_rfc2202_4() {
230        let key = &hex!("0102030405060708090a0b0c0d0e0f10111213141516171819");
231        // 0xcd repeated 50 times
232        let data = &[0xcd_u8; 50];
233        let result = hmac_sha1(key, data).into_bytes();
234        let expected = &hex!("4c9007f4026250c6bc8414f9bf50c86c2d7235da");
235        assert_eq!(result[..], expected[..]);
236    }
237
238    #[test]
239    fn hmac_sha1_rfc2202_5() {
240        let key = &hex!("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c");
241        let data = b"Test With Truncation";
242        let result = hmac_sha1(key, data).into_bytes();
243        let expected = &hex!("4c1a03424b55e07fe7f27be1d58bb9324a9a5a04");
244        assert_eq!(result[..], expected[..]);
245    }
246
247    #[test]
248    fn hmac_sha1_rfc2202_6() {
249        // 0xaa repeated 80 times
250        let key = &[0xaa_u8; 80];
251        let data = b"Test Using Larger Than Block-Size Key - Hash Key First";
252        let result = hmac_sha1(key, data).into_bytes();
253        let expected = &hex!("aa4ae5e15272d00e95705637ce8a3b55ed402112");
254        assert_eq!(result[..], expected[..]);
255    }
256
257    #[test]
258    fn hmac_sha1_rfc2202_7() {
259        // 0xaa repeated 80 times
260        let key = &[0xaa_u8; 80];
261        let data = b"Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data";
262        let result = hmac_sha1(key, data).into_bytes();
263        let expected = &hex!("e8e99d0f45237d786d6bbaa7965c7808bbff1a91");
264        assert_eq!(result[..], expected[..]);
265    }
266}