Skip to content

Commit 865a532

Browse files
Reuse digest auth in multiple requests
1 parent b08709b commit 865a532

File tree

3 files changed

+60
-18
lines changed

3 files changed

+60
-18
lines changed

onvif/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ futures = "0.3"
1919
futures-core = "0.3"
2020
futures-util = "0.3"
2121
num-bigint = "0.4"
22+
nonzero_ext = "0.3"
2223
reqwest = { version = "0.12", default-features = false }
2324
schema = { version = "0.1.0", path = "../schema", default-features = false, features = ["analytics", "devicemgmt", "event", "media", "ptz"] }
2425
sha1 = "0.6"

onvif/src/soap/auth/digest.rs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::soap::client::Credentials;
2+
use nonzero_ext::nonzero;
23
use reqwest::{RequestBuilder, Response};
34
use std::fmt::{Debug, Formatter};
5+
use std::num::NonZeroU8;
46
use thiserror::Error;
57
use url::Url;
68

@@ -22,8 +24,10 @@ pub struct Digest {
2224

2325
enum State {
2426
Default,
25-
Got401(reqwest::Response),
26-
Got401Twice,
27+
Got401 {
28+
response: Response,
29+
count: NonZeroU8,
30+
},
2731
}
2832

2933
impl Digest {
@@ -37,29 +41,55 @@ impl Digest {
3741
}
3842

3943
impl Digest {
44+
/// Call this when the authentication was successful.
45+
pub fn set_success(&mut self) {
46+
if let State::Got401 { count, .. } = &mut self.state {
47+
// We always store at least one request, so it's never zero.
48+
*count = nonzero!(1_u8);
49+
}
50+
}
51+
52+
/// Call this when received 401 Unauthorized.
4053
pub fn set_401(&mut self, response: Response) {
41-
match self.state {
42-
State::Default => self.state = State::Got401(response),
43-
State::Got401(_) => self.state = State::Got401Twice,
44-
State::Got401Twice => {}
54+
self.state = match self.state {
55+
State::Default => State::Got401 {
56+
response,
57+
count: nonzero!(1_u8),
58+
},
59+
State::Got401 { count, .. } => State::Got401 {
60+
response,
61+
count: count.saturating_add(1),
62+
},
4563
}
4664
}
4765

4866
pub fn is_failed(&self) -> bool {
49-
matches!(self.state, State::Got401Twice)
67+
match &self.state {
68+
State::Default => false,
69+
// Possible scenarios:
70+
// - We've got 401 with a challenge for the first time, we calculate the answer, then
71+
// we get 200 OK. So, a single 401 is never a failure.
72+
// - After successful auth the count is 1 because we always store at least one request,
73+
// and the caller decided to reuse the same challenge for multiple requests. But at
74+
// some point, we'll get a 401 with a new challenge and `stale=true`.
75+
// So, we'll get a second 401, and this is also not a failure because after
76+
// calculating the answer to the challenge, we'll get a 200 OK, and will reset the
77+
// counter in `set_success()`.
78+
// - Three 401's in a row is certainly a failure.
79+
State::Got401 { count, .. } => count.get() >= 3,
80+
}
5081
}
5182

5283
pub fn add_headers(&self, mut request: RequestBuilder) -> Result<RequestBuilder, Error> {
5384
match &self.state {
5485
State::Default => Ok(request),
55-
State::Got401(response) => {
86+
State::Got401 { response, .. } => {
5687
let creds = self.creds.as_ref().ok_or(Error::NoCredentials)?;
5788

5889
request = request.header("Authorization", digest_auth(response, creds, &self.uri)?);
5990

6091
Ok(request)
6192
}
62-
State::Got401Twice => Err(Error::InvalidState),
6393
}
6494
}
6595
}
@@ -94,10 +124,11 @@ impl Debug for Digest {
94124

95125
impl Debug for State {
96126
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
97-
f.write_str(match self {
98-
State::Default => "FirstRequest",
99-
State::Got401(_) => "Got401",
100-
State::Got401Twice => "Got401Twice",
101-
})
127+
match self {
128+
State::Default => write!(f, "FirstRequest")?,
129+
State::Got401 { count, .. } => write!(f, "Got401({count})")?,
130+
};
131+
132+
Ok(())
102133
}
103134
}

onvif/src/soap/client.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use crate::soap::{
66
};
77
use async_recursion::async_recursion;
88
use async_trait::async_trait;
9+
use futures_util::lock::Mutex;
910
use schema::transport::{Error, Transport};
11+
use std::ops::DerefMut;
1012
use std::{
1113
fmt::{Debug, Formatter},
1214
sync::Arc,
@@ -19,6 +21,7 @@ use url::Url;
1921
pub struct Client {
2022
client: reqwest::Client,
2123
config: Config,
24+
digest_auth_state: Arc<Mutex<Digest>>,
2225
}
2326

2427
#[derive(Clone)]
@@ -84,9 +87,12 @@ impl ClientBuilder {
8487
.unwrap()
8588
};
8689

90+
let digest = Digest::new(&self.config.uri, &self.config.credentials);
91+
8792
Client {
8893
client,
8994
config: self.config,
95+
digest_auth_state: Arc::new(Mutex::new(digest)),
9096
}
9197
}
9298

@@ -144,8 +150,8 @@ impl Debug for Credentials {
144150
pub type ResponsePatcher = Arc<dyn Fn(&str) -> Result<String, String> + Send + Sync>;
145151

146152
#[derive(Debug)]
147-
enum RequestAuthType {
148-
Digest(Digest),
153+
enum RequestAuthType<'a> {
154+
Digest(&'a mut Digest),
149155
UsernameToken,
150156
}
151157

@@ -172,8 +178,8 @@ impl Transport for Client {
172178

173179
impl Client {
174180
async fn request_with_digest(&self, message: &str) -> Result<String, Error> {
175-
let mut auth_type =
176-
RequestAuthType::Digest(Digest::new(&self.config.uri, &self.config.credentials));
181+
let mut guard = self.digest_auth_state.lock().await;
182+
let mut auth_type = RequestAuthType::Digest(guard.deref_mut());
177183

178184
self.request_recursive(message, &self.config.uri, &mut auth_type, 0)
179185
.await
@@ -232,6 +238,10 @@ impl Client {
232238
debug!("Response status: {status}");
233239

234240
if status.is_success() {
241+
if let RequestAuthType::Digest(digest) = auth_type {
242+
digest.set_success();
243+
}
244+
235245
response
236246
.text()
237247
.await

0 commit comments

Comments
 (0)