///|
/// Digest credentials scoped to one HTTP(S) origin, wrapping a Backend.
/// Put this inside FollowRedirects so credentials are checked on every hop.
pub struct DigestScheme {
priv inner : &@http.Backend
priv origin : String
priv username : String
priv password : String
priv random : () -> Double
priv state : DigestState
priv mut closed : Bool
}
///|
/// Creates an origin-scoped wrapper. The origin must be an absolute HTTP(S)
/// URL without userinfo. Paths are ignored. Supply an unpredictable random
/// source returning values in [0,1) for cnonces; tests can inject fixed values.
/// No network I/O occurs here. Explicit request Authorization takes precedence.
pub fn DigestScheme::new(
inner : &@http.Backend,
username : String,
password : String,
origin~ : String,
random~ : () -> Double,
) -> DigestScheme raise @http.TransportError {
@http.Request::get(origin).validate()
guard digest_target(origin) is Some((url, origin, _)) &&
(url.scheme == Some("http") || url.scheme == Some("https")) &&
url.userinfo is None else {
raise @http.TransportError(
kind=Invalid,
phase=Pool,
detail="Digest requires an HTTP(S) credential origin without userinfo",
)
}
{
inner,
origin,
username,
password,
random,
state: DigestState::new(),
closed: false,
}
}
///|
/// Sends one exchange, answering at most one challenge and one stale nonce.
/// Intermediate response bodies close before resending. Stream bodies are
/// never replayed. Requests outside the configured origin pass through.
/// SendOptions pass through unchanged to every send. A Client with a Timer
/// can bound the whole challenge exchange. After a 401 was answered, a
/// transport failure has phase Unknown; Connect and Proxy become Network,
/// and the detail retains the original failure, so it cannot appear unsent.
pub async fn DigestScheme::send(
self : DigestScheme,
request : @http.Request,
options : @http.SendOptions,
) -> @http.Response raise @http.TransportError {
defer (if request.body is Stream(stream) { stream.close() })
if self.closed {
raise @http.TransportError(
kind=Closed,
phase=Pool,
detail="Digest backend is closed",
)
}
request.validate()
options.validate()
guard !request.headers.contains("authorization") &&
digest_target(request.url) is Some((url, origin, uri)) &&
origin == self.origin else {
return self.inner.send(request, options)
}
let body = request.body.as_bytes()
let uri = if request.meth == @http.Method::Connect {
// H1 uses authority-form for CONNECT, including an explicit port.
let authority = origin.split("://").last().unwrap().to_owned()
authority
} else {
uri
}
let authorize = (challenge : DigestChallenge, count : Int64) => {
let value = digest_authorization(
challenge,
self.username,
self.password,
request.meth.to_string(),
uri,
count,
make_cnonce(self.random),
body,
)
request.header("authorization", value)
}
let first = match self.state.next(origin, body is Some(_)) {
Some((challenge, count)) => authorize(challenge, count)
None => request
}
let mut response = self.inner.send(first, options)
for answers = 0 {
guard response.status == 401 &&
body is Some(_) &&
same_origin_as(
url,
response.attributes.get(@client.final_url).unwrap_or(request.url),
) &&
find_digest_challenge(response.headers, body is Some(_))
is Some(challenge) &&
should_answer(answers, challenge.stale) else {
return response
}
guard self.state.adopt(origin, challenge) is Some(count) else {
return response
}
response.close()
response = self.inner.send(authorize(challenge, count), options) catch {
error => raise after_challenge(error)
}
continue answers + 1
}
}
///|
// A 401 proves the original request reached the peer. A later dial failure
// cannot license an unsent-only retry of that original request.
fn after_challenge(error : @http.TransportError) -> @http.TransportError {
let @http.TransportError(kind~, ..) = error
let kind : @http.Kind = match kind {
Connect | Proxy(_) => Network
kind => kind
}
@http.TransportError(
kind~,
phase=Unknown,
detail="Digest send failed after a challenge was answered: \{error.message()}",
)
}
///|
/// Closes the inner backend once and forgets retained nonces.
pub fn DigestScheme::close(self : DigestScheme) -> Unit {
if !self.closed {
self.closed = true
self.state.nonces.clear()
self.inner.close()
}
}
///|
/// Implements the standard single-exchange Backend interface.
pub impl @http.Backend for DigestScheme with fn send(self, request, options) {
self.send(request, options)
}
///|
/// Closes this wrapper through Backend.
pub impl @http.Backend for DigestScheme with fn close(self) {
self.close()
}