utils/
netutil.rs

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