diff --git a/examples/cookie.rb b/examples/cookie.rb new file mode 100644 index 0000000..91ff959 --- /dev/null +++ b/examples/cookie.rb @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby + +require_relative "../lib/wreq" + +# Make a client +client = Wreq::Client.new + +# Send a GET request with cookies provided as a Hash. +# This form is serialized as multiple Cookie header fields (common in HTTP/2). +resp = client.get( + "https://tls.browserleaks.com", + cookies: {"foo" => "bar", "baz" => "qux"} +) + +puts resp.text + +# Send a GET request with cookies provided as a single Cookie header string. +# This form is common in HTTP/1.1: one Cookie header with '; ' separated pairs. +resp = client.get( + "https://tls.browserleaks.com", + cookies: "foo=bar; baz=qux" +) + +puts resp.text diff --git a/src/client.rs b/src/client.rs index aa536e4..df5256c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -131,7 +131,7 @@ impl Builder { if let Ok(hash) = RHash::try_convert(*keyword) { let mut builder: Self = serde_magnus::deserialize(ruby, hash)?; // extra emulation handling - if let Some(v) = hash.get(ruby.to_symbol("emulation")) { + if let Some(v) = hash.get(ruby.to_symbol(stringify!(emulation))) { let emulation_obj = Obj::::try_convert(v)?; builder.emulation = Some((*emulation_obj).clone()); } @@ -149,7 +149,7 @@ impl Builder { builder.proxy = Extractor::::try_convert(*keyword)?.into_inner(); // extra cookie store handling - if let Some(jar) = hash.get(ruby.to_symbol("cookie_provider")) { + if let Some(jar) = hash.get(ruby.to_symbol(stringify!(cookie_provider))) { builder.cookie_provider = Some((*Obj::::try_convert(jar)?).clone()); } diff --git a/src/client/req.rs b/src/client/req.rs index c9ff446..20d8ea2 100644 --- a/src/client/req.rs +++ b/src/client/req.rs @@ -1,6 +1,6 @@ use std::{net::IpAddr, time::Duration}; -use http::{HeaderValue, header}; +use http::header; use magnus::{RHash, TryConvert, typed_data::Obj, value::ReprValue}; use serde::Deserialize; use wreq::{ @@ -11,6 +11,7 @@ use wreq::{ use super::body::{Body, Form, Json}; use crate::{ client::{query::Query, resp::Response}, + cookie::Cookies, emulate::Emulation, error::wreq_error_to_magnus, extractor::Extractor, @@ -59,7 +60,7 @@ pub struct Request { /// The cookies to use for the request. #[serde(skip)] - cookies: Option>, + cookies: Option, /// Whether to allow redirects. allow_redirects: Option, @@ -109,7 +110,7 @@ impl Request { let mut builder: Self = serde_magnus::deserialize(ruby, kwargs)?; // extra emulation handling - if let Some(v) = hash.get(ruby.to_symbol("emulation")) { + if let Some(v) = hash.get(ruby.to_symbol(stringify!(emulation))) { let emulation_obj = Obj::::try_convert(v)?; builder.emulation = Some((*emulation_obj).clone()); } @@ -124,13 +125,13 @@ impl Request { builder.orig_headers = Extractor::::try_convert(kwargs)?.into_inner(); // extra cookies handling - builder.cookies = Extractor::>::try_convert(kwargs)?.into_inner(); + builder.cookies = Cookies::try_convert(kwargs).map(Some)?; // extra proxy handling builder.proxy = Extractor::::try_convert(kwargs)?.into_inner(); // extra body handling - if let Some(body) = hash.get(ruby.to_symbol("body")) { + if let Some(body) = hash.get(ruby.to_symbol(stringify!(body))) { builder.body = Some(Body::try_convert(body)?); } @@ -199,7 +200,7 @@ pub fn execute_request>( // Cookies options. if let Some(cookies) = request.cookies.take() { - for cookie in cookies { + for cookie in cookies.0 { builder = builder.header(header::COOKIE, cookie); } } diff --git a/src/cookie.rs b/src/cookie.rs index 060b4c1..21ff752 100644 --- a/src/cookie.rs +++ b/src/cookie.rs @@ -1,13 +1,14 @@ use std::{fmt, sync::Arc, time::SystemTime}; +use bytes::Bytes; use cookie::{Cookie as RawCookie, Expiration, ParseError, time::Duration}; use magnus::{ - Error, Module, Object, RModule, Ruby, Value, function, method, typed_data::Obj, - value::ReprValue, + Error, Module, Object, RHash, RModule, RString, Ruby, TryConvert, Value, function, method, + r_hash::ForEach, typed_data::Obj, value::ReprValue, }; use wreq::header::{self, HeaderMap, HeaderValue}; -use crate::gvl; +use crate::{error::header_value_error_to_magnus, gvl}; define_ruby_enum!( /// The Cookie SameSite attribute. @@ -25,6 +26,10 @@ define_ruby_enum!( #[magnus::wrap(class = "Wreq::Cookie", free_immediately, size)] pub struct Cookie(RawCookie<'static>); +/// A collection of HTTP cookies. +#[derive(Default)] +pub struct Cookies(pub Vec); + /// A good default `CookieStore` implementation. /// /// This is the implementation used when simply calling `cookie_store(true)`. @@ -197,6 +202,45 @@ impl fmt::Display for Cookie { } } +// ===== impl Cookies ===== + +impl TryConvert for Cookies { + fn try_convert(value: magnus::Value) -> Result { + let ruby = Ruby::get_with(value); + let rhash = RHash::try_convert(value)?; + + // try extract uncompressed cookies + if let Some(hash) = rhash + .get(ruby.to_symbol(stringify!(cookies))) + .and_then(RHash::from_value) + { + let mut cookies = Vec::new(); + hash.foreach(|name: RString, value: RString| { + let cookie = format!("{name}={value}"); + let header_value = HeaderValue::from_maybe_shared(Bytes::from(cookie)) + .map_err(header_value_error_to_magnus)?; + cookies.push(header_value); + Ok(ForEach::Continue) + })?; + + return Ok(Self(cookies)); + } + + // try extract compressed cookies + if let Some(cookies) = rhash + .get(ruby.to_symbol(stringify!(cookies))) + .and_then(RString::from_value) + { + return Ok(Self(vec![ + HeaderValue::from_maybe_shared(Bytes::from(cookies.to_string()?)) + .map_err(header_value_error_to_magnus)?, + ])); + } + + Ok(Self::default()) + } +} + // ===== impl Jar ===== impl Jar { diff --git a/src/extractor.rs b/src/extractor.rs index 0ccfa8e..e16d548 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1,4 +1,3 @@ -use bytes::Bytes; use magnus::{RArray, RHash, RString, Ruby, TryConvert, r_hash::ForEach}; use wreq::{ Proxy, Version, @@ -138,38 +137,6 @@ impl TryConvert for Extractor { } } -// ===== impl Extractor> ===== - -impl ExtractorName for Vec { - const NAME: &str = "cookies"; -} - -impl TryConvert for Extractor> { - fn try_convert(value: magnus::Value) -> Result { - let ruby = Ruby::get_with(value); - let keyword = RHash::try_convert(value)?; - - if let Some(hash) = keyword - .get(ruby.to_symbol(Vec::::NAME)) - .and_then(RHash::from_value) - { - let mut cookies = Vec::new(); - hash.foreach(|name: RString, value: RString| { - let value = value.to_string()?; - let cookie = format!("{name}={value}"); - let header_value = HeaderValue::from_maybe_shared(Bytes::from(cookie)) - .map_err(header_value_error_to_magnus)?; - cookies.push(header_value); - Ok(ForEach::Continue) - })?; - - return Ok(Extractor(Some(cookies))); - } - - Ok(Extractor(None)) - } -} - // ===== impl Extractor ===== impl ExtractorName for Proxy { @@ -179,9 +146,9 @@ impl ExtractorName for Proxy { impl TryConvert for Extractor { fn try_convert(value: magnus::Value) -> Result { let ruby = Ruby::get_with(value); - let keyword = RHash::try_convert(value)?; + let rhash = RHash::try_convert(value)?; - if let Some(proxy) = keyword + if let Some(proxy) = rhash .get(ruby.to_symbol(Proxy::NAME)) .and_then(RString::from_value) { diff --git a/test/cookie_test.rb b/test/cookie_test.rb index 1631a9d..cdc6f18 100644 --- a/test/cookie_test.rb +++ b/test/cookie_test.rb @@ -154,13 +154,27 @@ def test_same_site_flags_from_parsed_header assert_equal [false, true], h["s2"] end - def test_request_cookie_value_percent_encoding - raw_value = "hello world?" + def test_request_uncompressed_cookies client = Wreq::Client.new resp = client.get( - "http://localhost:8080/cookies", - cookies: {"mykey" => raw_value} + "https://httpbin.io/cookies", + cookies: {"foo" => "bar", "baz" => "qux"} ) - assert_includes resp.text, "hello world?" + json = resp.json + assert_instance_of Hash, json + assert_equal "bar", json["foo"] + assert_equal "qux", json["baz"] + end + + def test_request_compressed_cookies + client = Wreq::Client.new + resp = client.get( + "https://httpbin.io/cookies", + cookies: "foo=bar; baz=qux" + ) + json = resp.json + assert_instance_of Hash, json + assert_equal "bar", json["foo"] + assert_equal "qux", json["baz"] end end