io_webdav/rfc4918/follow_redirects.rs
1//! Send coroutine that surfaces 3xx redirects to the caller.
2//!
3//! Runs an HTTP/1.1 exchange and turns the underlying
4//! `HttpSendYield::WantsRedirect` into a
5//! [`WebdavRedirectYield::WantsRedirect`] so the client can rebuild its
6//! connection and restart the operation against the new target URL. The
7//! success body is returned raw; callers parse it with
8//! `parse_multistatus`.
9//!
10//! [`WebdavRedirectYield::WantsRedirect`]: crate::rfc4918::coroutine::WebdavRedirectYield::WantsRedirect
11//!
12//! # Example
13//!
14//! ```rust,no_run
15//! use std::{
16//! io::{Read, Write},
17//! net::TcpStream,
18//! };
19//!
20//! use io_webdav::{
21//! coroutine::{WebdavCoroutine, WebdavCoroutineState},
22//! rfc4918::{
23//! WebdavAuth,
24//! coroutine::WebdavRedirectYield,
25//! follow_redirects::FollowRedirects,
26//! request::WebdavRequest,
27//! },
28//! };
29//! use url::Url;
30//!
31//! // Ready stream needed (TCP-connected, TLS-negociated)
32//! let mut stream = TcpStream::connect("dav.example.org:443").unwrap();
33//! let mut buf = [0u8; 4096];
34//!
35//! let base_url: Url = "https://dav.example.org/".parse().unwrap();
36//! let auth = WebdavAuth::None;
37//! let request = WebdavRequest::propfind(&base_url, &auth, "io-webdav", "/")
38//! .content_type_xml()
39//! .body(Vec::new());
40//! let mut coroutine = FollowRedirects::new(request);
41//! let mut arg = None;
42//!
43//! let ok = loop {
44//! match coroutine.resume(arg.take()) {
45//! WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsWrite(bytes)) => {
46//! stream.write_all(&bytes).unwrap();
47//! }
48//! WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsRead) => {
49//! let n = stream.read(&mut buf).unwrap();
50//! arg = Some(&buf[..n]);
51//! }
52//! WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsRedirect { url, .. }) => {
53//! todo!("reconnect to {url}");
54//! }
55//! WebdavCoroutineState::Complete(Ok(ok)) => break ok,
56//! WebdavCoroutineState::Complete(Err(err)) => panic!("{err}"),
57//! }
58//! };
59//!
60//! println!("{} bytes", ok.body.len());
61//! ```
62
63use alloc::{string::String, vec::Vec};
64
65use io_http::{
66 coroutine::*,
67 rfc9110::{request::HttpRequest, send::HttpSendOutput},
68 rfc9112::send::{Http11Send, Http11SendError},
69};
70use log::trace;
71use thiserror::Error;
72
73use crate::{
74 coroutine::*,
75 rfc4918::{coroutine::WebdavRedirectYield, send::SendOk},
76};
77
78/// Failure causes during a redirect-aware WebDAV send.
79#[derive(Debug, Error)]
80pub enum FollowRedirectsError {
81 /// The server returned a non-2xx, non-redirect HTTP status.
82 #[error("WebDAV server returned HTTP {0}: {1}")]
83 HttpStatus(u16, String),
84
85 /// The underlying HTTP/1.1 send failed.
86 #[error(transparent)]
87 Send(#[from] Http11SendError),
88}
89
90/// I/O-free coroutine that sends a WebDAV request, surfaces 3xx
91/// redirects via [`WebdavRedirectYield::WantsRedirect`] and returns the
92/// success body as raw bytes.
93#[derive(Debug)]
94pub struct FollowRedirects {
95 state: State,
96}
97
98impl FollowRedirects {
99 /// Builds a new redirect-aware send coroutine. `request` must
100 /// already carry its body bytes.
101 pub fn new(request: HttpRequest) -> Self {
102 Self {
103 state: State::Send(Http11Send::new(request)),
104 }
105 }
106}
107
108impl WebdavCoroutine for FollowRedirects {
109 type Yield = WebdavRedirectYield;
110 type Return = Result<SendOk<Vec<u8>>, FollowRedirectsError>;
111
112 fn resume(&mut self, arg: Option<&[u8]>) -> WebdavCoroutineState<Self::Yield, Self::Return> {
113 trace!("sending request");
114 match &mut self.state {
115 State::Send(send) => {
116 let out = match send.resume(arg) {
117 HttpCoroutineState::Yielded(y) => {
118 return WebdavCoroutineState::Yielded(y.into());
119 }
120 HttpCoroutineState::Complete(Err(err)) => {
121 return WebdavCoroutineState::Complete(Err(err.into()));
122 }
123 HttpCoroutineState::Complete(Ok(out)) => out,
124 };
125
126 let HttpSendOutput {
127 response,
128 keep_alive,
129 ..
130 } = out;
131
132 if !response.status.is_success() {
133 let body = String::from_utf8_lossy(&response.body).into_owned();
134 let err = FollowRedirectsError::HttpStatus(*response.status, body);
135 return WebdavCoroutineState::Complete(Err(err));
136 }
137
138 let body = response.body.clone();
139 WebdavCoroutineState::Complete(Ok(SendOk {
140 response,
141 keep_alive,
142 body,
143 }))
144 }
145 }
146 }
147}
148
149#[derive(Debug)]
150enum State {
151 Send(Http11Send),
152}