16dbb5987Sopenharmony_ci// Copyright (c) 2023 Huawei Device Co., Ltd. 26dbb5987Sopenharmony_ci// Licensed under the Apache License, Version 2.0 (the "License"); 36dbb5987Sopenharmony_ci// you may not use this file except in compliance with the License. 46dbb5987Sopenharmony_ci// You may obtain a copy of the License at 56dbb5987Sopenharmony_ci// 66dbb5987Sopenharmony_ci// http://www.apache.org/licenses/LICENSE-2.0 76dbb5987Sopenharmony_ci// 86dbb5987Sopenharmony_ci// Unless required by applicable law or agreed to in writing, software 96dbb5987Sopenharmony_ci// distributed under the License is distributed on an "AS IS" BASIS, 106dbb5987Sopenharmony_ci// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 116dbb5987Sopenharmony_ci// See the License for the specific language governing permissions and 126dbb5987Sopenharmony_ci// limitations under the License. 136dbb5987Sopenharmony_ci 146dbb5987Sopenharmony_ci//! Proxy implementation. 156dbb5987Sopenharmony_ci 166dbb5987Sopenharmony_ciuse core::convert::TryFrom; 176dbb5987Sopenharmony_ciuse std::net::IpAddr; 186dbb5987Sopenharmony_ci 196dbb5987Sopenharmony_ciuse ylong_http::headers::HeaderValue; 206dbb5987Sopenharmony_ciuse ylong_http::request::uri::{Authority, Scheme, Uri}; 216dbb5987Sopenharmony_ci 226dbb5987Sopenharmony_ciuse crate::error::HttpClientError; 236dbb5987Sopenharmony_ciuse crate::util::base64::encode; 246dbb5987Sopenharmony_ciuse crate::util::normalizer::UriFormatter; 256dbb5987Sopenharmony_ci 266dbb5987Sopenharmony_ci/// `Proxies` is responsible for managing a list of proxies. 276dbb5987Sopenharmony_ci#[derive(Clone, Default)] 286dbb5987Sopenharmony_cipub(crate) struct Proxies { 296dbb5987Sopenharmony_ci list: Vec<Proxy>, 306dbb5987Sopenharmony_ci} 316dbb5987Sopenharmony_ci 326dbb5987Sopenharmony_ciimpl Proxies { 336dbb5987Sopenharmony_ci pub(crate) fn add_proxy(&mut self, proxy: Proxy) { 346dbb5987Sopenharmony_ci self.list.push(proxy) 356dbb5987Sopenharmony_ci } 366dbb5987Sopenharmony_ci 376dbb5987Sopenharmony_ci pub(crate) fn match_proxy(&self, uri: &Uri) -> Option<&Proxy> { 386dbb5987Sopenharmony_ci self.list.iter().find(|proxy| proxy.is_intercepted(uri)) 396dbb5987Sopenharmony_ci } 406dbb5987Sopenharmony_ci} 416dbb5987Sopenharmony_ci 426dbb5987Sopenharmony_ci/// Proxy is a configuration of client which should manage the destination 436dbb5987Sopenharmony_ci/// address of request. 446dbb5987Sopenharmony_ci/// 456dbb5987Sopenharmony_ci/// A `Proxy` has below rules: 466dbb5987Sopenharmony_ci/// 476dbb5987Sopenharmony_ci/// - Manage the uri of destination address. 486dbb5987Sopenharmony_ci/// - Manage the request content such as headers. 496dbb5987Sopenharmony_ci/// - Provide no proxy function which the request will not affected by proxy. 506dbb5987Sopenharmony_ci#[derive(Clone)] 516dbb5987Sopenharmony_cipub(crate) struct Proxy { 526dbb5987Sopenharmony_ci pub(crate) intercept: Intercept, 536dbb5987Sopenharmony_ci pub(crate) no_proxy: Option<NoProxy>, 546dbb5987Sopenharmony_ci} 556dbb5987Sopenharmony_ci 566dbb5987Sopenharmony_ciimpl Proxy { 576dbb5987Sopenharmony_ci pub(crate) fn new(intercept: Intercept) -> Self { 586dbb5987Sopenharmony_ci Self { 596dbb5987Sopenharmony_ci intercept, 606dbb5987Sopenharmony_ci no_proxy: None, 616dbb5987Sopenharmony_ci } 626dbb5987Sopenharmony_ci } 636dbb5987Sopenharmony_ci 646dbb5987Sopenharmony_ci pub(crate) fn http(uri: &str) -> Result<Self, HttpClientError> { 656dbb5987Sopenharmony_ci Ok(Proxy::new(Intercept::Http(ProxyInfo::new(uri)?))) 666dbb5987Sopenharmony_ci } 676dbb5987Sopenharmony_ci 686dbb5987Sopenharmony_ci pub(crate) fn https(uri: &str) -> Result<Self, HttpClientError> { 696dbb5987Sopenharmony_ci Ok(Proxy::new(Intercept::Https(ProxyInfo::new(uri)?))) 706dbb5987Sopenharmony_ci } 716dbb5987Sopenharmony_ci 726dbb5987Sopenharmony_ci pub(crate) fn all(uri: &str) -> Result<Self, HttpClientError> { 736dbb5987Sopenharmony_ci Ok(Proxy::new(Intercept::All(ProxyInfo::new(uri)?))) 746dbb5987Sopenharmony_ci } 756dbb5987Sopenharmony_ci 766dbb5987Sopenharmony_ci pub(crate) fn basic_auth(&mut self, username: &str, password: &str) { 776dbb5987Sopenharmony_ci let auth = encode(format!("{username}:{password}").as_bytes()); 786dbb5987Sopenharmony_ci 796dbb5987Sopenharmony_ci // All characters in base64 format are valid characters, so we ignore the error. 806dbb5987Sopenharmony_ci let mut auth = HeaderValue::from_bytes(auth.as_slice()).unwrap(); 816dbb5987Sopenharmony_ci auth.set_sensitive(true); 826dbb5987Sopenharmony_ci 836dbb5987Sopenharmony_ci match &mut self.intercept { 846dbb5987Sopenharmony_ci Intercept::All(info) => info.basic_auth = Some(auth), 856dbb5987Sopenharmony_ci Intercept::Http(info) => info.basic_auth = Some(auth), 866dbb5987Sopenharmony_ci Intercept::Https(info) => info.basic_auth = Some(auth), 876dbb5987Sopenharmony_ci } 886dbb5987Sopenharmony_ci } 896dbb5987Sopenharmony_ci 906dbb5987Sopenharmony_ci pub(crate) fn no_proxy(&mut self, no_proxy: &str) { 916dbb5987Sopenharmony_ci self.no_proxy = NoProxy::from_str(no_proxy); 926dbb5987Sopenharmony_ci } 936dbb5987Sopenharmony_ci 946dbb5987Sopenharmony_ci pub(crate) fn via_proxy(&self, uri: &Uri) -> Uri { 956dbb5987Sopenharmony_ci let info = self.intercept.proxy_info(); 966dbb5987Sopenharmony_ci let mut builder = Uri::builder(); 976dbb5987Sopenharmony_ci builder = builder 986dbb5987Sopenharmony_ci .scheme(info.scheme().clone()) 996dbb5987Sopenharmony_ci .authority(info.authority().clone()); 1006dbb5987Sopenharmony_ci 1016dbb5987Sopenharmony_ci if let Some(path) = uri.path() { 1026dbb5987Sopenharmony_ci builder = builder.path(path.clone()); 1036dbb5987Sopenharmony_ci } 1046dbb5987Sopenharmony_ci 1056dbb5987Sopenharmony_ci if let Some(query) = uri.query() { 1066dbb5987Sopenharmony_ci builder = builder.query(query.clone()); 1076dbb5987Sopenharmony_ci } 1086dbb5987Sopenharmony_ci 1096dbb5987Sopenharmony_ci // Here all parts of builder is accurate. 1106dbb5987Sopenharmony_ci builder.build().unwrap() 1116dbb5987Sopenharmony_ci } 1126dbb5987Sopenharmony_ci 1136dbb5987Sopenharmony_ci pub(crate) fn is_intercepted(&self, uri: &Uri) -> bool { 1146dbb5987Sopenharmony_ci // uri is formatted uri, use unwrap directly 1156dbb5987Sopenharmony_ci let no_proxy = self 1166dbb5987Sopenharmony_ci .no_proxy 1176dbb5987Sopenharmony_ci .as_ref() 1186dbb5987Sopenharmony_ci .map(|no_proxy| no_proxy.contain(uri.host().unwrap().as_str())) 1196dbb5987Sopenharmony_ci .unwrap_or(false); 1206dbb5987Sopenharmony_ci 1216dbb5987Sopenharmony_ci match self.intercept { 1226dbb5987Sopenharmony_ci Intercept::All(_) => !no_proxy, 1236dbb5987Sopenharmony_ci Intercept::Http(_) => !no_proxy && *uri.scheme().unwrap() == Scheme::HTTP, 1246dbb5987Sopenharmony_ci Intercept::Https(_) => !no_proxy && *uri.scheme().unwrap() == Scheme::HTTPS, 1256dbb5987Sopenharmony_ci } 1266dbb5987Sopenharmony_ci } 1276dbb5987Sopenharmony_ci} 1286dbb5987Sopenharmony_ci 1296dbb5987Sopenharmony_ci#[derive(Clone)] 1306dbb5987Sopenharmony_cipub(crate) enum Intercept { 1316dbb5987Sopenharmony_ci All(ProxyInfo), 1326dbb5987Sopenharmony_ci Http(ProxyInfo), 1336dbb5987Sopenharmony_ci Https(ProxyInfo), 1346dbb5987Sopenharmony_ci} 1356dbb5987Sopenharmony_ci 1366dbb5987Sopenharmony_ciimpl Intercept { 1376dbb5987Sopenharmony_ci pub(crate) fn proxy_info(&self) -> &ProxyInfo { 1386dbb5987Sopenharmony_ci match self { 1396dbb5987Sopenharmony_ci Self::All(info) => info, 1406dbb5987Sopenharmony_ci Self::Http(info) => info, 1416dbb5987Sopenharmony_ci Self::Https(info) => info, 1426dbb5987Sopenharmony_ci } 1436dbb5987Sopenharmony_ci } 1446dbb5987Sopenharmony_ci} 1456dbb5987Sopenharmony_ci 1466dbb5987Sopenharmony_ci/// ProxyInfo which contains authentication, scheme and host. 1476dbb5987Sopenharmony_ci#[derive(Clone)] 1486dbb5987Sopenharmony_cipub(crate) struct ProxyInfo { 1496dbb5987Sopenharmony_ci pub(crate) scheme: Scheme, 1506dbb5987Sopenharmony_ci pub(crate) authority: Authority, 1516dbb5987Sopenharmony_ci pub(crate) basic_auth: Option<HeaderValue>, 1526dbb5987Sopenharmony_ci} 1536dbb5987Sopenharmony_ci 1546dbb5987Sopenharmony_ciimpl ProxyInfo { 1556dbb5987Sopenharmony_ci pub(crate) fn new(uri: &str) -> Result<Self, HttpClientError> { 1566dbb5987Sopenharmony_ci let mut uri = match Uri::try_from(uri) { 1576dbb5987Sopenharmony_ci Ok(u) => u, 1586dbb5987Sopenharmony_ci Err(e) => { 1596dbb5987Sopenharmony_ci return err_from_other!(Build, e); 1606dbb5987Sopenharmony_ci } 1616dbb5987Sopenharmony_ci }; 1626dbb5987Sopenharmony_ci // Makes sure that all parts of uri exist. 1636dbb5987Sopenharmony_ci UriFormatter::new().format(&mut uri)?; 1646dbb5987Sopenharmony_ci let (scheme, authority, _, _) = uri.into_parts(); 1656dbb5987Sopenharmony_ci // `scheme` and `authority` must have values after formatting. 1666dbb5987Sopenharmony_ci Ok(Self { 1676dbb5987Sopenharmony_ci basic_auth: None, 1686dbb5987Sopenharmony_ci scheme: scheme.unwrap(), 1696dbb5987Sopenharmony_ci authority: authority.unwrap(), 1706dbb5987Sopenharmony_ci }) 1716dbb5987Sopenharmony_ci } 1726dbb5987Sopenharmony_ci 1736dbb5987Sopenharmony_ci pub(crate) fn authority(&self) -> &Authority { 1746dbb5987Sopenharmony_ci &self.authority 1756dbb5987Sopenharmony_ci } 1766dbb5987Sopenharmony_ci 1776dbb5987Sopenharmony_ci pub(crate) fn scheme(&self) -> &Scheme { 1786dbb5987Sopenharmony_ci &self.scheme 1796dbb5987Sopenharmony_ci } 1806dbb5987Sopenharmony_ci} 1816dbb5987Sopenharmony_ci 1826dbb5987Sopenharmony_ci#[derive(Clone)] 1836dbb5987Sopenharmony_cienum Ip { 1846dbb5987Sopenharmony_ci Address(IpAddr), 1856dbb5987Sopenharmony_ci} 1866dbb5987Sopenharmony_ci 1876dbb5987Sopenharmony_ci#[derive(Clone, Default)] 1886dbb5987Sopenharmony_cipub(crate) struct NoProxy { 1896dbb5987Sopenharmony_ci ips: Vec<Ip>, 1906dbb5987Sopenharmony_ci domains: Vec<String>, 1916dbb5987Sopenharmony_ci} 1926dbb5987Sopenharmony_ci 1936dbb5987Sopenharmony_ciimpl NoProxy { 1946dbb5987Sopenharmony_ci pub(crate) fn from_str(no_proxy: &str) -> Option<Self> { 1956dbb5987Sopenharmony_ci if no_proxy.is_empty() { 1966dbb5987Sopenharmony_ci return None; 1976dbb5987Sopenharmony_ci } 1986dbb5987Sopenharmony_ci 1996dbb5987Sopenharmony_ci let no_proxy_vec = no_proxy.split(',').map(|c| c.trim()).collect::<Vec<&str>>(); 2006dbb5987Sopenharmony_ci let mut ip_list = Vec::new(); 2016dbb5987Sopenharmony_ci let mut domains_list = Vec::new(); 2026dbb5987Sopenharmony_ci 2036dbb5987Sopenharmony_ci for host in no_proxy_vec { 2046dbb5987Sopenharmony_ci let address = match Uri::from_bytes(host.as_bytes()) { 2056dbb5987Sopenharmony_ci Ok(uri) => uri, 2066dbb5987Sopenharmony_ci Err(_) => { 2076dbb5987Sopenharmony_ci continue; 2086dbb5987Sopenharmony_ci } 2096dbb5987Sopenharmony_ci }; 2106dbb5987Sopenharmony_ci // use unwrap directly, host has been checked before 2116dbb5987Sopenharmony_ci match address.host().unwrap().as_str().parse::<IpAddr>() { 2126dbb5987Sopenharmony_ci Ok(ip) => ip_list.push(Ip::Address(ip)), 2136dbb5987Sopenharmony_ci Err(_) => domains_list.push(host.to_string()), 2146dbb5987Sopenharmony_ci } 2156dbb5987Sopenharmony_ci } 2166dbb5987Sopenharmony_ci Some(NoProxy { 2176dbb5987Sopenharmony_ci ips: ip_list, 2186dbb5987Sopenharmony_ci domains: domains_list, 2196dbb5987Sopenharmony_ci }) 2206dbb5987Sopenharmony_ci } 2216dbb5987Sopenharmony_ci 2226dbb5987Sopenharmony_ci pub(crate) fn contain(&self, proxy_host: &str) -> bool { 2236dbb5987Sopenharmony_ci match proxy_host.parse::<IpAddr>() { 2246dbb5987Sopenharmony_ci Ok(ip) => self.contains_ip(ip), 2256dbb5987Sopenharmony_ci Err(_) => self.contains_domain(proxy_host), 2266dbb5987Sopenharmony_ci } 2276dbb5987Sopenharmony_ci } 2286dbb5987Sopenharmony_ci 2296dbb5987Sopenharmony_ci fn contains_ip(&self, ip: IpAddr) -> bool { 2306dbb5987Sopenharmony_ci for Ip::Address(i) in self.ips.iter() { 2316dbb5987Sopenharmony_ci if &ip == i { 2326dbb5987Sopenharmony_ci return true; 2336dbb5987Sopenharmony_ci } 2346dbb5987Sopenharmony_ci } 2356dbb5987Sopenharmony_ci false 2366dbb5987Sopenharmony_ci } 2376dbb5987Sopenharmony_ci 2386dbb5987Sopenharmony_ci fn contains_domain(&self, domain: &str) -> bool { 2396dbb5987Sopenharmony_ci for block_domain in self.domains.iter() { 2406dbb5987Sopenharmony_ci let mut block_domain = block_domain.clone(); 2416dbb5987Sopenharmony_ci // Changes *.example.com to .example.com 2426dbb5987Sopenharmony_ci if (block_domain.starts_with('*')) && (block_domain.len() > 1) { 2436dbb5987Sopenharmony_ci block_domain = block_domain.trim_matches('*').to_string(); 2446dbb5987Sopenharmony_ci } 2456dbb5987Sopenharmony_ci 2466dbb5987Sopenharmony_ci if block_domain == "*" 2476dbb5987Sopenharmony_ci || block_domain.ends_with(domain) 2486dbb5987Sopenharmony_ci || block_domain == domain 2496dbb5987Sopenharmony_ci || block_domain.trim_matches('.') == domain 2506dbb5987Sopenharmony_ci { 2516dbb5987Sopenharmony_ci return true; 2526dbb5987Sopenharmony_ci } else if domain.ends_with(&block_domain) { 2536dbb5987Sopenharmony_ci // .example.com and www. 2546dbb5987Sopenharmony_ci if block_domain.starts_with('.') 2556dbb5987Sopenharmony_ci || domain.as_bytes().get(domain.len() - block_domain.len() - 1) == Some(&b'.') 2566dbb5987Sopenharmony_ci { 2576dbb5987Sopenharmony_ci return true; 2586dbb5987Sopenharmony_ci } 2596dbb5987Sopenharmony_ci } 2606dbb5987Sopenharmony_ci } 2616dbb5987Sopenharmony_ci false 2626dbb5987Sopenharmony_ci } 2636dbb5987Sopenharmony_ci} 2646dbb5987Sopenharmony_ci 2656dbb5987Sopenharmony_ci#[cfg(test)] 2666dbb5987Sopenharmony_cimod ut_proxy { 2676dbb5987Sopenharmony_ci use ylong_http::request::uri::{Scheme, Uri}; 2686dbb5987Sopenharmony_ci 2696dbb5987Sopenharmony_ci use crate::util::proxy::{Proxies, Proxy}; 2706dbb5987Sopenharmony_ci 2716dbb5987Sopenharmony_ci /// UT test cases for `Proxy::via_proxy`. 2726dbb5987Sopenharmony_ci /// 2736dbb5987Sopenharmony_ci /// # Brief 2746dbb5987Sopenharmony_ci /// 1. Creates a `Proxy`. 2756dbb5987Sopenharmony_ci /// 2. Calls `Proxy::via_proxy` with some `Uri`to get the results. 2766dbb5987Sopenharmony_ci /// 4. Checks if the test result is correct. 2776dbb5987Sopenharmony_ci #[test] 2786dbb5987Sopenharmony_ci fn ut_via_proxy() { 2796dbb5987Sopenharmony_ci let proxy = Proxy::http("http://www.example.com").unwrap(); 2806dbb5987Sopenharmony_ci let uri = Uri::from_bytes(b"http://www.example2.com").unwrap(); 2816dbb5987Sopenharmony_ci let res = proxy.via_proxy(&uri); 2826dbb5987Sopenharmony_ci assert_eq!(res.to_string(), "http://www.example.com:80"); 2836dbb5987Sopenharmony_ci } 2846dbb5987Sopenharmony_ci 2856dbb5987Sopenharmony_ci /// UT test cases for `Proxies`. 2866dbb5987Sopenharmony_ci /// 2876dbb5987Sopenharmony_ci /// # Brief 2886dbb5987Sopenharmony_ci /// 1. Creates a `Proxies`. 2896dbb5987Sopenharmony_ci /// 2. Adds some `Proxy` to `Proxies` 2906dbb5987Sopenharmony_ci /// 3. Calls `Proxies::match_proxy` with some `Uri`s and get the results. 2916dbb5987Sopenharmony_ci /// 4. Checks if the test result is correct. 2926dbb5987Sopenharmony_ci #[test] 2936dbb5987Sopenharmony_ci fn ut_proxies() { 2946dbb5987Sopenharmony_ci let mut proxies = Proxies::default(); 2956dbb5987Sopenharmony_ci proxies.add_proxy(Proxy::http("http://www.aaa.com").unwrap()); 2966dbb5987Sopenharmony_ci proxies.add_proxy(Proxy::https("http://www.bbb.com").unwrap()); 2976dbb5987Sopenharmony_ci 2986dbb5987Sopenharmony_ci let uri = Uri::from_bytes(b"http://www.example.com").unwrap(); 2996dbb5987Sopenharmony_ci let proxy = proxies.match_proxy(&uri).unwrap(); 3006dbb5987Sopenharmony_ci assert!(proxy.no_proxy.is_none()); 3016dbb5987Sopenharmony_ci let info = proxy.intercept.proxy_info(); 3026dbb5987Sopenharmony_ci assert_eq!(info.scheme, Scheme::HTTP); 3036dbb5987Sopenharmony_ci assert_eq!(info.authority.to_string(), "www.aaa.com:80"); 3046dbb5987Sopenharmony_ci 3056dbb5987Sopenharmony_ci let uri = Uri::from_bytes(b"https://www.example.com").unwrap(); 3066dbb5987Sopenharmony_ci let matched = proxies.match_proxy(&uri).unwrap(); 3076dbb5987Sopenharmony_ci assert!(matched.no_proxy.is_none()); 3086dbb5987Sopenharmony_ci let info = matched.intercept.proxy_info(); 3096dbb5987Sopenharmony_ci assert_eq!(info.scheme, Scheme::HTTP); 3106dbb5987Sopenharmony_ci assert_eq!(info.authority.to_string(), "www.bbb.com:80"); 3116dbb5987Sopenharmony_ci 3126dbb5987Sopenharmony_ci // with no_proxy 3136dbb5987Sopenharmony_ci let mut proxies = Proxies::default(); 3146dbb5987Sopenharmony_ci let mut proxy = Proxy::http("http://www.aaa.com").unwrap(); 3156dbb5987Sopenharmony_ci proxy.no_proxy("http://no_proxy.aaa.com"); 3166dbb5987Sopenharmony_ci proxies.add_proxy(proxy); 3176dbb5987Sopenharmony_ci 3186dbb5987Sopenharmony_ci let uri = Uri::from_bytes(b"http://www.bbb.com").unwrap(); 3196dbb5987Sopenharmony_ci let matched = proxies.match_proxy(&uri).unwrap(); 3206dbb5987Sopenharmony_ci let info = matched.intercept.proxy_info(); 3216dbb5987Sopenharmony_ci assert_eq!(info.scheme, Scheme::HTTP); 3226dbb5987Sopenharmony_ci assert_eq!(info.authority.to_string(), "www.aaa.com:80"); 3236dbb5987Sopenharmony_ci 3246dbb5987Sopenharmony_ci let uri = Uri::from_bytes(b"http://no_proxy.aaa.com").unwrap(); 3256dbb5987Sopenharmony_ci assert!(proxies.match_proxy(&uri).is_none()); 3266dbb5987Sopenharmony_ci 3276dbb5987Sopenharmony_ci let mut proxies = Proxies::default(); 3286dbb5987Sopenharmony_ci let mut proxy = Proxy::http("http://www.aaa.com").unwrap(); 3296dbb5987Sopenharmony_ci proxy.no_proxy(".aaa.com"); 3306dbb5987Sopenharmony_ci proxies.add_proxy(proxy); 3316dbb5987Sopenharmony_ci 3326dbb5987Sopenharmony_ci let uri = Uri::from_bytes(b"http://no_proxy.aaa.com").unwrap(); 3336dbb5987Sopenharmony_ci assert!(proxies.match_proxy(&uri).is_none()); 3346dbb5987Sopenharmony_ci 3356dbb5987Sopenharmony_ci let mut proxies = Proxies::default(); 3366dbb5987Sopenharmony_ci let mut proxy = Proxy::http("http://127.0.0.1:3000").unwrap(); 3376dbb5987Sopenharmony_ci proxy.no_proxy("http://127.0.0.1:80"); 3386dbb5987Sopenharmony_ci proxies.add_proxy(proxy); 3396dbb5987Sopenharmony_ci 3406dbb5987Sopenharmony_ci let uri = Uri::from_bytes(b"http://127.0.0.1:80").unwrap(); 3416dbb5987Sopenharmony_ci assert!(proxies.match_proxy(&uri).is_none()); 3426dbb5987Sopenharmony_ci } 3436dbb5987Sopenharmony_ci} 344