Add handling of HTTP header "Expires"

The value of the "Expires" header is now available as a property on
the HttpResponse and the HttpClientResponse.

Added formatting of dates as HTTP dates in RFC 1123 format and parsing
of HTTP dates in all three formats mentioned in RFC 2616.

R=ajohnsen@google.com

BUG=none
TEST=tests/standalone/src/io/HttpDateTest.dart

Review URL: https://chromiumcodereview.appspot.com//9602011

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@5016 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
sgjesse@google.com 2012-03-06 11:24:11 +00:00
parent 93eecd778e
commit 144e6d044c
5 changed files with 362 additions and 4 deletions

View file

@ -176,6 +176,12 @@ interface HttpResponse default _HttpResponse {
*/
String reasonPhrase;
/**
* Gets and sets the expiry date. The value of this property will
* reflect the "Expires" header
*/
Date expires;
/**
* Sets a header on the response. NOTE: If the same header name is
* set more than once only the last value will be part of the
@ -365,6 +371,14 @@ interface HttpClientResponse default _HttpClientResponse {
*/
bool get keepAlive();
/**
* Returns the date value for the "Expires" header. Returns null if
* the response has no "Expires" header. Throws a HttpException if
* the response has an "Expires" header which is not formatted as a
* valid HTTP date.
*/
Date get expires();
/**
* Returns the response headers.
*/

View file

@ -61,7 +61,7 @@ class _HttpRequestResponseBase {
Map get headers() => _headers;
void _setHeader(String name, String value) {
_headers[name] = value;
_headers[name.toLowerCase()] = value;
}
bool _write(List<int> data, bool copyBuffer) {
@ -280,11 +280,25 @@ class _HttpResponse extends _HttpRequestResponseBase implements HttpResponse {
_reasonPhrase = reasonPhrase;
}
Date get expires() => _expires;
void set expires(Date expires) {
if (_outputStream != null) throw new HttpException("Header already sent");
_expires = expires;
// Format "Expires" header with date in Greenwich Mean Time (GMT).
String formatted =
_HttpUtils.formatDate(_expires.changeTimeZone(new TimeZone.utc()));
_setHeader("Expires", formatted);
}
// Set a header on the response. NOTE: If the same header is set
// more than once only the last one will be part of the response.
void setHeader(String name, String value) {
if (_outputStream != null) return new HttpException("Header already sent");
_setHeader(name, value);
if (name.toLowerCase() == "expires") {
expires = _HttpUtils.parseDate(value);
} else {
_setHeader(name, value);
}
}
OutputStream get outputStream() {
@ -431,6 +445,7 @@ class _HttpResponse extends _HttpRequestResponseBase implements HttpResponse {
// Response status code.
int _statusCode;
String _reasonPhrase;
Date _expires;
_HttpOutputStream _outputStream;
int _state;
}
@ -766,7 +781,7 @@ class _HttpClientRequest
_updateHostHeader() {
String portPart = _port == HttpClient.DEFAULT_HTTP_PORT ? "" : ":$_port";
_setHeader("host", "$host$portPart");
_setHeader("Host", "$host$portPart");
}
// Delegate functions for the HttpOutputStream implementation.
@ -855,6 +870,14 @@ class _HttpClientResponse
int get statusCode() => _statusCode;
String get reasonPhrase() => _reasonPhrase;
Date get expires() {
String str = _headers["expires"];
if (str == null) return null;
return _HttpUtils.parseDate(str);
}
Map get headers() => _headers;
InputStream get inputStream() {
if (_inputStream == null) {
_inputStream = new _HttpInputStream(this);

View file

@ -59,4 +59,172 @@ class _HttpUtils {
}
return result;
}
// From RFC 2616 section "3.3.1 Full Date"
// HTTP-date = rfc1123-date | rfc850-date | asctime-date
// rfc1123-date = wkday "," SP date1 SP time SP "GMT"
// rfc850-date = weekday "," SP date2 SP time SP "GMT"
// asctime-date = wkday SP date3 SP time SP 4DIGIT
// date1 = 2DIGIT SP month SP 4DIGIT
// ; day month year (e.g., 02 Jun 1982)
// date2 = 2DIGIT "-" month "-" 2DIGIT
// ; day-month-year (e.g., 02-Jun-82)
// date3 = month SP ( 2DIGIT | ( SP 1DIGIT ))
// ; month day (e.g., Jun 2)
// time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
// ; 00:00:00 - 23:59:59
// wkday = "Mon" | "Tue" | "Wed"
// | "Thu" | "Fri" | "Sat" | "Sun"
// weekday = "Monday" | "Tuesday" | "Wednesday"
// | "Thursday" | "Friday" | "Saturday" | "Sunday"
// month = "Jan" | "Feb" | "Mar" | "Apr"
// | "May" | "Jun" | "Jul" | "Aug"
// | "Sep" | "Oct" | "Nov" | "Dec"
// Format as RFC 1123 date.
static String formatDate(Date date) {
List wkday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
List month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
Date d = date.changeTimeZone(new TimeZone.utc());
StringBuffer sb = new StringBuffer();
sb.add(wkday[d.weekday]);
sb.add(", ");
sb.add(d.day.toString());
sb.add(" ");
sb.add(month[d.month - 1]);
sb.add(" ");
sb.add(d.year.toString());
d.hours < 9 ? sb.add(" 0") : sb.add(" ");
sb.add(d.hours.toString());
d.minutes < 9 ? sb.add(":0") : sb.add(":");
sb.add(d.minutes.toString());
d.seconds < 9 ? sb.add(":0") : sb.add(":");
sb.add(d.seconds.toString());
sb.add(" GMT");
return sb.toString();
}
static Date parseDate(String date) {
final int SP = 32;
List wkdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
List weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"];
List months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
final int formatRfc1123 = 0;
final int formatRfc850 = 1;
final int formatAsctime = 2;
int index = 0;
String tmp;
int format;
void expect(String s) {
if (date.length - index < s.length) {
throw new HttpException("Invalid HTTP date $date");
}
String tmp = date.substring(index, index + s.length);
if (tmp != s) {
throw new HttpException("Invalid HTTP date $date");
}
index += s.length;
}
int expectWeekday() {
int weekday;
// The formatting of the weekday signals the format of the date string.
int pos = date.indexOf(",", index);
if (pos == -1) {
int pos = date.indexOf(" ", index);
if (pos == -1) throw new HttpException("Invalid HTTP date $date");
tmp = date.substring(index, pos);
index = pos + 1;
weekday = wkdays.indexOf(tmp);
if (weekday != -1) {
format = formatAsctime;
return weekday;
}
} else {
tmp = date.substring(index, pos);
index = pos + 1;
weekday = wkdays.indexOf(tmp);
if (weekday != -1) {
format = formatRfc1123;
return weekday;
}
weekday = weekdays.indexOf(tmp);
if (weekday != -1) {
format = formatRfc850;
return weekday;
}
}
throw new HttpException("Invalid HTTP date $date");
}
int expectMonth(String separator) {
int pos = date.indexOf(separator, index);
if (pos - index != 3) throw new HttpException("Invalid HTTP date $date");
tmp = date.substring(index, pos);
index = pos + 1;
int month = months.indexOf(tmp);
if (month != -1) return month;
throw new HttpException("Invalid HTTP date $date");
}
int expectNum(String separator) {
int pos;
if (separator.length > 0) {
pos = date.indexOf(separator, index);
} else {
pos = date.length;
}
String tmp = date.substring(index, pos);
index = pos + separator.length;
try {
int value = Math.parseInt(tmp);
return value;
} catch (BadNumberFormatException e) {
throw new HttpException("Invalid HTTP date $date");
}
}
void expectEnd() {
if (index != date.length) {
throw new HttpException("Invalid HTTP date $date");
}
}
int weekday = expectWeekday();
int day;
int month;
int year;
int hours;
int minutes;
int seconds;
if (format == formatAsctime) {
month = expectMonth(" ");
if (date.charCodeAt(index) == SP) index++;
day = expectNum(" ");
hours = expectNum(":");
minutes = expectNum(":");
seconds = expectNum(" ");
year = expectNum("");
} else {
expect(" ");
day = expectNum(format == formatRfc1123 ? " " : "-");
month = expectMonth(format == formatRfc1123 ? " " : "-");
year = expectNum(" ");
hours = expectNum(":");
minutes = expectNum(":");
seconds = expectNum(" ");
expect("GMT");
}
expectEnd();
TimeZone utc = new TimeZone.utc();
return new Date.withTimeZone(
year, month + 1, day, hours, minutes, seconds, 0, utc);
}
}

View file

@ -0,0 +1,91 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#source("../../../../runtime/bin/http_utils.dart");
class HttpException implements Exception {
const HttpException([String this.message = ""]);
String toString() => "HttpException: $message";
final String message;
}
void testParseHttpDate() {
TimeZone utc = new TimeZone.utc();
Date date;
date = new Date.withTimeZone(1999, Date.JUN, 11, 18, 46, 53, 0, utc);
Expect.equals(date, _HttpUtils.parseDate("Fri, 11 Jun 1999 18:46:53 GMT"));
Expect.equals(date, _HttpUtils.parseDate("Friday, 11-Jun-1999 18:46:53 GMT"));
Expect.equals(date, _HttpUtils.parseDate("Fri Jun 11 18:46:53 1999"));
date = new Date.withTimeZone(1970, Date.JAN, 1, 0, 0, 0, 0, utc);
Expect.equals(date, _HttpUtils.parseDate("Thu, 1 Jan 1970 00:00:00 GMT"));
Expect.equals(date,
_HttpUtils.parseDate("Thursday, 1-Jan-1970 00:00:00 GMT"));
Expect.equals(date, _HttpUtils.parseDate("Thu Jan 1 00:00:00 1970"));
date = new Date.withTimeZone(2012, Date.MAR, 5, 23, 59, 59, 0, utc);
Expect.equals(date, _HttpUtils.parseDate("Mon, 5 Mar 2012 23:59:59 GMT"));
Expect.equals(date, _HttpUtils.parseDate("Monday, 5-Mar-2012 23:59:59 GMT"));
Expect.equals(date, _HttpUtils.parseDate("Mon Mar 5 23:59:59 2012"));
}
void testFormatParseHttpDate() {
test(int year,
int month,
int day,
int hours,
int minutes,
int seconds,
String expectedFormatted) {
TimeZone utc = new TimeZone.utc();
Date date;
String formatted;
date = new Date.withTimeZone(
year, month, day, hours, minutes, seconds, 0, utc);
formatted = _HttpUtils.formatDate(date);
Expect.equals(expectedFormatted, formatted);
Expect.equals(date, _HttpUtils.parseDate(formatted));
}
test(1999, Date.JUN, 11, 18, 46, 53, "Fri, 11 Jun 1999 18:46:53 GMT");
test(1970, Date.JAN, 1, 0, 0, 0, "Thu, 1 Jan 1970 00:00:00 GMT");
test(2012, Date.MAR, 5, 23, 59, 59, "Mon, 5 Mar 2012 23:59:59 GMT");
}
void testParseHttpDateFailures() {
Expect.throws(() {
_HttpUtils.parseDate("");
});
String valid = "Mon, 5 Mar 2012 23:59:59 GMT";
for (int i = 1; i < valid.length - 1; i++) {
String tmp = valid.substring(0, i);
Expect.throws(() {
_HttpUtils.parseDate(tmp);
});
Expect.throws(() {
_HttpUtils.parseDate(" $tmp");
});
Expect.throws(() {
_HttpUtils.parseDate(" $tmp ");
});
Expect.throws(() {
_HttpUtils.parseDate("$tmp ");
});
}
Expect.throws(() {
_HttpUtils.parseDate(" $valid");
});
Expect.throws(() {
_HttpUtils.parseDate(" $valid ");
});
Expect.throws(() {
_HttpUtils.parseDate("$valid ");
});
}
void main() {
testParseHttpDate();
testFormatParseHttpDate();
testParseHttpDateFailures();
}

View file

@ -124,13 +124,33 @@ class TestServer extends Isolate {
response.outputStream.close();
}
// Return a 301 with a custom reason phrase.
// Check the "Host" header.
void _hostHandler(HttpRequest request, HttpResponse response) {
Expect.equals("www.dartlang.org:1234", request.headers["host"]);
response.statusCode = HttpStatus.OK;
response.outputStream.close();
}
// Set the "Expires" header using the expires property.
void _expires1Handler(HttpRequest request, HttpResponse response) {
Date date =
new Date.withTimeZone(
1999, Date.JUN, 11, 18, 46, 53, 0, new TimeZone.utc());
response.expires = date;
Expect.equals(date, response.expires);
response.outputStream.close();
}
// Set the "Expires" header.
void _expires2Handler(HttpRequest request, HttpResponse response) {
response.setHeader("Expires", "Fri, 11 Jun 1999 18:46:53 GMT");
Date date =
new Date.withTimeZone(
1999, Date.JUN, 11, 18, 46, 53, 0, new TimeZone.utc());
Expect.equals(date, response.expires);
response.outputStream.close();
}
void main() {
// Setup request handlers.
_requestHandlers = new Map();
@ -149,6 +169,14 @@ class TestServer extends Isolate {
(HttpRequest request, HttpResponse response) {
_hostHandler(request, response);
};
_requestHandlers["/expires1"] =
(HttpRequest request, HttpResponse response) {
_expires1Handler(request, response);
};
_requestHandlers["/expires2"] =
(HttpRequest request, HttpResponse response) {
_expires2Handler(request, response);
};
this.port.receive((var message, SendPort replyTo) {
if (message.isStart) {
@ -445,6 +473,39 @@ void testHost() {
testServerMain.start();
}
void testExpires() {
TestServerMain testServerMain = new TestServerMain();
testServerMain.setServerStartedHandler((int port) {
int responses = 0;
HttpClient httpClient = new HttpClient();
void processResponse(HttpClientResponse response) {
Expect.equals(HttpStatus.OK, response.statusCode);
Expect.equals("Fri, 11 Jun 1999 18:46:53 GMT",
response.headers["expires"]);
Expect.equals(
new Date.withTimeZone(
1999, Date.JUN, 11, 18, 46, 53, 0, new TimeZone.utc()),
response.expires);
responses++;
if (responses == 2) {
httpClient.shutdown();
testServerMain.shutdown();
}
}
HttpClientConnection conn1 = httpClient.get("127.0.0.1", port, "/expires1");
conn1.onResponse = (HttpClientResponse response) {
processResponse(response);
};
HttpClientConnection conn2 = httpClient.get("127.0.0.1", port, "/expires2");
conn2.onResponse = (HttpClientResponse response) {
processResponse(response);
};
});
testServerMain.start();
}
void main() {
testStartStop();
@ -458,4 +519,5 @@ void main() {
test404();
testReasonPhrase();
testHost();
testExpires();
}