1use std::collections::BTreeMap;
4use std::time::Duration;
5
6use tokio::net::UdpSocket;
7use url::Url;
8
9use super::packet::{Packet, encode_ack, parse_packet};
10use super::{GUPPY_PORT, GuppyResponse, MAX_REQUEST_BYTES};
11
12#[derive(Debug, Clone)]
14pub struct FetchOptions {
15 pub timeout: Duration,
17 pub retransmit_after: Duration,
20 pub max_body: usize,
23 pub connect_addr: Option<std::net::SocketAddr>,
26}
27
28impl Default for FetchOptions {
29 fn default() -> Self {
30 Self {
31 timeout: Duration::from_secs(30),
32 retransmit_after: Duration::from_secs(1),
33 max_body: 16 * 1024 * 1024,
34 connect_addr: None,
35 }
36 }
37}
38
39#[derive(Debug)]
41pub enum ClientError {
42 BadUrl(String),
43 RequestTooLong { request_bytes: usize, max: usize },
44 Io(String),
45 Timeout,
49 Protocol(String),
50 BodyTooLarge { max: usize },
51}
52
53impl std::fmt::Display for ClientError {
54 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::BadUrl(message) => write!(formatter, "bad guppy URL: {message}"),
57 Self::RequestTooLong { request_bytes, max } => write!(
58 formatter,
59 "guppy request is {request_bytes} bytes (max {max})"
60 ),
61 Self::Io(message) => write!(formatter, "guppy IO error: {message}"),
62 Self::Timeout => write!(formatter, "guppy transaction timed out"),
63 Self::Protocol(message) => write!(formatter, "guppy protocol error: {message}"),
64 Self::BodyTooLarge { max } => {
65 write!(formatter, "guppy response exceeds {max} bytes")
66 }
67 }
68 }
69}
70
71impl std::error::Error for ClientError {}
72
73pub async fn fetch(url: &str, options: &FetchOptions) -> Result<GuppyResponse, ClientError> {
77 let parsed = Url::parse(url).map_err(|error| ClientError::BadUrl(error.to_string()))?;
78 if parsed.scheme() != "guppy" {
79 return Err(ClientError::BadUrl(format!(
80 "expected guppy:// scheme, got {}://",
81 parsed.scheme()
82 )));
83 }
84 let host = parsed
85 .host_str()
86 .ok_or_else(|| ClientError::BadUrl("URL has no host".to_string()))?;
87 let port = parsed.port().unwrap_or(GUPPY_PORT);
88
89 let request = format!("{url}\r\n");
90 if request.len() > MAX_REQUEST_BYTES {
91 return Err(ClientError::RequestTooLong {
92 request_bytes: request.len(),
93 max: MAX_REQUEST_BYTES,
94 });
95 }
96
97 let socket = UdpSocket::bind("0.0.0.0:0")
100 .await
101 .map_err(|error| ClientError::Io(format!("bind: {error}")))?;
102 match options.connect_addr {
103 Some(addr) => socket.connect(addr).await,
104 None => socket.connect((host, port)).await,
105 }
106 .map_err(|error| ClientError::Io(format!("connect: {error}")))?;
107
108 tokio::time::timeout(options.timeout, transact(&socket, &request, options))
109 .await
110 .map_err(|_| ClientError::Timeout)?
111}
112
113struct Reassembly {
115 first_seq: u32,
116 mime: String,
117 chunks: BTreeMap<u32, Vec<u8>>,
119 contiguous_end: u32,
122 buffered: usize,
124 eof_seq: Option<u32>,
126}
127
128impl Reassembly {
129 fn insert(&mut self, seq: u32, data: Vec<u8>) {
130 self.buffered += data.len();
131 self.chunks.entry(seq).or_insert(data);
132 while self.chunks.contains_key(&(self.contiguous_end + 1)) {
133 self.contiguous_end += 1;
134 }
135 }
136
137 fn complete(&self) -> bool {
138 self.eof_seq == Some(self.contiguous_end + 1)
139 }
140
141 fn into_response(self) -> GuppyResponse {
142 let mut body = Vec::with_capacity(self.buffered);
143 for (_, chunk) in self.chunks {
144 body.extend_from_slice(&chunk);
145 }
146 GuppyResponse::Success {
147 mime: self.mime,
148 body,
149 }
150 }
151}
152
153async fn transact(
154 socket: &UdpSocket,
155 request: &str,
156 options: &FetchOptions,
157) -> Result<GuppyResponse, ClientError> {
158 let send = |bytes: Vec<u8>| async move {
159 socket
160 .send(&bytes)
161 .await
162 .map_err(|error| ClientError::Io(format!("send: {error}")))
163 };
164
165 send(request.as_bytes().to_vec()).await?;
166
167 let mut state: Option<Reassembly> = None;
168 let mut buffer = vec![0u8; 65_536];
169
170 loop {
171 if let Some(reassembly) = &state {
172 if reassembly.complete() {
173 return Ok(state.take().expect("checked").into_response());
174 }
175 if reassembly.buffered > options.max_body {
176 return Err(ClientError::BodyTooLarge {
177 max: options.max_body,
178 });
179 }
180 }
181
182 let received =
183 tokio::time::timeout(options.retransmit_after, socket.recv(&mut buffer)).await;
184 let count = match received {
185 Ok(Ok(count)) => count,
186 Ok(Err(error)) => return Err(ClientError::Io(format!("recv: {error}"))),
187 Err(_) => {
188 match &state {
192 None => {
193 send(request.as_bytes().to_vec()).await?;
194 }
195 Some(reassembly) => {
196 send(encode_ack(reassembly.contiguous_end)).await?;
197 if let Some(eof) = reassembly.eof_seq {
198 send(encode_ack(eof)).await?;
199 }
200 }
201 }
202 continue;
203 }
204 };
205
206 let packet = match parse_packet(&buffer[..count]) {
207 Ok(packet) => packet,
208 Err(error) => {
209 log::debug!("guppy: ignoring malformed packet: {error}");
210 continue;
211 }
212 };
213
214 match packet {
215 Packet::Prompt { text } if state.is_none() => {
218 return Ok(GuppyResponse::Prompt { text });
219 }
220 Packet::Redirect { target } if state.is_none() => {
221 return Ok(GuppyResponse::Redirect { target });
222 }
223 Packet::Error { message } if state.is_none() => {
224 return Ok(GuppyResponse::Error { message });
225 }
226 Packet::Prompt { .. } | Packet::Redirect { .. } | Packet::Error { .. } => {}
227 Packet::First { seq, mime, data } => {
228 send(encode_ack(seq)).await?;
231 if state.is_none() {
232 let mut reassembly = Reassembly {
233 first_seq: seq,
234 mime,
235 chunks: BTreeMap::new(),
236 contiguous_end: seq,
237 buffered: 0,
238 eof_seq: None,
239 };
240 reassembly.buffered = data.len();
241 reassembly.chunks.insert(seq, data);
242 state = Some(reassembly);
243 }
244 }
245 Packet::Continuation { seq, data } => {
246 let Some(reassembly) = &mut state else {
250 continue;
251 };
252 if seq <= reassembly.first_seq {
253 continue;
254 }
255 send(encode_ack(seq)).await?;
256 if data.is_empty() {
257 reassembly.eof_seq = Some(seq);
258 } else {
259 reassembly.insert(seq, data);
260 }
261 }
262 }
263 }
264}