Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions examples/cookie.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Emulation>::try_convert(v)?;
builder.emulation = Some((*emulation_obj).clone());
}
Expand All @@ -149,7 +149,7 @@ impl Builder {
builder.proxy = Extractor::<Proxy>::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::<Jar>::try_convert(jar)?).clone());
}

Expand Down
13 changes: 7 additions & 6 deletions src/client/req.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -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,
Expand Down Expand Up @@ -59,7 +60,7 @@ pub struct Request {

/// The cookies to use for the request.
#[serde(skip)]
cookies: Option<Vec<HeaderValue>>,
cookies: Option<Cookies>,

/// Whether to allow redirects.
allow_redirects: Option<bool>,
Expand Down Expand Up @@ -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::<Emulation>::try_convert(v)?;
builder.emulation = Some((*emulation_obj).clone());
}
Expand All @@ -124,13 +125,13 @@ impl Request {
builder.orig_headers = Extractor::<OrigHeaderMap>::try_convert(kwargs)?.into_inner();

// extra cookies handling
builder.cookies = Extractor::<Vec<HeaderValue>>::try_convert(kwargs)?.into_inner();
builder.cookies = Cookies::try_convert(kwargs).map(Some)?;

// extra proxy handling
builder.proxy = Extractor::<Proxy>::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)?);
}

Expand Down Expand Up @@ -199,7 +200,7 @@ pub fn execute_request<U: AsRef<str>>(

// Cookies options.
if let Some(cookies) = request.cookies.take() {
for cookie in cookies {
for cookie in cookies.0 {
builder = builder.header(header::COOKIE, cookie);
}
}
Expand Down
50 changes: 47 additions & 3 deletions src/cookie.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<HeaderValue>);

/// A good default `CookieStore` implementation.
///
/// This is the implementation used when simply calling `cookie_store(true)`.
Expand Down Expand Up @@ -197,6 +202,45 @@ impl fmt::Display for Cookie {
}
}

// ===== impl Cookies =====

impl TryConvert for Cookies {
fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
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 {
Expand Down
37 changes: 2 additions & 35 deletions src/extractor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use bytes::Bytes;
use magnus::{RArray, RHash, RString, Ruby, TryConvert, r_hash::ForEach};
use wreq::{
Proxy, Version,
Expand Down Expand Up @@ -138,38 +137,6 @@ impl TryConvert for Extractor<OrigHeaderMap> {
}
}

// ===== impl Extractor<Vec<HeaderValue>> =====

impl ExtractorName for Vec<HeaderValue> {
const NAME: &str = "cookies";
}

impl TryConvert for Extractor<Vec<HeaderValue>> {
fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
let ruby = Ruby::get_with(value);
let keyword = RHash::try_convert(value)?;

if let Some(hash) = keyword
.get(ruby.to_symbol(Vec::<HeaderValue>::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<Proxy> =====

impl ExtractorName for Proxy {
Expand All @@ -179,9 +146,9 @@ impl ExtractorName for Proxy {
impl TryConvert for Extractor<Proxy> {
fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
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)
{
Expand Down
24 changes: 19 additions & 5 deletions test/cookie_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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