diff --git a/server/core/httpresponse.cc b/server/core/httpresponse.cc index 8708d7c11..557ce4c8e 100644 --- a/server/core/httpresponse.cc +++ b/server/core/httpresponse.cc @@ -30,9 +30,6 @@ HttpResponse::HttpResponse(int code, json_t* response): { string http_date = http_get_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) { diff --git a/server/core/maxscale/resource.hh b/server/core/maxscale/resource.hh index b3e80498d..c92c77611 100644 --- a/server/core/maxscale/resource.hh +++ b/server/core/maxscale/resource.hh @@ -62,8 +62,8 @@ private: bool matching_variable_path(const std::string& path, const std::string& target) const; - ResourceCallback m_cb; /**< Resource handler callback */ - std::deque m_path; /**< Path components */ + ResourceCallback m_cb; /**< Resource handler callback */ + std::deque m_path; /**< Path components */ }; /** diff --git a/server/core/resource.cc b/server/core/resource.cc index bf51ebc05..e68262a38 100644 --- a/server/core/resource.cc +++ b/server/core/resource.cc @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -29,13 +30,76 @@ #include "maxscale/config_runtime.h" #include "maxscale/modules.h" #include "maxscale/worker.h" +#include "maxscale/http.hh" using std::list; +using std::map; using std::string; using std::stringstream; using mxs::SpinLock; 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::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::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::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 m_last_modified; + map m_etag; +}; + Resource::Resource(ResourceCallback cb, int components, ...) : m_cb(cb) { @@ -603,12 +667,55 @@ private: }; static RootResource resources; /**< Core resource set */ +static ResourceWatcher watcher; /**< Modification watcher */ 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) { - SpinLockGuard guard(resource_lock); MXS_DEBUG("%s %s %s", request.get_verb().c_str(), request.get_uri().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; } diff --git a/server/core/test/rest-api/test/http.js b/server/core/test/rest-api/test/http.js new file mode 100644 index 000000000..c919cc8f1 --- /dev/null +++ b/server/core/test/rest-api/test/http.js @@ -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) +});