MXS-1220: Add Last-Modified and ETag headers
The resource system now tracks both the time when a resource was last modified and the revision number of the resource. This allows working Last-Modified and ETag headers to be generated by the REST API. The If-Modified-Since and If-None-Match request headers are not yet processed and using them will always return the resource instead of a 304 Not Modified response.
This commit is contained in:
@ -30,9 +30,6 @@ HttpResponse::HttpResponse(int code, json_t* response):
|
|||||||
{
|
{
|
||||||
string http_date = http_get_date();
|
string http_date = http_get_date();
|
||||||
add_header(HTTP_RESPONSE_HEADER_DATE, http_date);
|
add_header(HTTP_RESPONSE_HEADER_DATE, http_date);
|
||||||
add_header(HTTP_RESPONSE_HEADER_LAST_MODIFIED, http_date);
|
|
||||||
// This ETag is the base64 encoding of `not-yet-implemented`
|
|
||||||
add_header(HTTP_RESPONSE_HEADER_ETAG, "bm90LXlldC1pbXBsZW1lbnRlZAo");
|
|
||||||
|
|
||||||
if (m_body)
|
if (m_body)
|
||||||
{
|
{
|
||||||
|
@ -62,8 +62,8 @@ private:
|
|||||||
|
|
||||||
bool matching_variable_path(const std::string& path, const std::string& target) const;
|
bool matching_variable_path(const std::string& path, const std::string& target) const;
|
||||||
|
|
||||||
ResourceCallback m_cb; /**< Resource handler callback */
|
ResourceCallback m_cb; /**< Resource handler callback */
|
||||||
std::deque<std::string> m_path; /**< Path components */
|
std::deque<std::string> m_path; /**< Path components */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
#include <maxscale/alloc.h>
|
#include <maxscale/alloc.h>
|
||||||
#include <maxscale/jansson.hh>
|
#include <maxscale/jansson.hh>
|
||||||
@ -29,13 +30,76 @@
|
|||||||
#include "maxscale/config_runtime.h"
|
#include "maxscale/config_runtime.h"
|
||||||
#include "maxscale/modules.h"
|
#include "maxscale/modules.h"
|
||||||
#include "maxscale/worker.h"
|
#include "maxscale/worker.h"
|
||||||
|
#include "maxscale/http.hh"
|
||||||
|
|
||||||
using std::list;
|
using std::list;
|
||||||
|
using std::map;
|
||||||
using std::string;
|
using std::string;
|
||||||
using std::stringstream;
|
using std::stringstream;
|
||||||
using mxs::SpinLock;
|
using mxs::SpinLock;
|
||||||
using mxs::SpinLockGuard;
|
using mxs::SpinLockGuard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that keeps track of resource modification times
|
||||||
|
*/
|
||||||
|
class ResourceWatcher
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
ResourceWatcher() :
|
||||||
|
m_init(time(NULL))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void modify(const string& path)
|
||||||
|
{
|
||||||
|
map<string, uint64_t>::iterator it = m_etag.find(path);
|
||||||
|
|
||||||
|
if (it != m_etag.end())
|
||||||
|
{
|
||||||
|
it->second++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// First modification
|
||||||
|
m_etag[path] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_last_modified[path] = time(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t last_modified(const string& path) const
|
||||||
|
{
|
||||||
|
map<string, time_t>::const_iterator it = m_last_modified.find(path);
|
||||||
|
|
||||||
|
if (it != m_last_modified.end())
|
||||||
|
{
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource has not yet been updated
|
||||||
|
return m_init;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t etag(const string& path) const
|
||||||
|
{
|
||||||
|
map<string, uint64_t>::const_iterator it = m_etag.find(path);
|
||||||
|
|
||||||
|
if (it != m_etag.end())
|
||||||
|
{
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource has not yet been updated
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
time_t m_init;
|
||||||
|
map<string, time_t> m_last_modified;
|
||||||
|
map<string, uint64_t> m_etag;
|
||||||
|
};
|
||||||
|
|
||||||
Resource::Resource(ResourceCallback cb, int components, ...) :
|
Resource::Resource(ResourceCallback cb, int components, ...) :
|
||||||
m_cb(cb)
|
m_cb(cb)
|
||||||
{
|
{
|
||||||
@ -603,12 +667,55 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
static RootResource resources; /**< Core resource set */
|
static RootResource resources; /**< Core resource set */
|
||||||
|
static ResourceWatcher watcher; /**< Modification watcher */
|
||||||
static SpinLock resource_lock;
|
static SpinLock resource_lock;
|
||||||
|
|
||||||
|
static bool request_modifies_data(const string& verb)
|
||||||
|
{
|
||||||
|
return verb == MHD_HTTP_METHOD_POST ||
|
||||||
|
verb == MHD_HTTP_METHOD_PUT ||
|
||||||
|
verb == MHD_HTTP_METHOD_DELETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool request_reads_data(const string& verb)
|
||||||
|
{
|
||||||
|
return verb == MHD_HTTP_METHOD_GET ||
|
||||||
|
verb == MHD_HTTP_METHOD_HEAD;
|
||||||
|
}
|
||||||
|
|
||||||
HttpResponse resource_handle_request(const HttpRequest& request)
|
HttpResponse resource_handle_request(const HttpRequest& request)
|
||||||
{
|
{
|
||||||
SpinLockGuard guard(resource_lock);
|
|
||||||
MXS_DEBUG("%s %s %s", request.get_verb().c_str(), request.get_uri().c_str(),
|
MXS_DEBUG("%s %s %s", request.get_verb().c_str(), request.get_uri().c_str(),
|
||||||
request.get_json_str().c_str());
|
request.get_json_str().c_str());
|
||||||
return resources.process_request(request);
|
|
||||||
|
SpinLockGuard guard(resource_lock);
|
||||||
|
HttpResponse rval = resources.process_request(request);
|
||||||
|
|
||||||
|
if (request_modifies_data(request.get_verb()))
|
||||||
|
{
|
||||||
|
switch (rval.get_code())
|
||||||
|
{
|
||||||
|
case MHD_HTTP_OK:
|
||||||
|
case MHD_HTTP_NO_CONTENT:
|
||||||
|
case MHD_HTTP_CREATED:
|
||||||
|
watcher.modify(request.get_uri());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (request_reads_data(request.get_verb()))
|
||||||
|
{
|
||||||
|
const string& uri = request.get_uri();
|
||||||
|
|
||||||
|
rval.add_header(HTTP_RESPONSE_HEADER_LAST_MODIFIED,
|
||||||
|
http_to_date(watcher.last_modified(uri)));
|
||||||
|
|
||||||
|
stringstream ss;
|
||||||
|
ss << watcher.etag(uri);
|
||||||
|
rval.add_header(HTTP_RESPONSE_HEADER_ETAG, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return rval;
|
||||||
}
|
}
|
||||||
|
59
server/core/test/rest-api/test/http.js
Normal file
59
server/core/test/rest-api/test/http.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
require("../utils.js")()
|
||||||
|
|
||||||
|
|
||||||
|
describe("HTTP Headers", function() {
|
||||||
|
before(startMaxScale)
|
||||||
|
|
||||||
|
it("ETag changes after modification", function() {
|
||||||
|
return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true})
|
||||||
|
.then(function(resp) {
|
||||||
|
resp.headers.etag.should.be.equal("0")
|
||||||
|
var srv = JSON.parse(resp.body)
|
||||||
|
delete srv.data.relationships
|
||||||
|
return request.put(base_url + "/servers/server1", {json: srv})
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true})
|
||||||
|
})
|
||||||
|
.then(function(resp) {
|
||||||
|
resp.headers.etag.should.be.equal("1")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Last-Modified changes after modification", function() {
|
||||||
|
var date;
|
||||||
|
|
||||||
|
return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true})
|
||||||
|
.then(function(resp) {
|
||||||
|
|
||||||
|
// Store the current modification time
|
||||||
|
resp.headers["last-modified"].should.not.be.null
|
||||||
|
date = resp.headers["last-modified"]
|
||||||
|
|
||||||
|
// Modify resource after three seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
var srv = JSON.parse(resp.body)
|
||||||
|
|
||||||
|
srv.data.relationships = {
|
||||||
|
services: {
|
||||||
|
data: [
|
||||||
|
{id: "RW-Split-Router", type: "services"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.put(base_url + "/servers/server1", {json: srv})
|
||||||
|
.then(function() {
|
||||||
|
return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true})
|
||||||
|
})
|
||||||
|
.then(function(resp) {
|
||||||
|
resp.headers["last-modified"].should.not.be.null
|
||||||
|
resp.headers["last-modified"].should.not.be.equal(date)
|
||||||
|
})
|
||||||
|
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
after(stopMaxScale)
|
||||||
|
});
|
Reference in New Issue
Block a user