MXS-1220: Add support for conditional HTTP requests
The REST API now supports the If-Modified-Since, If-Unmodified-Since, If-Match and If-None-Match headers and returns the correct response if the conditional fails. Added tests for the date parsing and expanded the HTTP header tests in the REST API test suite.
This commit is contained in:
parent
3deb497394
commit
778631a860
@ -15,6 +15,7 @@
|
||||
#include <maxscale/cppdefs.hh>
|
||||
|
||||
#include <string>
|
||||
#include <time.h>
|
||||
|
||||
#include <maxscale/debug.h>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
});
|
||||
|
39
server/core/test/testhttp.cc
Normal file
39
server/core/test/testhttp.cc
Normal file
@ -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 <iostream>
|
||||
|
||||
#include <maxscale/debug.h>
|
||||
#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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user