Skip to main content

acts_package_http/
package.rs

1use acts::{
2    ActError, ActPackage, ActPackageCatalog, ActPackageDefinition, ActRunAs, Result, Vars,
3    include_json,
4};
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, InvalidHeaderValue};
7use serde::{Deserialize, Serialize};
8use serde_json::Value as JsonValue;
9
10const DATA_KEY: &str = "data";
11
12#[derive(Debug, Clone, Default, Deserialize, Serialize)]
13pub enum ContentType {
14    #[serde(rename(deserialize = "none"))]
15    None,
16    #[serde(rename(deserialize = "text"))]
17    Text,
18    #[serde(rename(deserialize = "html"))]
19    Html,
20    #[default]
21    #[serde(rename(deserialize = "json"))]
22    Json,
23    #[serde(rename(deserialize = "urlencoded"))]
24    UrlEncoded,
25    #[serde(rename(deserialize = "form-data"))]
26    FormData,
27    #[serde(rename(deserialize = "binary"))]
28    Binary,
29    #[serde(rename(deserialize = "image"))]
30    Image,
31    #[serde(rename(deserialize = "video"))]
32    Video,
33    #[serde(rename(deserialize = "audio"))]
34    Audio,
35}
36
37#[derive(Debug, Clone, Deserialize, Serialize)]
38pub struct Pair {
39    pub key: String,
40    pub value: JsonValue,
41}
42
43#[derive(Debug, Clone)]
44pub struct HttpPackage;
45
46#[derive(Debug, Clone, Deserialize, Serialize)]
47pub struct HttpPackageParams {
48    pub url: String,
49    pub method: String,
50    #[serde(default)]
51    #[serde(rename(deserialize = "content-type"))]
52    pub content_type: ContentType,
53    #[serde(default)]
54    pub headers: Vec<Pair>,
55    #[serde(default)]
56    pub params: Vec<Pair>,
57    pub body: Option<JsonValue>,
58}
59
60#[async_trait::async_trait]
61impl ActPackage for HttpPackage {
62    fn definition() -> ActPackageDefinition {
63        ActPackageDefinition {
64            id: "acts.core.http",
65            name: "Http",
66            desc: "do a http request",
67            version: "0.1.0",
68            icon: r#"<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor"><path d="M53.312 512a458.688 458.688 0 1 1 917.376 0A458.688 458.688 0 0 1 53.312 512z m339.52-376.32a394.048 394.048 0 0 0-138.88 77.632c25.6 22.08 53.952 40.96 84.48 55.936 6.784-25.408 14.528-49.152 23.168-70.848 9.216-22.912 19.584-44.16 31.232-62.72zM209.024 258.944A392.896 392.896 0 0 0 118.592 480h191.232c1.472-51.584 6.528-100.992 14.656-146.688a459.136 459.136 0 0 1-115.456-74.24z m422.144 629.376a394.176 394.176 0 0 0 138.88-77.696 395.328 395.328 0 0 0-84.48-55.936 610.752 610.752 0 0 1-23.168 70.848c-9.152 22.912-19.584 44.096-31.232 62.72z m183.808-123.456A392.832 392.832 0 0 0 905.408 544H714.24a1008.704 1008.704 0 0 1-14.72 146.624c42.24 19.008 81.152 44.16 115.456 74.304zM905.408 480a392.96 392.96 0 0 0-90.432-220.928 459.2 459.2 0 0 1-115.392 74.24c8.064 45.696 13.12 95.104 14.656 146.688h191.168zM769.92 213.312a394.112 394.112 0 0 0-138.88-77.696c11.712 18.688 22.144 39.872 31.296 62.784 8.704 21.76 16.448 45.44 23.104 70.848a395.328 395.328 0 0 0 84.48-55.936zM392.832 888.384a399.04 399.04 0 0 1-31.232-62.784 610.624 610.624 0 0 1-23.104-70.848 395.264 395.264 0 0 0-84.48 55.936 394.048 394.048 0 0 0 138.88 77.696z m-183.744-123.456a459.136 459.136 0 0 1 115.392-74.24A1008.448 1008.448 0 0 1 309.76 544H118.656a392.896 392.896 0 0 0 90.496 220.928zM512 117.312c-11.904 0-26.496 5.952-43.2 23.552-16.64 17.664-33.216 44.928-47.744 81.28-8.448 21.12-16 44.8-22.528 70.656A394.752 394.752 0 0 0 512 309.312c39.488 0 77.568-5.76 113.536-16.512a557.824 557.824 0 0 0-22.528-70.592c-14.592-36.416-31.104-63.68-47.808-81.344-16.64-17.6-31.232-23.552-43.2-23.552zM373.824 480h276.352a953.28 953.28 0 0 0-11.712-124.352A458.88 458.88 0 0 1 512 373.312a458.88 458.88 0 0 1-126.4-17.664A953.216 953.216 0 0 0 373.76 480z m11.776 188.352A458.944 458.944 0 0 1 512 650.688c43.84 0 86.272 6.144 126.464 17.664 6.272-38.656 10.368-80.448 11.712-124.352H373.824c1.344 43.904 5.44 85.76 11.776 124.352zM512 714.688c-39.424 0-77.568 5.76-113.472 16.512 6.464 25.792 14.08 49.472 22.528 70.592 14.528 36.416 31.04 63.68 47.744 81.344 16.64 17.6 31.296 23.552 43.2 23.552 11.968 0 26.56-5.952 43.2-23.552 16.704-17.664 33.28-44.928 47.808-81.28 8.448-21.184 16-44.8 22.464-70.656A394.752 394.752 0 0 0 512 714.688z" ></path></svg>"#,
69            doc: "",
70            schema: include_json!("./in-schema.json"),
71            options: Some(include_json!("./ui-schema.json")),
72            run_as: ActRunAs::Irq,
73            resources: vec![],
74            catalog: ActPackageCatalog::Core,
75        }
76    }
77
78    fn new(_: &acts::Config) -> Result<Self>
79    where
80        Self: Sized,
81    {
82        Ok(Self)
83    }
84
85    async fn execute(
86        &self,
87        _ctx: &acts::Context,
88        params: &serde_json::Value,
89    ) -> Result<Option<Vars>> {
90        let mut ret = Vars::new();
91        let mut headers = HeaderMap::new();
92        headers.insert(
93            HeaderName::from_static("accept"),
94            HeaderValue::from_static("*/*"),
95        );
96
97        let params = serde_json::from_value::<HttpPackageParams>(params.clone()).map_err(|e| {
98            ActError::Package(format!(
99                "invalid ActPackage({}) params: {}",
100                Self::definition().id,
101                e
102            ))
103        })?;
104
105        for Pair { key, value } in &params.headers {
106            headers.insert(
107                key.parse::<HeaderName>()
108                    .map_err(|err| ActError::Runtime(err.to_string()))?,
109                value
110                    .to_string()
111                    .parse()
112                    .map_err(|err: InvalidHeaderValue| ActError::Runtime(err.to_string()))?,
113            );
114        }
115        let mut query = Vec::new();
116        for Pair { key, value } in &params.params {
117            query.push((key.clone(), value.clone()));
118        }
119
120        let c = reqwest::blocking::Client::new();
121        let mut request = c
122            .request(
123                params.method.parse().map_err(|_| {
124                    ActError::Runtime(format!("invalid method '{}'", params.method))
125                })?,
126                &params.url,
127            )
128            .headers(headers)
129            .query(&query);
130
131        match params.content_type {
132            ContentType::Text | ContentType::Html => {
133                if let Some(text) = &params.body {
134                    let data = text.as_str().ok_or(ActError::Package(
135                        "content-type did not match the body content".to_string(),
136                    ))?;
137                    request = request.body::<String>(data.to_string());
138                }
139            }
140            ContentType::Json => {
141                if let Some(json) = &params.body {
142                    let body = serde_json::to_vec(json)?;
143                    request = request.body(body);
144                }
145            }
146            ContentType::FormData | ContentType::UrlEncoded => {
147                if let Some(form) = &params.body {
148                    let data = form.as_object().ok_or(ActError::Package(
149                        "content-type did not match the body content".to_string(),
150                    ))?;
151                    request = request.form(data);
152                }
153            }
154            ContentType::Binary | ContentType::Image | ContentType::Video | ContentType::Audio => {
155                if let Some(value) = &params.body {
156                    let data = value.as_str().ok_or(ActError::Package(
157                        "content-type did not match the body content".to_string(),
158                    ))?;
159                    let data = STANDARD
160                        .decode(data)
161                        .map_err(|err| ActError::Package(err.to_string()))?;
162                    request = request.body(data);
163                }
164            }
165            _ => {}
166        }
167
168        let res = request
169            .send()
170            .map_err(|err| ActError::Runtime(format!("Http error: {err}")))?;
171
172        let default_type = HeaderValue::from_static("application/json");
173        let response_type = res
174            .headers()
175            .get(CONTENT_TYPE)
176            .unwrap_or(&default_type)
177            .to_str()
178            .map_err(|err| ActError::Package(err.to_string()))?;
179        let status = res.status();
180        let response_type = get_content_type(response_type);
181        match response_type {
182            ContentType::Text | ContentType::Html => {
183                ret.insert(
184                    DATA_KEY.to_string(),
185                    res.text().map_err(map_package_err)?.into(),
186                );
187            }
188            ContentType::Json => {
189                ret.insert(
190                    DATA_KEY.to_string(),
191                    res.json::<serde_json::Value>().map_err(map_package_err)?,
192                );
193            }
194            ContentType::Binary | ContentType::Image | ContentType::Video | ContentType::Audio => {
195                let data = res.bytes().map_err(map_package_err)?.to_vec();
196                let data = STANDARD.encode(&data);
197                ret.insert(DATA_KEY.to_string(), data.into());
198            }
199            _ => {}
200        }
201        if !status.is_success() {
202            return Err(ActError::Exception {
203                ecode: status.as_u16().to_string(),
204                message: ret.get(DATA_KEY).unwrap_or(status.to_string()),
205            });
206        }
207
208        Ok(Some(ret))
209    }
210}
211
212fn map_package_err(err: reqwest::Error) -> ActError {
213    ActError::Package(err.to_string())
214}
215
216fn get_content_type(mime_type: &str) -> ContentType {
217    let mut ret = ContentType::None;
218    if mime_type.starts_with("application/json") {
219        ret = ContentType::Json;
220    } else if mime_type.starts_with("text/html") {
221        ret = ContentType::Html;
222    } else if mime_type.starts_with("application/x-www-form-urlencoded") {
223        ret = ContentType::UrlEncoded;
224    } else if mime_type.starts_with("multipart/form-data") {
225        ret = ContentType::FormData;
226    } else if mime_type.starts_with("image/") {
227        ret = ContentType::Image;
228    } else if mime_type.starts_with("audio/") {
229        ret = ContentType::Audio;
230    } else if mime_type.starts_with("video/") {
231        ret = ContentType::Video;
232    } else if mime_type.starts_with("text/") || mime_type.starts_with("application/javascript") {
233        ret = ContentType::Text;
234    }
235
236    ret
237}