| 31 |
31 |
|
pub(crate) fn probe(host: &str, port: u16, timeout: Duration) -> Reachability {
|
| 32 |
32 |
|
let url = format!("http://{host}:{port}/");
|
| 33 |
33 |
|
match agent(timeout).get(&url).call() {
|
| 34 |
|
- |
Ok(resp) if resp.status() >= 200 && resp.status() < 300 => Reachability::Up,
|
| 35 |
|
- |
Ok(_) => Reachability::WifiTransferOff,
|
| 36 |
|
- |
Err(ureq::Error::Status(401 | 403, _)) => Reachability::Up,
|
| 37 |
|
- |
Err(ureq::Error::Status(_, _)) => Reachability::WifiTransferOff,
|
| 38 |
|
- |
Err(ureq::Error::Transport(_)) => Reachability::Unreachable,
|
|
34 |
+ |
// The agent reports 4xx/5xx as ordinary responses, so every status
|
|
35 |
+ |
// lands in this arm rather than being split across Ok and Err.
|
|
36 |
+ |
Ok(resp) => classify_probe_status(resp.status().as_u16()),
|
|
37 |
+ |
Err(_) => Reachability::Unreachable,
|
|
38 |
+ |
}
|
|
39 |
+ |
}
|
|
40 |
+ |
|
|
41 |
+ |
/// A passcode-protected device still answers, it just answers 401/403, so an
|
|
42 |
+ |
/// auth challenge counts as up.
|
|
43 |
+ |
fn classify_probe_status(status: u16) -> Reachability {
|
|
44 |
+ |
match status {
|
|
45 |
+ |
200..=299 | 401 | 403 => Reachability::Up,
|
|
46 |
+ |
_ => Reachability::WifiTransferOff,
|
| 39 |
47 |
|
}
|
| 40 |
48 |
|
}
|
| 41 |
49 |
|
|
| 53 |
61 |
|
|
| 54 |
62 |
|
let mut req = agent(timeout)
|
| 55 |
63 |
|
.post(&url)
|
| 56 |
|
- |
.set("Content-Type", &content_type)
|
| 57 |
|
- |
.set("file-size", &bytes.len().to_string());
|
|
64 |
+ |
.header("Content-Type", &content_type)
|
|
65 |
+ |
.header("file-size", bytes.len().to_string());
|
| 58 |
66 |
|
if let Some(pc) = passcode {
|
| 59 |
|
- |
req = req.set("Authorization", &basic(pc));
|
|
67 |
+ |
req = req.header("Authorization", basic(pc));
|
|
68 |
+ |
}
|
|
69 |
+ |
let mut resp = req.send(&body[..]).map_err(Error::from_http)?;
|
|
70 |
+ |
if let Some(e) = status_error(&mut resp) {
|
|
71 |
+ |
return Err(e);
|
| 60 |
72 |
|
}
|
| 61 |
|
- |
req.send_bytes(&body).map_err(Error::from_http)?;
|
| 62 |
73 |
|
Ok(())
|
| 63 |
74 |
|
}
|
| 64 |
75 |
|
|
| 72 |
83 |
|
let url = build_url(host, port, dir, None);
|
| 73 |
84 |
|
let mut req = agent(timeout).get(&url);
|
| 74 |
85 |
|
if let Some(pc) = passcode {
|
| 75 |
|
- |
req = req.set("Authorization", &basic(pc));
|
|
86 |
+ |
req = req.header("Authorization", basic(pc));
|
|
87 |
+ |
}
|
|
88 |
+ |
let mut resp = req.call().map_err(Error::from_http)?;
|
|
89 |
+ |
if let Some(e) = status_error(&mut resp) {
|
|
90 |
+ |
return Err(e);
|
| 76 |
91 |
|
}
|
| 77 |
|
- |
let html = req.call().map_err(Error::from_http)?.into_string()?;
|
|
92 |
+ |
let html = resp.body_mut().read_to_string().map_err(Error::from_http)?;
|
| 78 |
93 |
|
let json = extract_embedded_json(&html).ok_or_else(|| {
|
| 79 |
94 |
|
Error::Http("could not find embedded webInfo JSON in listing page".to_string())
|
| 80 |
95 |
|
})?;
|
| 88 |
103 |
|
}
|
| 89 |
104 |
|
|
| 90 |
105 |
|
fn agent(timeout: Duration) -> ureq::Agent {
|
| 91 |
|
- |
ureq::AgentBuilder::new().timeout(timeout).build()
|
|
106 |
+ |
ureq::Agent::new_with_config(
|
|
107 |
+ |
ureq::Agent::config_builder()
|
|
108 |
+ |
.timeout_global(Some(timeout))
|
|
109 |
+ |
// ureq 3 would otherwise fail a 4xx/5xx with Error::StatusCode,
|
|
110 |
+ |
// which carries the code but not the response. The device puts a
|
|
111 |
+ |
// useful message in the body, so take every status as a normal
|
|
112 |
+ |
// response and inspect it in `status_error`.
|
|
113 |
+ |
.http_status_as_error(false)
|
|
114 |
+ |
.build(),
|
|
115 |
+ |
)
|
|
116 |
+ |
}
|
|
117 |
+ |
|
|
118 |
+ |
/// Returns the error for a non-2xx response, draining the body into it.
|
|
119 |
+ |
fn status_error(resp: &mut ureq::http::Response<ureq::Body>) -> Option<Error> {
|
|
120 |
+ |
let status = resp.status().as_u16();
|
|
121 |
+ |
if (200..300).contains(&status) {
|
|
122 |
+ |
return None;
|
|
123 |
+ |
}
|
|
124 |
+ |
Some(Error::Status {
|
|
125 |
+ |
status,
|
|
126 |
+ |
body: resp.body_mut().read_to_string().unwrap_or_default(),
|
|
127 |
+ |
})
|
| 92 |
128 |
|
}
|
| 93 |
129 |
|
|
| 94 |
130 |
|
fn build_url(host: &str, port: u16, dir: &str, filename: Option<&str>) -> String {
|
| 269 |
305 |
|
}
|
| 270 |
306 |
|
|
| 271 |
307 |
|
#[test]
|
|
308 |
+ |
fn probe_treats_auth_challenges_as_reachable() {
|
|
309 |
+ |
assert_eq!(classify_probe_status(200), Reachability::Up);
|
|
310 |
+ |
assert_eq!(classify_probe_status(204), Reachability::Up);
|
|
311 |
+ |
assert_eq!(classify_probe_status(401), Reachability::Up);
|
|
312 |
+ |
assert_eq!(classify_probe_status(403), Reachability::Up);
|
|
313 |
+ |
// Anything else answering on the port is a device with Wi-Fi Transfer off.
|
|
314 |
+ |
assert_eq!(classify_probe_status(404), Reachability::WifiTransferOff);
|
|
315 |
+ |
assert_eq!(classify_probe_status(500), Reachability::WifiTransferOff);
|
|
316 |
+ |
}
|
|
317 |
+ |
|
|
318 |
+ |
#[test]
|
| 272 |
319 |
|
fn basic_header_is_colon_prefixed() {
|
| 273 |
320 |
|
assert_eq!(basic("secret"), "Basic OnNlY3JldA==");
|
| 274 |
321 |
|
}
|