Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design direction #1

Open
pfernie opened this issue Dec 14, 2016 · 0 comments
Open

Design direction #1

pfernie opened this issue Dec 14, 2016 · 0 comments

Comments

@pfernie
Copy link
Owner

pfernie commented Dec 14, 2016

This basic implementation of the Cookie concept is relatively similar to the existing cookie-rs crate, although it follows the pattern of the url crate in that it maintains a single underlying string representation and provides getter/setter methods slicing into the serialization. The current design eagerly sets/modifies the underlying serialization as various fields are set.

However, as @seanmonstar mentions in reqwest issue #14, there may be utility in having a "more opinionated" Cookie. To that end, some thoughts follow.

In terms of usage scenarios, I would typically envision the various handlers of cookies as:
Server: The component producing Set-Cookie headers. This server may have a set of commonly generated cookies; that is, cookies with the same name, Domain, etc., but with changing values and possibly expiry information.

It may also receive Requests from clients which contain Cookie headers which are strictly of the form <name>=<value> (e.g. should not contain any attributes such as Domain, etc.).

Client: The component consuming Set-Cookie headers, possibly storing the results in a local store ("CookieJar"), and making Requests of a server containing Cookie headers as described previously.

CookieJar/Store: The client component actually concerned with the various attributes (Domain, Expiry, etc.) of a Cookie; the client itself will only be interested in the name/value pair (CookiePair).

As such, rather than a single, universal Cookie type, we could introduce this conceptual split in the API. For the server, following a builder-style pattern, avoiding creating/modifying the final string until actually need for placement into a Set-Cookie header:

// ``cooky`` lib
struct Recipe {
  name: String,
  max_age: u64, // 0 is not a valid max_age value, and as such can be used to signify "no MaxAge"
  expires: Option<Tm>,
  domain: Option<CookieDomain>,
  path: Option<CookiePath>,
  secure: bool,
  http_only: bool,
}

impl Recipe {
  pub fn new(name: String) -> Recipe { ... }
  pub fn expires_in(self, max_age: u64) -> Recipe { ... }
  pub fn expires_at(self, at: Tm) -> Recipe { ... }
  // validation of domain & path handled elsewhere, so builder API doesn't need to return Result<..>
  pub fn set_domain(self, domain: CookieDomain) -> Recipe { ... }
  pub fn set_path(self, path: CookiePath) -> Recipe { ... }
  pub fn set_secure(self, secure: bool) -> Recipe { ... }
  pub fn set_http_only(self, http_only: bool) -> Recipe { ... }

// instead of returning String, could return ``SetCookiePayload`` or somesuch specific type
  pub fn bake() -> String {
    // creates a cookie String with no value ("<name>=; Domain=...")
  }
  pub fn bake_value(value: &str) -> String {
    // creates a cookie String with the given value
  }
  pub fn stale() -> String {
    // creates an expired version of the cookie ("<name>=; Expires=<time in past>")
  }
}

// inside hyper / some http server
let recipe1 = cooky::Recipe::new("foo").set_secure(true).set_domain("www.example.com".into());
let recipe2 = cooky::Recipe::new("bus").set_http_only(true).expires_in(10);
let response: ResponseBuilder = ...;
response.headers_mut().set(SetCookie(recipe1.bake_value("bar"))); // Set-Cookie: foo=bar; Secure; Domain=www.example.com
response.headers_mut().set(SetCookie(recipe2.bake()); // Set-Cookie: bus=; HttpOnly; Max-Age: 10

// .. responding to some other kind of request elsewhere
response.headers_mut().set(SetCookie(recipe1.stale()); // Set-Cookie: foo=; Secure; Domain=www.example.com; Expires=1900-01-01T00:00:00Z

For the client receiving such a response:

// ``cooky`` lib
struct CookiePair {
  raw_cookie_pair: String,
  name_end: usize,
  value_end: usize,
}

impl CookiePair {
  pub fn name() -> &str { ... }
  pub fn value() -> &str { ... } // possibly the empty string
  pub fn pair() -> (&str, &str) { ... }
}

enum Expiry {
  SessionEnd,
  AtUtc(Tm),
}

struct CookieStoreEntry {
  cookie_pair: CookiePair,
  expiry: Expiry,
  domain: Option<CookieDomain>, // "HostOnly" case encoded here
  path: Option<CookiePath>,
  secure: bool,
  http_only: bool,
  created: Tm,
  last_accessed: Tm,
}

struct CookieStore { ... }
impl CookieStore {
  pub fn store(raw_set_cookie: String, request_url: &Url) { .. } // or SetCookiePayload instead of String?
  pub fn get<U: IntoUrl>(url: U) -> Vec<&CookiePair> { ... }
}

// Client code...
// jar storing/retrieval in the below could be handled by the ``Session`` concept in the ``user_agent`` crate.

// ... client processing some response from www.example.com
if let Some(cs) = response.get::<SetCookie>() {
  // Set-Cookie: foo=bar; Secure; Domain=www.example.com
  // Set-Cookie: bus=; HttpOnly; Max-Age=10
  for c in cs {
    jar.store(c, &request_url);
  }
}

// .. get some other url for www.not-example.com
if let Some(cs) = response.get::<SetCookie>() {
  // Set-Cookie: baz=bang; Domain=www.not-example.com
  for c in cs {
    jar.store(c, &other_url);
  }
}

// ... client making a subsequent request
let url = Url::parse("https://www.example.com/api/foo");
let cookies: Vec<&CookiePair> = jar.get(&url); // get relevant cookies, only the www.example.com ones
for c in cookies {
  request.headers_mut().Set(Cookie(c));
}

(please excuse the naming, I can't help myself).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant