2021-12-03 17:58:48 +01:00

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;