1use std::net::IpAddr;
3use std::time::Duration;
4use ureq::http::Uri;
5use ureq::unversioned::resolver::{DefaultResolver, ResolvedSocketAddrs, Resolver};
6use ureq::unversioned::transport::{DefaultConnector, NextTimeout};
7
8fn public_ip(ip: IpAddr) -> bool {
9 match ip {
10 IpAddr::V4(ip) => {
11 let [a, b, c, d] = ip.octets();
12 !ip.is_private()
13 && !ip.is_loopback()
14 && !ip.is_link_local()
15 && !ip.is_documentation()
16 && !ip.is_broadcast()
17 && a != 0
18 && a < 224
19 && !(a == 100 && (64..=127).contains(&b))
20 && !(a == 198 && (18..=19).contains(&b))
21 && !(a == 192 && b == 0 && c == 0 && !matches!(d, 9 | 10))
23 && !(a == 192 && b == 88 && c == 99)
24 }
25 IpAddr::V6(ip) => {
26 if let Some(ip) = ip.to_ipv4_mapped() {
27 return public_ip(IpAddr::V4(ip));
28 }
29 let s = ip.segments();
30 (0x2000..=0x3fff).contains(&s[0])
31 && s[0] != 0x2002
32 && !(s[0] == 0x2001 && matches!(s[1], 0 | 2 | 0xdb8))
33 }
34 }
35}
36pub fn feed_address(value: &str) -> Result<Uri, String> {
38 if value.len() > 2048 || value.chars().any(char::is_control) || value.contains('#') {
39 return Err("Adresse RSS invalide.".into());
40 }
41 let uri: Uri = value.trim().parse().map_err(|_| "Adresse RSS invalide.")?;
42 let authority = uri.authority().ok_or("Adresse RSS invalide.")?;
43 let host = uri
44 .host()
45 .ok_or("Adresse RSS invalide.")?
46 .trim_matches(['[', ']']);
47 if !matches!(uri.scheme_str(), Some("http" | "https"))
48 || authority.as_str().contains('@')
49 || !matches!(uri.port_u16(), None | Some(80 | 443))
50 || host.eq_ignore_ascii_case("localhost")
51 || host.ends_with(".local")
52 || host.parse::<IpAddr>().is_ok_and(|ip| !public_ip(ip))
53 {
54 return Err("Utilisez un flux RSS HTTP/HTTPS public sans identifiants.".into());
55 }
56 Ok(uri)
57}
58#[derive(Debug)]
59pub(crate) struct PublicResolver;
60impl Resolver for PublicResolver {
61 fn resolve(
62 &self,
63 uri: &Uri,
64 config: &ureq::config::Config,
65 timeout: NextTimeout,
66 ) -> Result<ResolvedSocketAddrs, ureq::Error> {
67 feed_address(&uri.to_string()).map_err(|_| ureq::Error::HostNotFound)?;
68 let addresses = DefaultResolver::default().resolve(uri, config, timeout)?;
69 if addresses.iter().any(|address| !public_ip(address.ip())) {
70 return Err(ureq::Error::HostNotFound);
71 }
72 Ok(addresses)
73 }
74}
75pub struct FeedClient {
77 agent: ureq::Agent,
78}
79impl Default for FeedClient {
80 fn default() -> Self {
81 let config = ureq::Agent::config_builder()
82 .timeout_global(Some(Duration::from_secs(15)))
83 .max_redirects(3)
84 .proxy(None)
85 .build();
86 Self {
87 agent: ureq::Agent::with_parts(config, DefaultConnector::default(), PublicResolver),
88 }
89 }
90}
91impl FeedClient {
92 pub fn read(&self, url: &str) -> Result<String, String> {
94 let uri = feed_address(url)?;
95 let mut response = self
96 .agent
97 .get(uri.to_string())
98 .header(
99 "User-Agent",
100 concat!(
101 "Phemios/",
102 env!("CARGO_PKG_VERSION"),
103 " (Podcast RSS client)"
104 ),
105 )
106 .header(
107 "Accept",
108 "application/rss+xml, application/atom+xml, application/xml, text/xml",
109 )
110 .call()
111 .map_err(|_| "Flux RSS indisponible ou adresse non publique.")?;
112 let bytes = response
113 .body_mut()
114 .with_config()
115 .limit(8 * 1024 * 1024)
116 .read_to_vec()
117 .map_err(|_| "Flux RSS trop grand ou interrompu.")?;
118 String::from_utf8(bytes).map_err(|_| "Le flux RSS doit utiliser UTF-8.".into())
119 }
120}
121#[cfg(test)]
122#[path = "podcast_tests.rs"]
123mod tests;