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

Add custom time format implementations #5123

Merged
Prev Previous commit
Next Next commit
Add custom formatter for HTTP date format
  • Loading branch information
straight-shoota authored and bcardiff committed Jun 9, 2018
commit b69bc43aec7c2d3630cd4080097af201d91c648a
6 changes: 3 additions & 3 deletions spec/std/http/http_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ describe HTTP do
parsed_time.to_utc.to_s.should eq("2011-09-10 02:36:00 UTC")
end

describe "generates RFC 1123" do
describe "generates HTTP date" do
it "without time zone" do
time = Time.utc(1994, 11, 6, 8, 49, 37, nanosecond: 0)
HTTP.rfc1123_date(time).should eq("Sun, 06 Nov 1994 08:49:37 GMT")
HTTP.format_time(time).should eq("Sun, 6 Nov 1994 08:49:37 GMT")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preferred format is with two day digits as of https://tools.ietf.org/html/rfc7231#section-7.1.1.1 and https://tools.ietf.org/html/rfc2616#page-21

Somehow I would prefer rfc/iso in the function names for the format.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name was changed because the methods doesn't implement one exact standard format but generally format and parse time instances for use in a HTTP protocol context (see previous comments in #4729 (comment)).

I'll look into the preferred digit format.

end

it "with local time zone" do
time = Time.new(1994, 11, 6, 8, 49, 37, nanosecond: 0, location: Time::Location.load("Europe/Berlin"))
HTTP.rfc1123_date(time).should eq(time.to_utc.to_s("%a, %d %b %Y %H:%M:%S GMT"))
HTTP.format_time(time).should eq(time.to_utc.to_s("%a, %-d %b %Y %H:%M:%S GMT"))
end
end

Expand Down
12 changes: 6 additions & 6 deletions spec/std/http/server/handlers/static_file_handler_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,26 @@ describe HTTP::StaticFileHandler do
context "with header If-Modified-Since" do
it "should return 304 Not Modified if file mtime is equal" do
headers = HTTP::Headers.new
headers["If-Modified-Since"] = HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time)
headers["If-Modified-Since"] = HTTP.format_time(File.info("#{__DIR__}/static/test.txt").modification_time)
response = handle HTTP::Request.new("GET", "/test.txt", headers), ignore_body: true
response.status_code.should eq(304)
response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time))
response.headers["Last-Modified"].should eq(HTTP.format_time(File.info("#{__DIR__}/static/test.txt").modification_time))
end

it "should return 304 Not Modified if file mtime is older" do
headers = HTTP::Headers.new
headers["If-Modified-Since"] = HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time + 1.hour)
headers["If-Modified-Since"] = HTTP.format_time(File.info("#{__DIR__}/static/test.txt").modification_time + 1.hour)
response = handle HTTP::Request.new("GET", "/test.txt", headers), ignore_body: true
response.status_code.should eq(304)
response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time))
response.headers["Last-Modified"].should eq(HTTP.format_time(File.info("#{__DIR__}/static/test.txt").modification_time))
end

it "should serve file if file mtime is younger" do
headers = HTTP::Headers.new
headers["If-Modified-Since"] = HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time - 1.hour)
headers["If-Modified-Since"] = HTTP.format_time(File.info("#{__DIR__}/static/test.txt").modification_time - 1.hour)
response = handle HTTP::Request.new("GET", "/test.txt")
response.status_code.should eq(200)
response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time))
response.headers["Last-Modified"].should eq(HTTP.format_time(File.info("#{__DIR__}/static/test.txt").modification_time))
response.body.should eq(File.read("#{__DIR__}/static/test.txt"))
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/std/yaml/serialization_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ describe "YAML serialization" do

it "does for utc time with nanoseconds" do
time = Time.utc(2010, 11, 12, 1, 2, 3, nanosecond: 456_000_000)
time.to_yaml.should eq("--- 2010-11-12 01:02:03.456\n...\n")
time.to_yaml.should eq("--- 2010-11-12 01:02:03.456000000\n...\n")
end

it "does for bytes" do
Expand Down
41 changes: 26 additions & 15 deletions src/http/common.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
{% end %}

module HTTP
private DATE_PATTERNS = {"%a, %d %b %Y %H:%M:%S %z", "%d %b %Y %H:%M:%S %z", "%A, %d-%b-%y %H:%M:%S %z", "%a %b %e %H:%M:%S %Y"}

# :nodoc:
MAX_HEADER_SIZE = 16_384

Expand Down Expand Up @@ -227,25 +225,38 @@ module HTTP
ComputedContentTypeHeader.new(content_type.strip, nil)
end

# Parse a time string using the formats specified by [RFC 2616](https://tools.ietf.org/html/rfc2616#section-3.3.1)
#
# ```
# HTTP.parse_time("Sun, 14 Feb 2016 21:00:00 GMT") # => "2016-02-14 21:00:00 UTC"
# HTTP.parse_time("Sunday, 14-Feb-16 21:00:00 GMT") # => "2016-02-14 21:00:00 UTC"
# HTTP.parse_time("Sun Feb 14 21:00:00 2016") # => "2016-02-14 21:00:00 UTC"
# ```
#
# Uses `Time::Format::HTTP_DATE` as parser.
def self.parse_time(time_str : String) : Time?
DATE_PATTERNS.each do |pattern|
begin
return Time.parse(time_str, pattern, location: Time::Location::UTC)
rescue Time::Format::Error
end
end

nil
Time::Format::HTTP_DATE.parse(time_str)
rescue Time::Format::Error
end

# Format a Time object as a String using the format specified by [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55).
# Format a `Time` object as a `String` using the format specified as `sane-cookie-date`
# by [RFC 6265](https://tools.ietf.org/html/rfc6265#section-4.1.1) which is
# according to [RFC 2616](https://tools.ietf.org/html/rfc2616#section-3.3.1) a
# [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55) format with explicit
# timezone `GMT` (interpreted as `UTC`).
#
# ```
# HTTP.rfc1123_date(Time.new(2016, 2, 15)) # => "Sun, 14 Feb 2016 21:00:00 GMT"
# HTTP.format_time(Time.new(2016, 2, 15)) # => "Sun, 14 Feb 2016 21:00:00 GMT"
# ```
def self.rfc1123_date(time : Time) : String
# TODO: GMT should come from the Time classes instead
time.to_utc.to_s("%a, %d %b %Y %H:%M:%S GMT")
#
# Uses `Time::Format::HTTP_DATE` as formatter.
def self.format_time(time : Time) : String
Time::Format::HTTP_DATE.format(time)
end

# DEPRECATED: Use `HTTP.format_time` instead.
def self.rfc1123_time(time : Time) : String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is left and marked as deprecated and rfc1123_date dropped completely? I think is either both or none.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it needs to be removed.

format_time(time)
end

# Dequotes an [RFC 2616](https://tools.ietf.org/html/rfc2616#page-17)
Expand Down
2 changes: 1 addition & 1 deletion src/http/cookie.cr
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module HTTP
header << "#{URI.escape @name}=#{URI.escape value}"
header << "; domain=#{domain}" if domain
header << "; path=#{path}" if path
header << "; expires=#{HTTP.rfc1123_date(expires)}" if expires
header << "; expires=#{HTTP.format_time(expires)}" if expires
header << "; Secure" if @secure
header << "; HttpOnly" if @http_only
header << "; #{@extension}" if @extension
Expand Down
2 changes: 1 addition & 1 deletion src/http/server/handlers/static_file_handler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class HTTP::StaticFileHandler
directory_listing(context.response, request_path, file_path)
elsif is_file
last_modified = File.info(file_path).modification_time
context.response.headers["Last-Modified"] = HTTP.rfc1123_date(last_modified)
context.response.headers["Last-Modified"] = HTTP.format_time(last_modified)

if if_modified_since = context.request.headers["If-Modified-Since"]?
header_time = HTTP.parse_time(if_modified_since)
Expand Down
106 changes: 106 additions & 0 deletions src/time/format/custom/http_date.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
require "./rfc_2822"

# Parse a time string using the formats specified by [RFC 2616](https://tools.ietf.org/html/rfc2616#section-3.3.1).
#
# Supported formats:
# * [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55)
# * [RFC 850](https://tools.ietf.org/html/rfc850#section-2.1.4)
# * [asctime](http://en.cppreference.com/w/c/chrono/asctime)
#
# ```
# Time::Format::HTTP_DATE.parse("Sun, 14 Feb 2016 21:00:00 GMT") # => 2016-02-14 21:00:00 UTC
# Time::Format::HTTP_DATE.parse("Sunday, 14-Feb-16 21:00:00 GMT") # => 2016-02-14 21:00:00 UTC
# Time::Format::HTTP_DATE.parse("Sun Feb 14 21:00:00 2016") # => 2016-02-14 21:00:00 UTC
#
# Time::Format::HTTP_DATE.format(Time.new(2016, 2, 15)) # => "Sun, 14 Feb 2016 21:00:00 GMT"
# ```
struct Time::Format
module HTTP_DATE
# Parses a string into a `Time`.
def self.parse(string, location = Time::Location::UTC) : Time
parser = Parser.new(string)
parser.http_date
parser.time(location)
end

# Formats a `Time` into the given *io*.
#
# *time* is always converted to UTC.
def self.format(time : Time, io : IO)
formatter = Formatter.new(time.to_utc, io)
formatter.rfc_2822(time_zone_gmt: true)
io
end

# Formats a `Time` into a `String`.
#
# *time* is always converted to UTC.
def self.format(time : Time)
String.build do |io|
format(time, io)
end
end
end

struct Parser
def http_date
ansi_c_format = http_date_short_day_name_with_comma?

if ansi_c_format
return http_date_ansi_c
end

day_of_month_zero_padded

if current_char.ascii_whitespace?
whitespace
short_month_name
whitespace
year
else
char '-'
short_month_name
char '-'
year_modulo_100
end

whitespace
twenty_four_hour_time_with_seconds
whitespace
time_zone_gmt_or_rfc2822
end

def http_date_ansi_c
short_month_name
whitespace
day_of_month_blank_padded

whitespace

twenty_four_hour_time_with_seconds

whitespace

year

@location = Time::Location::UTC
end

def http_date_rfc1123?(ansi_c_format)
!ansi_c_format && current_char.ascii_whitespace?
end

def http_date_short_day_name_with_comma?
return unless current_char.ascii_letter?

short_day_name

ansi_c_format = current_char != ','
next_char unless ansi_c_format

whitespace

ansi_c_format
end
end
end
8 changes: 6 additions & 2 deletions src/time/format/custom/rfc_2822.cr
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct Time::Format
end

module Pattern
def rfc_2822
def rfc_2822(time_zone_gmt = false)
cfws?
short_day_name_with_comma?
day_of_month
Expand All @@ -48,7 +48,11 @@ struct Time::Format

folding_white_space

time_zone_rfc2822
if time_zone_gmt
time_zone_gmt_or_rfc2822
else
time_zone_rfc2822
end

cfws?
end
Expand Down
Loading