Repository: rack/rack (Ruby)

Issue:
Cookie expires format is invalid (take 2)
It appears that the date format used by the `expires` attribute the `Set-cookie` header is incorrect. This was broken by this commit: 5e0a9413a80e9b0aa8a74bfc806fb20f2ea0bf3c

The comment in the commit is incorrect. It implies that the date format should be as defined in RFC 2822, however I think this is a misreading of section [RFC 2616 section 3.3.1](https://tools.ietf.org/html/rfc2616#section-3.3.1) which includes its own specification of the date format. In particular the difference is that the timezone must be specified as "GMT".

RFC2616 has been superseded by rfc7231 which has the same definition in [section 7.1.1.1](https://tools.ietf.org/html/rfc7231#section-7.1.1.1).

In practice this is probably not causing a lot of problems because most cookie parsers are robust to the error. However occasionally there are problems.

This was first reported in #527. 



Discussion:
We encountered an issue with the invalid date format for the cookie `expires` value as well. 

If you look at the expires format for cookies coming out of Rack you'll see the GMT times end with "0000" rather than "GMT". 

In our case, this was revealed by Apache JMeter, which has strong cookie validation by default.

Here is the snippet from the RFC:

![image](https://cloud.githubusercontent.com/assets/172409/17818815/2705d65a-6613-11e6-82a3-7cf3e394c075.png)

Here is how Rack formats the expires time:

```
Rack::Utils.rfc2822(Time.now.gmtime)
=> "Fri, 19 Aug 2016 17:48:47 -0000"
```

It is stupid to call this a "RFC2822" or "RFC5322" date, when it uses the obsolete RFC822 timezone encoding.

I guess its time for new `#rfc1123` formatting method.

I think the key point is that RFC7321 has its own date specification which is unique in that it always requires the timezone to be specified as `GMT`. So it is not really obsolete, just different (not that I am claiming that is a good thing, it seems like the RFC is trying to maintain compatibility with a legacy mistake).

Yes, but they cite an RFC which defines dates in a different way, which makes it easy to confuse it.

The [Wikipedia page on cookies](https://en.wikipedia.org/wiki/HTTP_cookie) says:

> A definitive specification for cookies as used in the real world was published as RFC 6265 in April 2011.

[That spec](https://tools.ietf.org/html/rfc6265#section-5.1.1) says:

> sane-cookie-date  = `<rfc1123-date, defined in [RFC2616], Section 3.3.1>`

And the [ruby stdlib](https://github.com/rack/rack/issues/1100) provides this date format out of  the box with `httpdate`.

> This library extends the Time class with the following conversions between date strings and Time objects:
> 
> date-time defined by RFC 2822
> HTTP-date defined by RFC 2616
> ...

My PR above uses httpdate for the date format which should tidy things up. 

This looks like it would solve our problem.

I have an example of a client that exhibits this problem. The cookie parsing code on Roku devices is very strict and will refuse to use any cookie that uses a timezone that does not contain the string "GMT".

Looks like latest version 2.0.7 does not contain this fix, which version is planned to have this fix?



Patch:
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index c253f3cf2..eb49466dd 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -221,31 +221,7 @@ def add_cookie_to_header(header, key, value)
         domain  = "; domain=#{value[:domain]}"   if value[:domain]
         path    = "; path=#{value[:path]}"       if value[:path]
         max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
-        # There is an RFC mess in the area of date formatting for Cookies. Not
-        # only are there contradicting RFCs and examples within RFC text, but
-        # there are also numerous conflicting names of fields and partially
-        # cross-applicable specifications.
-        #
-        # These are best described in RFC 2616 3.3.1. This RFC text also
-        # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
-        # fixed length format with space-date delimited fields.
-        #
-        # See also RFC 1123 section 5.2.14.
-        #
-        # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
-        # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
-        # the space delimited format. These formats are compliant with RFC 2822.
-        #
-        # For reference, all involved RFCs are:
-        # RFC 822
-        # RFC 1123
-        # RFC 2109
-        # RFC 2616
-        # RFC 2822
-        # RFC 2965
-        # RFC 6265
-        expires = "; expires=" +
-          rfc2822(value[:expires].clone.gmtime) if value[:expires]
+        expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
         secure = "; secure"  if value[:secure]
         httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
         same_site =


Test patch:
diff --git a/test/spec_response.rb b/test/spec_response.rb
index 987199de5..4fd7d2b3b 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -186,7 +186,7 @@
     response.delete_cookie "foo"
     response["Set-Cookie"].must_equal [
       "foo2=bar2",
-      "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
+      "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
     ].join("\n")
   end
 
@@ -196,10 +196,10 @@
     response.set_cookie "foo", {:value => "bar", :domain => ".example.com"}
     response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n")
     response.delete_cookie "foo", :domain => ".example.com"
-    response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
+    response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n")
     response.delete_cookie "foo", :domain => "sample.example.com"
-    response["Set-Cookie"].must_equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000",
-                                         "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
+    response["Set-Cookie"].must_equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT",
+                                         "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n")
   end
 
   it "can delete cookies with the same name with different paths" do
@@ -211,7 +211,7 @@
 
     response.delete_cookie "foo", :path => "/path"
     response["Set-Cookie"].must_equal ["foo=bar; path=/",
-                                         "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
+                                         "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n")
   end
 
   it "can do redirects" do