1use std::collections::HashMap;
5use std::io::Read;
6
7use url::Url;
8
9use go_lib::context::Context;
10use crate::error::HttpError;
11use crate::header::Header;
12use crate::parse::transfer::Body;
13
14pub struct Request {
17 pub method: String,
19 pub url: Url,
21 pub proto: String,
23 pub proto_major: u8,
24 pub proto_minor: u8,
25 pub header: Header,
27 pub body: Option<Body>,
29 pub content_length: i64,
31 pub transfer_encoding: Vec<String>,
33 pub host: String,
35 pub trailer: Header,
37 pub remote_addr: String,
39 form: Option<HashMap<String, Vec<String>>>,
41 ctx: Context,
43}
44
45impl Request {
46 pub fn new(method: &str, url: &str, body: Option<Body>) -> Result<Self, HttpError> {
51 let ctx = go_lib::context::background();
52 Self::new_with_context(method, url, body, ctx)
53 }
54
55 pub fn new_with_context(
58 method: &str,
59 url: &str,
60 body: Option<Body>,
61 ctx: Context,
62 ) -> Result<Self, HttpError> {
63 if !crate::method::is_valid(method) {
64 return Err(HttpError::InvalidUrl(format!("invalid method: {method}")));
65 }
66 let parsed = Url::parse(url).map_err(|e| HttpError::InvalidUrl(e.to_string()))?;
67 let host = parsed.host_str().unwrap_or("").to_owned();
68 let content_length = match &body {
69 None => 0,
70 Some(_) => -1,
71 };
72 Ok(Self {
73 method: method.to_owned(),
74 url: parsed,
75 proto: "HTTP/1.1".into(),
76 proto_major: 1,
77 proto_minor: 1,
78 header: Header::new(),
79 body,
80 content_length,
81 transfer_encoding: Vec::new(),
82 host,
83 trailer: Header::new(),
84 remote_addr: String::new(),
85 form: None,
86 ctx,
87 })
88 }
89
90 pub fn context(&self) -> &Context {
93 &self.ctx
94 }
95
96 pub fn with_context(mut self, ctx: Context) -> Self {
99 self.ctx = ctx;
100 self
101 }
102
103 pub fn user_agent(&self) -> &str {
106 self.header.get("User-Agent").unwrap_or("")
107 }
108
109 pub fn referer(&self) -> &str {
110 self.header.get("Referer").unwrap_or("")
111 }
112
113 pub fn basic_auth(&self) -> Option<(String, String)> {
116 let val = self.header.get("Authorization")?;
117 let rest = val.strip_prefix("Basic ")?;
118 let decoded = base64::Engine::decode(
119 &base64::engine::general_purpose::STANDARD,
120 rest.trim(),
121 )
122 .ok()?;
123 let s = String::from_utf8(decoded).ok()?;
124 let colon = s.find(':')?;
125 Some((s[..colon].to_owned(), s[colon + 1..].to_owned()))
126 }
127
128 pub fn cookies(&self) -> Vec<crate::cookie::Cookie> {
132 crate::cookie::parse_request_cookies(&self.header)
133 }
134
135 pub fn cookie(&self, name: &str) -> Option<crate::cookie::Cookie> {
137 self.cookies().into_iter().find(|c| c.name == name)
138 }
139
140 pub fn parse_form(&mut self) -> Result<(), HttpError> {
145 if self.form.is_some() {
146 return Ok(());
147 }
148 let mut values: HashMap<String, Vec<String>> = HashMap::new();
149
150 for (k, v) in self.url.query_pairs() {
152 values.entry(k.into_owned()).or_default().push(v.into_owned());
153 }
154
155 let ct = self
157 .header
158 .get("Content-Type")
159 .unwrap_or("")
160 .to_ascii_lowercase();
161 if matches!(self.method.as_str(), "POST" | "PUT" | "PATCH")
162 && ct.starts_with("application/x-www-form-urlencoded")
163 {
164 if let Some(body) = self.body.take() {
165 let mut raw = Vec::new();
166 BodyReader(body)
167 .read_to_end(&mut raw)
168 .map_err(|_| HttpError::BodyRead)?;
169 let s = String::from_utf8_lossy(&raw);
170 for pair in s.split('&') {
171 if let Some((k, v)) = pair.split_once('=') {
172 let k = url_decode(k);
173 let v = url_decode(v);
174 values.entry(k).or_default().push(v);
175 }
176 }
177 }
178 }
179
180 self.form = Some(values);
181 Ok(())
182 }
183
184 pub fn form_value(&self, key: &str) -> Option<&str> {
186 self.form
187 .as_ref()?
188 .get(key)?
189 .first()
190 .map(String::as_str)
191 }
192
193 pub fn write_header_to(&self, w: &mut impl std::io::Write) -> Result<(), HttpError> {
198 let path = if self.url.path().is_empty() { "/" } else { self.url.path() };
199 let query = self
200 .url
201 .query()
202 .map(|q| format!("?{q}"))
203 .unwrap_or_default();
204
205 write!(w, "{} {}{} {}\r\n", self.method, path, query, self.proto)?;
206 write!(w, "Host: {}\r\n", self.host)?;
207 self.header.write_to(w)?;
208 w.write_all(b"\r\n")?;
209 Ok(())
210 }
211}
212
213struct BodyReader(Body);
215impl Read for BodyReader {
216 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
217 self.0.read(buf)
218 }
219}
220
221fn url_decode(s: &str) -> String {
222 let s = s.replace('+', " ");
224 url::form_urlencoded::parse(s.as_bytes())
225 .map(|(k, _)| k.into_owned())
226 .next()
227 .unwrap_or(s.clone())
228}
229
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn new_get_request() {
237 let req = Request::new("GET", "http://example.com/path?q=1", None).unwrap();
238 assert_eq!(req.method, "GET");
239 assert_eq!(req.host, "example.com");
240 assert_eq!(req.url.path(), "/path");
241 }
242
243 #[test]
244 fn invalid_method_rejected() {
245 assert!(Request::new("GÉT", "http://example.com/", None).is_err());
246 }
247
248 #[test]
249 fn write_header() {
250 let mut req = Request::new("GET", "http://example.com/", None).unwrap();
251 req.header.set("Accept", "text/html");
252 let mut out = Vec::new();
253 req.write_header_to(&mut out).unwrap();
254 let s = String::from_utf8(out).unwrap();
255 assert!(s.starts_with("GET / HTTP/1.1\r\n"));
256 assert!(s.contains("Host: example.com\r\n"));
257 assert!(s.contains("Accept: text/html\r\n"));
258 }
259
260 #[test]
261 fn write_header_includes_query() {
262 let req = Request::new("GET", "http://example.com/search?q=rust&n=1", None).unwrap();
263 let mut out = Vec::new();
264 req.write_header_to(&mut out).unwrap();
265 let s = String::from_utf8(out).unwrap();
266 assert!(s.starts_with("GET /search?q=rust&n=1 HTTP/1.1\r\n"), "got: {s:?}");
267 }
268
269 #[test]
270 fn body_sets_content_length_unknown() {
271 let body = Body::Unbounded(Box::new(std::io::Cursor::new(b"abc".to_vec())));
272 let req = Request::new("POST", "http://example.com/", Some(body)).unwrap();
273 assert_eq!(req.content_length, -1);
274 let none = Request::new("GET", "http://example.com/", None).unwrap();
275 assert_eq!(none.content_length, 0);
276 }
277
278 #[test]
279 fn parse_form_query_only() {
280 let mut req = Request::new("GET", "http://x/p?a=1&a=2&b=hello", None).unwrap();
281 req.parse_form().unwrap();
282 assert_eq!(req.form_value("a"), Some("1"));
283 assert_eq!(req.form_value("b"), Some("hello"));
284 assert_eq!(req.form_value("missing"), None);
285 req.parse_form().unwrap();
287 assert_eq!(req.form_value("a"), Some("1"));
288 }
289
290 #[test]
291 fn parse_form_urlencoded_body() {
292 let body = Body::Unbounded(Box::new(std::io::Cursor::new(
293 b"name=Alice&city=New+York".to_vec(),
294 )));
295 let mut req = Request::new("POST", "http://x/submit?q=top", Some(body)).unwrap();
296 req.header.set("Content-Type", "application/x-www-form-urlencoded");
297 req.parse_form().unwrap();
298 assert_eq!(req.form_value("q"), Some("top"));
300 assert_eq!(req.form_value("name"), Some("Alice"));
301 assert_eq!(req.form_value("city"), Some("New York"));
302 }
303
304 #[test]
305 fn basic_auth_valid() {
306 use base64::Engine;
307 let creds = base64::engine::general_purpose::STANDARD.encode("alice:s3cret");
308 let mut req = Request::new("GET", "http://x/", None).unwrap();
309 req.header.set("Authorization", format!("Basic {creds}"));
310 let (user, pass) = req.basic_auth().unwrap();
311 assert_eq!(user, "alice");
312 assert_eq!(pass, "s3cret");
313 }
314
315 #[test]
316 fn basic_auth_absent_or_wrong_scheme() {
317 let req = Request::new("GET", "http://x/", None).unwrap();
318 assert!(req.basic_auth().is_none());
319
320 let mut bearer = Request::new("GET", "http://x/", None).unwrap();
321 bearer.header.set("Authorization", "Bearer token123");
322 assert!(bearer.basic_auth().is_none());
323 }
324
325 #[test]
326 fn user_agent_and_referer() {
327 let mut req = Request::new("GET", "http://x/", None).unwrap();
328 assert_eq!(req.user_agent(), "");
329 assert_eq!(req.referer(), "");
330 req.header.set("User-Agent", "go-http/0.1");
331 req.header.set("Referer", "http://ref/");
332 assert_eq!(req.user_agent(), "go-http/0.1");
333 assert_eq!(req.referer(), "http://ref/");
334 }
335
336 #[test]
337 fn cookies_parsed_from_header() {
338 let mut req = Request::new("GET", "http://x/", None).unwrap();
339 req.header.set("Cookie", "session=abc; theme=dark");
340 let all = req.cookies();
341 assert_eq!(all.len(), 2);
342 assert_eq!(req.cookie("session").unwrap().value, "abc");
343 assert_eq!(req.cookie("theme").unwrap().value, "dark");
344 assert!(req.cookie("nope").is_none());
345 }
346
347 #[test]
348 fn with_context_replaces_context() {
349 let req = Request::new("GET", "http://x/", None).unwrap();
350 let ctx = go_lib::context::background();
351 let req = req.with_context(ctx);
352 let _ = req.context();
354 }
355}