Skip to content

Commit

Permalink
Better conditional request support
Browse files Browse the repository at this point in the history
Adds support for date comparisons, correct ETag support, and maybe more.
  • Loading branch information
awwright committed Oct 24, 2019
1 parent eaaf200 commit ff66ad5
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 29 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -13,6 +13,7 @@ A prototype HTTP application framework using URI Templates and streams for looki
* 304 Not Modified
* 404 Not Found
* 405 Method Not Allowed
* 412 Precondition Failed
* 501 Not Implemented
* Allow (list of permitted methods)
* Content-Location
Expand Down
66 changes: 38 additions & 28 deletions lib/Application.js
Expand Up @@ -4,6 +4,7 @@ var Route = require('./Route.js').Route;
var RouteURITemplate = require('./RouteURITemplate.js').RouteURITemplate;
var TraceResource = require('./Resource.js').TraceResource;
var inherits = require('util').inherits;
var compareHTTPDateSince = require('./http-date.js').compareHTTPDateSince;

module.exports.Application = Application;
inherits(Application, Route);
Expand Down Expand Up @@ -159,36 +160,45 @@ Application.prototype.handleRequest = function handleRequest(req, res){
res.setHeader('Last-Modified', resource.lastModified.toUTCString());
}
if(resource.ETag){
res.setHeader('ETag', resource.ETag.toString());
res.setHeader('ETag', '"'+resource.ETag.toString()+'"');
}
// See if this response needs a 304 Not Modified response
// TODO use req.methodSafe, a shortcut for declaring future safe methods, I guess
if(req.method==='GET' || req.method==='HEAD' || req.methodSafe){
if(req.headers['if-none-match']){
if(req.headers['if-none-match']==resource.ETag.toString()){
// TODO check for resource.routePreconditionFailed, and use it if it exists
res.statusCode = 304;
// These headers must remain the same:
// Cache-Control, Content-Location, Date, ETag, Expires, Vary
// res.deleteHeader('Content-Length');
// res.deleteHeader('Content-Type');
// res.deleteHeader('Transfer-Encoding');
res.end();
return;
}
// TODO figure out if this Resource is the origin server or not
if(req.headers['if-match'] && resource.ETag){
// TODO handle multiple If-Match items
if(req.headers['if-match']==='*'){
// TODO do send this if the resource is not persisted yet
//return void preconditionFail();
}else if(req.headers['if-match'] !== '"'+resource.ETag+'"'){
return void preconditionFail();
}
if(req.headers['if-modified-since']){
if(req.headers['if-modified-since']==resource.lastModified.toUTCString()){
// TODO check for resource.routePreconditionFailed, and use it if it exists
res.statusCode = 304;
// These headers must remain the same:
// Cache-Control, Content-Location, Date, ETag, Expires, Vary
// res.deleteHeader('Content-Length');
// res.deleteHeader('Content-Type');
// res.deleteHeader('Transfer-Encoding');
res.end();
return;
}
}else if(req.headers['if-unmodified-since'] && resource.lastModified){
if(compareHTTPDateSince(req.headers['if-unmodified-since'], resource.lastModified) === true){
return void preconditionFail();
}
}
if(req.headers['if-none-match'] && resource.ETag){
// TODO handle multiple If-Match items
if(req.headers['if-none-match'] === '*'){
// TODO don't send this if the resource is not persisted yet
return void preconditionFail();
}else if(req.headers['if-none-match'] === '"'+resource.ETag+'"'){
return void preconditionFail();
}
}else if(req.headers['if-modified-since'] && resource.lastModified){
if(compareHTTPDateSince(req.headers['if-modified-since'], resource.lastModified) === false){
return void preconditionFail();
}
}
function preconditionFail(){
// TODO ensure these headers remain the same:
// Cache-Control, Content-Location, Date, ETag, Expires, Vary
if(req.method==='GET' || req.method==='HEAD'){
res.statusCode = 304; // Not Modified
res.end();
}else{
res.statusCode = 412; // Precondition Failed
// TODO: Allow Resource to define custom handling for this situation
res.end();
}
}
var stream;
Expand Down
74 changes: 74 additions & 0 deletions lib/http-date.js
@@ -0,0 +1,74 @@
'use strict';

// String#match will return an array with:
// [full, day_name, day, month_name, year, hour, minute, second]
const IMF_fixdate = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/;

// [full, day_name, day, month_name, year, hour, minute, second]
const rfc850_date = /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{2}) (\d\d):(\d\d):(\d\d) GMT$/;

// [full, day_name, month_name, day, hour, minute, second, year]
const asctime_date = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([ 0-9][0-9]) (\d\d):(\d\d):(\d\d) (\d{4})$/;

const Months0 = {
Jan: 0,
Feb: 1,
Mar: 2,
Apr: 3,
May: 4,
Jun: 5,
Jul: 6,
Aug: 7,
Sep: 8,
Oct: 9,
Nov: 10,
Dec: 11,
}

module.exports.parseHTTPDate = parseHTTPDate;
function parseHTTPDate(str){
if(typeof str !== 'string') return null;
{
const m = IMF_fixdate.exec(str);
if(m) return [parseInt(m[4]), Months0[m[3]], parseInt(m[2]), parseInt(m[5]), parseInt(m[6]), parseInt(m[7])];
}
{
const m = rfc850_date.exec(str);
if(m) return [parseInt(m[4]), Months0[m[3]], parseInt(m[2]), parseInt(m[5]), parseInt(m[6]), parseInt(m[7])];
}
{
const m = asctime_date.exec(str);
if(m) return [parseInt(m[7]), Months0[m[2]], parseInt(m[3]), parseInt(m[4]), parseInt(m[5]), parseInt(m[6])];
}
return null;
}

// Determine if `b` is an HTTP-date that occurs after `a`
module.exports.compareHTTPDateSince = compareHTTPDateSince;
function compareHTTPDateSince(a, b){
if(typeof a !== 'string') return null;
if(!b) return null;
const am = parseHTTPDate(a);
if(am===null) return null;
const bm = [
b.getUTCFullYear(),
b.getUTCMonth(),
b.getUTCDate(),
b.getUTCHours(),
b.getUTCMinutes(),
b.getUTCSeconds(),
];
if(bm[0] > am[0]) return true;
if(bm[0] < am[0]) return false;
if(bm[1] > am[1]) return true;
if(bm[1] < am[1]) return false;
if(bm[2] > am[2]) return true;
if(bm[2] < am[2]) return false;
if(bm[3] > am[3]) return true;
if(bm[3] < am[3]) return false;
if(bm[4] > am[4]) return true;
if(bm[4] < am[4]) return false;
if(bm[5] > am[5]) return true;
if(bm[5] < am[5]) return false;
return false;
}
2 changes: 1 addition & 1 deletion test/RouteStaticFile.test.js
Expand Up @@ -258,7 +258,7 @@ describe('RouteStaticFile', function(){
'Host: example.com',
]).then(function(res){
assert(res.toString().match(/^HTTP\/1.1 200 /));
var m = res.toString().match(/^ETag:\s+(.*)$/im);
var m = res.toString().match(/^ETag:\s+(".*")$/im);
assert(m);
return testMessage(server, [
'GET /data-table.html HTTP/1.1',
Expand Down
19 changes: 19 additions & 0 deletions test/http-date.test.js
@@ -0,0 +1,19 @@
'use strict';

const assert = require('assert').strict;
const compare = require('../lib/http-date.js').compareHTTPDateSince;

describe('Unit: HTTP-date parsing', function(){
it('bad form 1', function(){
assert.equal(compare('Tue, 15 Nnn 1994 08:12:31 GMT', undefined), null);
});
it('form 1, earlier', function(){
assert.equal(compare('Tue, 15 Nov 1994 08:12:31 GMT', new Date(784887150000)), false);
});
it('form 1, equal', function(){
assert.equal(compare('Tue, 15 Nov 1994 08:12:31 GMT', new Date(784887151000)), false);
});
it('form 1, later', function(){
assert.equal(compare('Tue, 15 Nov 1994 08:12:31 GMT', new Date(784887152000)), true);
});
});

0 comments on commit ff66ad5

Please sign in to comment.