222 lines
5.6 KiB
JavaScript
222 lines
5.6 KiB
JavaScript
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
|
|
|
|
'use strict';
|
|
|
|
var errors = require('restify-errors');
|
|
|
|
///--- Globals
|
|
|
|
var BadRequestError = errors.BadRequestError;
|
|
var PreconditionFailedError = errors.PreconditionFailedError;
|
|
|
|
var IF_MATCH_FAIL = "if-match '%s' didn't match etag '%s'";
|
|
var IF_NO_MATCH_FAIL = "if-none-match '%s' matched etag '%s'";
|
|
var IF_MOD_FAIL = "object was modified at '%s'; if-modified-since '%s'";
|
|
var IF_UNMOD_FAIL = "object was modified at '%s'; if-unmodified-since '%s'";
|
|
|
|
///--- API
|
|
// Reference RFC2616 section 14 for an explanation of what this all does.
|
|
|
|
function checkIfMatch(req, res, next) {
|
|
var clientETags;
|
|
var cur;
|
|
var etag = res.etag || res.getHeader('etag') || '';
|
|
var ifMatch;
|
|
var matched = false;
|
|
|
|
if ((ifMatch = req.headers['if-match'])) {
|
|
clientETags = ifMatch.split(/\s*,\s*/);
|
|
|
|
for (var i = 0; i < clientETags.length; i++) {
|
|
cur = clientETags[i];
|
|
|
|
// only strong comparison
|
|
|
|
cur = cur.replace(/^W\//, '');
|
|
cur = cur.replace(/^"(\w*)"$/, '$1');
|
|
|
|
if (cur === '*' || cur === etag) {
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!matched) {
|
|
var err = new PreconditionFailedError(IF_MATCH_FAIL, ifMatch, etag);
|
|
return next(err);
|
|
}
|
|
}
|
|
|
|
return next();
|
|
}
|
|
|
|
function checkIfNoneMatch(req, res, next) {
|
|
var clientETags;
|
|
var cur;
|
|
var etag = res.etag || res.getHeader('etag') || '';
|
|
var ifNoneMatch;
|
|
var matched = false;
|
|
|
|
if ((ifNoneMatch = req.headers['if-none-match'])) {
|
|
clientETags = ifNoneMatch.split(/\s*,\s*/);
|
|
|
|
for (var i = 0; i < clientETags.length; i++) {
|
|
cur = clientETags[i];
|
|
|
|
// ignore weak validation
|
|
cur = cur.replace(/^W\//, '');
|
|
cur = cur.replace(/^"(\w*)"$/, '$1');
|
|
|
|
if (cur === '*' || cur === etag) {
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!matched) {
|
|
return next();
|
|
}
|
|
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
var err = new PreconditionFailedError(
|
|
IF_NO_MATCH_FAIL,
|
|
ifNoneMatch,
|
|
etag
|
|
);
|
|
return next(err);
|
|
}
|
|
|
|
res.send(304);
|
|
return next(false);
|
|
}
|
|
|
|
return next();
|
|
}
|
|
|
|
function checkIfModified(req, res, next) {
|
|
var code;
|
|
var err;
|
|
var ctime = req.header('if-modified-since');
|
|
var mtime = res.mtime || res.header('Last-Modified') || '';
|
|
|
|
if (!mtime || !ctime) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
//
|
|
// TODO handle Range header modifications
|
|
//
|
|
// Note: this is not technically correct as per 2616 -
|
|
// 2616 only specifies semantics for GET requests, not
|
|
// any other method - but using if-modified-since with a
|
|
// PUT or DELETE seems like returning 412 is sane
|
|
//
|
|
if (Date.parse(mtime) <= Date.parse(ctime)) {
|
|
switch (req.method) {
|
|
case 'GET':
|
|
case 'HEAD':
|
|
code = 304;
|
|
break;
|
|
|
|
default:
|
|
err = new PreconditionFailedError(
|
|
IF_MOD_FAIL,
|
|
mtime,
|
|
ctime
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
next(new BadRequestError(e.message));
|
|
return;
|
|
}
|
|
|
|
if (code !== undefined) {
|
|
res.send(code);
|
|
next(false);
|
|
return;
|
|
}
|
|
|
|
next(err);
|
|
}
|
|
|
|
function checkIfUnmodified(req, res, next) {
|
|
var err;
|
|
var ctime = req.headers['if-unmodified-since'];
|
|
var mtime = res.mtime || res.header('Last-Modified') || '';
|
|
|
|
if (!mtime || !ctime) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (Date.parse(mtime) > Date.parse(ctime)) {
|
|
err = new PreconditionFailedError(IF_UNMOD_FAIL, mtime, ctime);
|
|
}
|
|
} catch (e) {
|
|
next(new BadRequestError(e.message));
|
|
return;
|
|
}
|
|
|
|
next(err);
|
|
}
|
|
|
|
///--- Exports
|
|
|
|
/**
|
|
* Returns a set of plugins that will compare an already set `ETag` header with
|
|
* the client's `If-Match` and `If-None-Match` header, and an already set
|
|
* Last-Modified header with the client's `If-Modified-Since` and
|
|
* `If-Unmodified-Since` header.
|
|
*
|
|
* You can use this handler to let clients do nice HTTP semantics with the
|
|
* "match" headers. Specifically, with this plugin in place, you would set
|
|
* `res.etag=$yourhashhere`, and then this plugin will do one of:
|
|
*
|
|
* - return `304` (Not Modified) [and stop the handler chain]
|
|
* - return `412` (Precondition Failed) [and stop the handler chain]
|
|
* - Allow the request to go through the handler chain.
|
|
*
|
|
* The specific headers this plugin looks at are:
|
|
*
|
|
* - `Last-Modified`
|
|
* - `If-Match`
|
|
* - `If-None-Match`
|
|
* - `If-Modified-Since`
|
|
* - `If-Unmodified-Since`
|
|
*
|
|
* @public
|
|
* @throws {BadRequestError}
|
|
* @throws {PreconditionFailedError}
|
|
* @function conditionalRequest
|
|
* @returns {Function[]} Handlers
|
|
* @example
|
|
* server.use(restify.plugins.conditionalRequest());
|
|
* @example
|
|
* server.use(function setETag(req, res, next) {
|
|
* res.header('ETag', 'myETag');
|
|
* res.header('Last-Modified', new Date());
|
|
* });
|
|
*
|
|
* server.use(restify.plugins.conditionalRequest());
|
|
*
|
|
* server.get('/hello/:name', function(req, res, next) {
|
|
* res.send('hello ' + req.params.name);
|
|
* });
|
|
*/
|
|
function conditionalRequest() {
|
|
var chain = [
|
|
checkIfMatch,
|
|
checkIfNoneMatch,
|
|
checkIfModified,
|
|
checkIfUnmodified
|
|
];
|
|
return chain;
|
|
}
|
|
|
|
module.exports = conditionalRequest;
|