diff --git a/server/core/maxscale/http.hh b/server/core/maxscale/http.hh index c529d1f4a..1632827be 100644 --- a/server/core/maxscale/http.hh +++ b/server/core/maxscale/http.hh @@ -15,6 +15,7 @@ #include #include +#include #include @@ -48,7 +49,41 @@ static inline std::string http_to_date(time_t t) char buf[200]; // Enough to store all dates gmtime_r(&t, &tm); - strftime(buf, sizeof(buf), "%a, %d %b %y %T GMT", &tm); + strftime(buf, sizeof(buf), "%a, %d %b %Y %T GMT", &tm); return std::string(buf); } + +/** + * @brief Convert a HTTP-date string into time_t + * + * @param str HTTP-date formatted string to convert + * + * @return The time converted to time_t + */ +static inline time_t http_from_date(const std::string& str) +{ + struct tm tm = {}; + + /** First get the GMT time in time_t format */ + strptime(str.c_str(), "%a, %d %b %Y %T GMT", &tm); + time_t t = mktime(&tm); + + /** Then convert it to local time by calculating the difference between + * the local time and the GMT time */ + struct tm local_tm = {}; + struct tm gmt_tm = {}; + time_t epoch = 0; + + /** Call tzset() for the sake of portability */ + tzset(); + gmtime_r(&epoch, &gmt_tm); + localtime_r(&epoch, &local_tm); + + time_t gmt_t = mktime(&gmt_tm); + time_t local_t = mktime(&local_tm); + + /** The value of `(gmt_t - local_t)` will be the number of seconds west + * from GMT. For timezones east of GMT, it will be negative. */ + return t - (gmt_t - local_t); +} diff --git a/server/core/resource.cc b/server/core/resource.cc index e68262a38..d8cc88302 100644 --- a/server/core/resource.cc +++ b/server/core/resource.cc @@ -683,38 +683,89 @@ static bool request_reads_data(const string& verb) verb == MHD_HTTP_METHOD_HEAD; } +bool request_precondition_met(const HttpRequest& request, HttpResponse& response) +{ + bool rval = true; + string str; + const string& uri = request.get_uri(); + + if ((str = request.get_header(MHD_HTTP_HEADER_IF_MODIFIED_SINCE)).length()) + { + if (watcher.last_modified(uri) <= http_from_date(str)) + { + rval = false; + response = HttpResponse(MHD_HTTP_NOT_MODIFIED); + } + } + else if ((str = request.get_header(MHD_HTTP_HEADER_IF_UNMODIFIED_SINCE)).length()) + { + if (watcher.last_modified(uri) > http_from_date(str)) + { + rval = false; + response = HttpResponse(MHD_HTTP_PRECONDITION_FAILED); + } + } + else if ((str = request.get_header(MHD_HTTP_HEADER_IF_MATCH)).length()) + { + str = str.substr(1, str.length() - 2); + + if (watcher.etag(uri) != strtol(str.c_str(), NULL, 10)) + { + rval = false; + response = HttpResponse(MHD_HTTP_PRECONDITION_FAILED); + } + } + else if ((str = request.get_header(MHD_HTTP_HEADER_IF_NONE_MATCH)).length()) + { + str = str.substr(1, str.length() - 2); + + if (watcher.etag(uri) == strtol(str.c_str(), NULL, 10)) + { + rval = false; + response = HttpResponse(MHD_HTTP_NOT_MODIFIED); + } + } + + return rval; +} + HttpResponse resource_handle_request(const HttpRequest& request) { MXS_DEBUG("%s %s %s", request.get_verb().c_str(), request.get_uri().c_str(), request.get_json_str().c_str()); SpinLockGuard guard(resource_lock); - HttpResponse rval = resources.process_request(request); + HttpResponse rval; - if (request_modifies_data(request.get_verb())) + if (request_precondition_met(request, rval)) { - switch (rval.get_code()) + rval = resources.process_request(request); + + if (request_modifies_data(request.get_verb())) { - case MHD_HTTP_OK: - case MHD_HTTP_NO_CONTENT: - case MHD_HTTP_CREATED: - watcher.modify(request.get_uri()); - break; + 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; + default: + break; + } } - } - else if (request_reads_data(request.get_verb())) - { - const string& uri = request.get_uri(); + 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))); + 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()); + stringstream ss; + ss << "\"" << watcher.etag(uri) << "\""; + rval.add_header(HTTP_RESPONSE_HEADER_ETAG, ss.str()); + } } return rval; diff --git a/server/core/test/CMakeLists.txt b/server/core/test/CMakeLists.txt index c2c674acc..e265a3cc8 100644 --- a/server/core/test/CMakeLists.txt +++ b/server/core/test/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(testmodulecmd testmodulecmd.cc) add_executable(testconfig testconfig.cc) add_executable(trxboundaryparser_profile trxboundaryparser_profile.cc) add_executable(testjson testjson.cc) +add_executable(testhttp testhttp.cc) target_link_libraries(test_atomic maxscale-common) target_link_libraries(test_adminusers maxscale-common) target_link_libraries(test_buffer maxscale-common) @@ -50,6 +51,7 @@ target_link_libraries(testmodulecmd maxscale-common) target_link_libraries(testconfig maxscale-common) target_link_libraries(trxboundaryparser_profile maxscale-common) target_link_libraries(testjson maxscale-common) +target_link_libraries(testhttp maxscale-common) add_test(TestAtomic test_atomic) add_test(TestAdminUsers test_adminusers) add_test(TestBuffer test_buffer) @@ -82,6 +84,7 @@ add_test(TestTrxCompare_Set test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../../.. add_test(TestTrxCompare_Update test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../../../query_classifier/test/update.test) add_test(TestTrxCompare_MaxScale test_trxcompare ${CMAKE_CURRENT_SOURCE_DIR}/../../../query_classifier/test/maxscale.test) add_test(TestJson testjson) +add_test(TestHttp testhttp) # This test requires external dependencies and thus cannot be run # as a part of the core test set diff --git a/server/core/test/rest-api/test/http.js b/server/core/test/rest-api/test/http.js index c919cc8f1..8287e5826 100644 --- a/server/core/test/rest-api/test/http.js +++ b/server/core/test/rest-api/test/http.js @@ -7,7 +7,7 @@ describe("HTTP Headers", function() { 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") + 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}) @@ -16,14 +16,14 @@ describe("HTTP Headers", function() { return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true}) }) .then(function(resp) { - resp.headers.etag.should.be.equal("1") + resp.headers.etag.should.be.equal("\"1\"") }) }); - it("Last-Modified changes after modification", function() { + it("Last-Modified changes after modification", function(done) { var date; - return request.get(base_url + "/servers/server1", {resolveWithFullResponse: true}) + request.get(base_url + "/servers/server1", {resolveWithFullResponse: true}) .then(function(resp) { // Store the current modification time @@ -49,11 +49,78 @@ describe("HTTP Headers", function() { .then(function(resp) { resp.headers["last-modified"].should.not.be.null resp.headers["last-modified"].should.not.be.equal(date) + done() + }) + .catch(function(e) { + done(e) }) - }, 3000) + }, 2000) + }) + .catch(function(e) { + done(e) }) }); + var oldtime = new Date(new Date().getTime() - 1000000).toUTCString(); + var newtime = new Date(new Date().getTime() + 1000000).toUTCString(); + + it("request with older If-Modified-Since value", function() { + return request.get(base_url + "/servers/server1", { + headers: { "If-Modified-Since": oldtime} + }).should.be.fulfilled + }) + + it("request with newer If-Modified-Since value", function() { + return request.get(base_url + "/servers/server1", { + resolveWithFullResponse: true, + headers: { "If-Modified-Since": newtime } + }).should.be.rejected + }) + + it("request with older If-Unmodified-Since value", function() { + return request.get(base_url + "/servers/server1", { + headers: { "If-Unmodified-Since": oldtime} + }).should.be.rejected + }) + + it("request with newer If-Unmodified-Since value", function() { + return request.get(base_url + "/servers/server1", { + headers: { "If-Unmodified-Since": newtime} + }).should.be.fulfilled + }) + + it("request with mismatching If-Match value", function() { + return request.get(base_url + "/servers/server1", { + headers: { "If-Match": "\"0\""} + }).should.be.rejected + }) + + it("request with matching If-Match value", function() { + return request.get(base_url + "/servers/server1", { resolveWithFullResponse: true }) + .then(function(resp) { + return request.get(base_url + "/servers/server1", { + headers: { "If-Match": resp.headers["etag"]} + }) + }) + .should.be.fulfilled + }) + + it("request with mismatching If-None-Match value", function() { + return request.get(base_url + "/servers/server1", { + headers: { "If-None-Match": "\"0\""} + }).should.be.fulfilled + }) + + it("request with matching If-None-Match value", function() { + return request.get(base_url + "/servers/server1", { resolveWithFullResponse: true }) + .then(function(resp) { + return request.get(base_url + "/servers/server1", { + headers: { "If-None-Match": resp.headers["etag"]} + }) + }) + .should.be.rejected + }) + after(stopMaxScale) }); diff --git a/server/core/test/testhttp.cc b/server/core/test/testhttp.cc new file mode 100644 index 000000000..04e439459 --- /dev/null +++ b/server/core/test/testhttp.cc @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl11. + * + * Change Date: 2019-07-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ + +#include + +#include +#include "../maxscale/http.hh" + +using std::string; +using std::cout; +using std::endl; + +int main(int argc, char** argv) +{ + time_t now = time(NULL); + string date = http_to_date(now); + time_t converted_now = http_from_date(date); + string converted_date = http_to_date(converted_now); + + cout << "Current linux time: " << now << endl; + cout << "HTTP-date from current time: " << date << endl; + cout << "Converted Linux time: " << converted_now << endl; + cout << "Converted HTTP-date: " << converted_date << endl; + + ss_dassert(now == converted_now); + ss_dassert(date == converted_date); + + return 0; +}