mirror of https://github.com/kiwix/libkiwix.git
Merge pull request #348 from kiwix/http_head_and_etag
This commit is contained in:
commit
0b25492edc
|
@ -21,6 +21,7 @@ kiwix_sources = [
|
|||
'tools/otherTools.cpp',
|
||||
'kiwixserve.cpp',
|
||||
'name_mapper.cpp',
|
||||
'server/etag.cpp',
|
||||
'server/request_context.cpp',
|
||||
'server/response.cpp'
|
||||
]
|
||||
|
|
|
@ -111,10 +111,11 @@ class InternalServer {
|
|||
bool start();
|
||||
void stop();
|
||||
|
||||
private:
|
||||
private: // functions
|
||||
Response handle_request(const RequestContext& request);
|
||||
Response build_500(const std::string& msg);
|
||||
Response build_404(const RequestContext& request, const std::string& zimName);
|
||||
Response build_304(const RequestContext& request, const ETag& etag) const;
|
||||
Response build_redirect(const std::string& bookName, const kiwix::Entry& entry) const;
|
||||
Response build_homepage(const RequestContext& request);
|
||||
Response handle_skin(const RequestContext& request);
|
||||
|
@ -131,6 +132,8 @@ class InternalServer {
|
|||
Response get_default_response() const;
|
||||
|
||||
std::shared_ptr<Reader> get_reader(const std::string& bookName) const;
|
||||
bool etag_not_needed(const RequestContext& r) const;
|
||||
ETag get_matching_if_none_match_etag(const RequestContext& request) const;
|
||||
|
||||
private: // data
|
||||
std::string m_addr;
|
||||
|
@ -145,6 +148,8 @@ class InternalServer {
|
|||
|
||||
Library* mp_library;
|
||||
NameMapper* mp_nameMapper;
|
||||
|
||||
std::string m_server_id;
|
||||
};
|
||||
|
||||
|
||||
|
@ -252,6 +257,8 @@ bool InternalServer::start() {
|
|||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
auto server_start_time = std::chrono::system_clock::now().time_since_epoch();
|
||||
m_server_id = kiwix::to_string(server_start_time.count());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -301,7 +308,8 @@ int InternalServer::handlerCallback(struct MHD_Connection* connection,
|
|||
}
|
||||
/* Unexpected method */
|
||||
if (request.get_method() != RequestMethod::GET
|
||||
&& request.get_method() != RequestMethod::POST) {
|
||||
&& request.get_method() != RequestMethod::POST
|
||||
&& request.get_method() != RequestMethod::HEAD) {
|
||||
printf("Reject request because of unhandled request method.\n");
|
||||
printf("----------------------\n");
|
||||
return MHD_NO;
|
||||
|
@ -318,6 +326,9 @@ int InternalServer::handlerCallback(struct MHD_Connection* connection,
|
|||
}
|
||||
}
|
||||
|
||||
if (response.getReturnCode() == MHD_HTTP_OK && !etag_not_needed(request))
|
||||
response.set_server_id(m_server_id);
|
||||
|
||||
auto ret = response.send(request, connection);
|
||||
auto end_time = std::chrono::steady_clock::now();
|
||||
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(end_time - start_time);
|
||||
|
@ -328,6 +339,14 @@ int InternalServer::handlerCallback(struct MHD_Connection* connection,
|
|||
return ret;
|
||||
}
|
||||
|
||||
Response InternalServer::build_304(const RequestContext& request, const ETag& etag) const
|
||||
{
|
||||
auto response = get_default_response();
|
||||
response.set_code(MHD_HTTP_NOT_MODIFIED);
|
||||
response.set_etag(etag);
|
||||
response.set_content("");
|
||||
return response;
|
||||
}
|
||||
|
||||
Response InternalServer::handle_request(const RequestContext& request)
|
||||
{
|
||||
|
@ -335,6 +354,10 @@ Response InternalServer::handle_request(const RequestContext& request)
|
|||
if (! request.is_valid_url())
|
||||
return build_404(request, "");
|
||||
|
||||
const ETag etag = get_matching_if_none_match_etag(request);
|
||||
if ( etag )
|
||||
return build_304(request, etag);
|
||||
|
||||
if (kiwix::startsWith(request.get_url(), "/skin/"))
|
||||
return handle_skin(request);
|
||||
|
||||
|
@ -427,6 +450,27 @@ MustacheData InternalServer::homepage_data() const
|
|||
return data;
|
||||
}
|
||||
|
||||
bool InternalServer::etag_not_needed(const RequestContext& request) const
|
||||
{
|
||||
const std::string url = request.get_url();
|
||||
return kiwix::startsWith(url, "/catalog")
|
||||
|| url == "/search"
|
||||
|| url == "/suggest"
|
||||
|| url == "/random"
|
||||
|| url == "/catch/external";
|
||||
}
|
||||
|
||||
ETag
|
||||
InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const
|
||||
{
|
||||
try {
|
||||
const std::string etag_list = r.get_header(MHD_HTTP_HEADER_IF_NONE_MATCH);
|
||||
return ETag::match(etag_list, m_server_id);
|
||||
} catch (const std::out_of_range&) {
|
||||
return ETag();
|
||||
}
|
||||
}
|
||||
|
||||
Response InternalServer::build_homepage(const RequestContext& request)
|
||||
{
|
||||
auto response = get_default_response();
|
||||
|
@ -485,7 +529,7 @@ Response InternalServer::handle_meta(const RequestContext& request)
|
|||
response.set_content(content);
|
||||
response.set_mimeType(mimeType);
|
||||
response.set_compress(false);
|
||||
response.set_cache(true);
|
||||
response.set_cacheable();
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -569,7 +613,7 @@ Response InternalServer::handle_skin(const RequestContext& request)
|
|||
}
|
||||
response.set_mimeType(getMimeTypeForFile(resourceName));
|
||||
response.set_compress(true);
|
||||
response.set_cache(true);
|
||||
response.set_cacheable();
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright 2020 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include "etag.h"
|
||||
|
||||
#include "tools/stringTools.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
namespace {
|
||||
|
||||
// Characters in the options part of the ETag could in principle be picked up
|
||||
// from the latin alphabet in natural order (the character corresponding to
|
||||
// ETag::Option opt would be 'a'+opt; that would somewhat simplify the code in
|
||||
// this file). However it is better to have some mnemonics in the option names,
|
||||
// hence below variable: all_options[opt] corresponds to the character going
|
||||
// into the ETag for ETag::Option opt.
|
||||
// IMPORTANT: The characters in all_options must come in sorted order (so that
|
||||
// IMPORTANT: isValidOptionsString() works correctly).
|
||||
const char all_options[] = "cz";
|
||||
|
||||
static_assert(ETag::OPTION_COUNT == sizeof(all_options) - 1, "");
|
||||
|
||||
bool isValidServerId(const std::string& s)
|
||||
{
|
||||
return !s.empty() && s.find_first_of("\"/") == std::string::npos;
|
||||
}
|
||||
|
||||
bool isSubsequenceOf(const std::string& s, const std::string& sortedString)
|
||||
{
|
||||
std::string::size_type i = 0;
|
||||
for ( const char c : s )
|
||||
{
|
||||
const std::string::size_type j = sortedString.find(c, i);
|
||||
if ( j == std::string::npos )
|
||||
return false;
|
||||
i = j+1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isValidOptionsString(const std::string& s)
|
||||
{
|
||||
return isSubsequenceOf(s, all_options);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
void ETag::set_option(Option opt)
|
||||
{
|
||||
if ( ! get_option(opt) )
|
||||
{
|
||||
m_options.push_back(all_options[opt]);
|
||||
std::sort(m_options.begin(), m_options.end());
|
||||
}
|
||||
}
|
||||
|
||||
bool ETag::get_option(Option opt) const
|
||||
{
|
||||
return m_options.find(all_options[opt]) != std::string::npos;
|
||||
}
|
||||
|
||||
std::string ETag::get_etag() const
|
||||
{
|
||||
if ( m_serverId.empty() )
|
||||
return std::string();
|
||||
|
||||
return "\"" + m_serverId + "/" + m_options + "\"";
|
||||
}
|
||||
|
||||
ETag::ETag(const std::string& serverId, const std::string& options)
|
||||
{
|
||||
if ( isValidServerId(serverId) && isValidOptionsString(options) )
|
||||
{
|
||||
m_serverId = serverId;
|
||||
m_options = options;
|
||||
}
|
||||
}
|
||||
|
||||
ETag ETag::parse(std::string s)
|
||||
{
|
||||
if ( kiwix::startsWith("W/", s) )
|
||||
s = s.substr(2);
|
||||
|
||||
if ( s.front() != '"' || s.back() != '"' )
|
||||
return ETag();
|
||||
|
||||
s = s.substr(1, s.size()-2);
|
||||
|
||||
const std::string::size_type i = s.find('/');
|
||||
if ( i == std::string::npos )
|
||||
return ETag();
|
||||
|
||||
return ETag(s.substr(0, i), s.substr(i+1));
|
||||
}
|
||||
|
||||
ETag ETag::match(const std::string& etags, const std::string& server_id)
|
||||
{
|
||||
std::istringstream ss(etags);
|
||||
std::string etag_str;
|
||||
while ( ss >> etag_str )
|
||||
{
|
||||
if ( etag_str.back() == ',' )
|
||||
etag_str.pop_back();
|
||||
|
||||
const ETag etag = parse(etag_str);
|
||||
if ( etag && etag.m_serverId == server_id )
|
||||
return etag;
|
||||
}
|
||||
|
||||
return ETag();
|
||||
}
|
||||
|
||||
} // namespace kiwix
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright 2020 Veloman Yunkan <veloman.yunkan@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
* MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef KIWIXLIB_SERVER_ETAG_H
|
||||
#define KIWIXLIB_SERVER_ETAG_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace kiwix {
|
||||
|
||||
// The ETag string used by Kiwix server (more precisely, its value inside the
|
||||
// double quotes) consists of two parts:
|
||||
//
|
||||
// 1. ServerId - The string obtained on server start up
|
||||
//
|
||||
// 2. Options - Zero or more characters encoding the values of some of the
|
||||
// headers of the response
|
||||
//
|
||||
// The two parts are separated with a slash (/) symbol (which is always present,
|
||||
// even when the the options part is empty). Neither portion of a Kiwix ETag
|
||||
// may contain the slash symbol.
|
||||
// Examples of valid Kiwix server ETags (including the double quotes):
|
||||
//
|
||||
// "abcdefghijklmn/"
|
||||
// "1234567890/z"
|
||||
// "1234567890/cz"
|
||||
//
|
||||
// The options part of the Kiwix ETag allows to correctly set the required
|
||||
// headers when responding to a conditional If-None-Match request with a 304
|
||||
// (Not Modified) response without following the full code path that would
|
||||
// discover the necessary options.
|
||||
|
||||
class ETag
|
||||
{
|
||||
public: // types
|
||||
enum Option {
|
||||
CACHEABLE_ENTITY,
|
||||
COMPRESSED_CONTENT,
|
||||
OPTION_COUNT
|
||||
};
|
||||
|
||||
public: // functions
|
||||
ETag() {}
|
||||
|
||||
void set_server_id(const std::string& id) { m_serverId = id; }
|
||||
void set_option(Option opt);
|
||||
|
||||
explicit operator bool() const { return !m_serverId.empty(); }
|
||||
|
||||
bool get_option(Option opt) const;
|
||||
std::string get_etag() const;
|
||||
|
||||
|
||||
static ETag match(const std::string& etags, const std::string& server_id);
|
||||
|
||||
private: // functions
|
||||
ETag(const std::string& serverId, const std::string& options);
|
||||
|
||||
static ETag parse(std::string s);
|
||||
|
||||
private: // data
|
||||
std::string m_serverId;
|
||||
std::string m_options;
|
||||
};
|
||||
|
||||
} // namespace kiwix
|
||||
|
||||
#endif // KIWIXLIB_SERVER_ETAG_H
|
|
@ -55,7 +55,6 @@ Response::Response(const std::string& root, bool verbose, bool withTaskbar, bool
|
|||
m_withTaskbar(withTaskbar),
|
||||
m_withLibraryButton(withLibraryButton),
|
||||
m_blockExternalLinks(blockExternalLinks),
|
||||
m_useCache(false),
|
||||
m_addTaskbar(false),
|
||||
m_bookName(""),
|
||||
m_startRange(0),
|
||||
|
@ -168,6 +167,14 @@ void Response::inject_externallinks_blocker()
|
|||
script_tag);
|
||||
}
|
||||
|
||||
bool
|
||||
Response::can_compress(const RequestContext& request) const
|
||||
{
|
||||
return request.can_compress()
|
||||
&& is_compressible_mime_type(m_mimeType)
|
||||
&& (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE);
|
||||
}
|
||||
|
||||
MHD_Response*
|
||||
Response::create_raw_content_mhd_response(const RequestContext& request)
|
||||
{
|
||||
|
@ -178,10 +185,7 @@ Response::create_raw_content_mhd_response(const RequestContext& request)
|
|||
inject_externallinks_blocker();
|
||||
}
|
||||
|
||||
bool shouldCompress = m_compress && request.can_compress();
|
||||
shouldCompress &= is_compressible_mime_type(m_mimeType);
|
||||
shouldCompress &= (m_content.size() > KIWIX_MIN_CONTENT_SIZE_TO_DEFLATE);
|
||||
|
||||
bool shouldCompress = m_compress && can_compress(request);
|
||||
if (shouldCompress) {
|
||||
std::vector<Bytef> compr_buffer(compressBound(m_content.size()));
|
||||
uLongf comprLen = compr_buffer.capacity();
|
||||
|
@ -196,6 +200,7 @@ Response::create_raw_content_mhd_response(const RequestContext& request)
|
|||
It has no incidence on other browsers
|
||||
See http://www.subbu.org/blog/2008/03/ie7-deflate-or-not and comments */
|
||||
m_content = string((char*)&compr_buffer[2], comprLen - 2);
|
||||
m_etag.set_option(ETag::COMPRESSED_CONTENT);
|
||||
} else {
|
||||
shouldCompress = false;
|
||||
}
|
||||
|
@ -204,9 +209,17 @@ Response::create_raw_content_mhd_response(const RequestContext& request)
|
|||
MHD_Response* response = MHD_create_response_from_buffer(
|
||||
m_content.size(), const_cast<char*>(m_content.data()), MHD_RESPMEM_MUST_COPY);
|
||||
|
||||
if (shouldCompress) {
|
||||
// At shis point m_etag.get_option(ETag::COMPRESSED_CONTENT) and
|
||||
// shouldCompress can have different values. This can happen for a 304 (Not
|
||||
// Modified) response generated while handling a conditional If-None-Match
|
||||
// request. In that case the m_etag (together with its COMPRESSED_CONTENT
|
||||
// option) is obtained from the ETag list of the If-None-Match header and the
|
||||
// response has no body (which shouldn't be compressed).
|
||||
if ( m_etag.get_option(ETag::COMPRESSED_CONTENT) ) {
|
||||
MHD_add_response_header(
|
||||
response, MHD_HTTP_HEADER_VARY, "Accept-Encoding");
|
||||
}
|
||||
if (shouldCompress) {
|
||||
MHD_add_response_header(
|
||||
response, MHD_HTTP_HEADER_CONTENT_ENCODING, "deflate");
|
||||
}
|
||||
|
@ -267,7 +280,10 @@ int Response::send(const RequestContext& request, MHD_Connection* connection)
|
|||
|
||||
MHD_add_response_header(response, "Access-Control-Allow-Origin", "*");
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
|
||||
m_useCache ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
|
||||
m_etag.get_option(ETag::CACHEABLE_ENTITY) ? "max-age=2723040, public" : "no-cache, no-store, must-revalidate");
|
||||
const std::string etag = m_etag.get_etag();
|
||||
if ( ! etag.empty() )
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_ETAG, etag.c_str());
|
||||
|
||||
if (m_returnCode == MHD_HTTP_OK && request.has_range())
|
||||
m_returnCode = MHD_HTTP_PARTIAL_CONTENT;
|
||||
|
@ -301,7 +317,7 @@ void Response::set_entry(const Entry& entry, const RequestContext& request) {
|
|||
|
||||
const std::string mimeType = get_mime_type(entry);
|
||||
set_mimeType(mimeType);
|
||||
set_cache(true);
|
||||
set_cacheable();
|
||||
|
||||
if ( is_compressible_mime_type(mimeType) ) {
|
||||
zim::Blob raw_content = entry.getBlob();
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
#include <mustache.hpp>
|
||||
#include "entry.h"
|
||||
#include "etag.h"
|
||||
|
||||
extern "C" {
|
||||
#include <microhttpd.h>
|
||||
|
@ -55,18 +56,22 @@ class Response {
|
|||
|
||||
void set_mimeType(const std::string& mimeType) { m_mimeType = mimeType; }
|
||||
void set_code(int code) { m_returnCode = code; }
|
||||
void set_cache(bool cache) { m_useCache = cache; }
|
||||
void set_cacheable() { m_etag.set_option(ETag::CACHEABLE_ENTITY); }
|
||||
void set_server_id(const std::string& id) { m_etag.set_server_id(id); }
|
||||
void set_etag(const ETag& etag) { m_etag = etag; }
|
||||
void set_compress(bool compress) { m_compress = compress; }
|
||||
void set_taskbar(const std::string& bookName, const std::string& bookTitle);
|
||||
void set_range_first(uint64_t start) { m_startRange = start; }
|
||||
void set_range_len(uint64_t len) { m_lenRange = len; }
|
||||
|
||||
int getReturnCode() { return m_returnCode; }
|
||||
int getReturnCode() const { return m_returnCode; }
|
||||
std::string get_mimeType() const { return m_mimeType; }
|
||||
|
||||
void introduce_taskbar();
|
||||
void inject_externallinks_blocker();
|
||||
|
||||
bool can_compress(const RequestContext& request) const;
|
||||
|
||||
private: // functions
|
||||
MHD_Response* create_mhd_response(const RequestContext& request);
|
||||
MHD_Response* create_raw_content_mhd_response(const RequestContext& request);
|
||||
|
@ -84,13 +89,13 @@ class Response {
|
|||
bool m_withTaskbar;
|
||||
bool m_withLibraryButton;
|
||||
bool m_blockExternalLinks;
|
||||
bool m_useCache;
|
||||
bool m_compress;
|
||||
bool m_addTaskbar;
|
||||
std::string m_bookName;
|
||||
std::string m_bookTitle;
|
||||
uint64_t m_startRange;
|
||||
uint64_t m_lenRange;
|
||||
ETag m_etag;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
283
test/server.cpp
283
test/server.cpp
|
@ -6,10 +6,48 @@
|
|||
|
||||
#include "./httplib.h"
|
||||
|
||||
using TestContextImpl = std::vector<std::pair<std::string, std::string> >;
|
||||
struct TestContext : TestContextImpl {
|
||||
TestContext(const std::initializer_list<value_type>& il)
|
||||
: TestContextImpl(il)
|
||||
{}
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& out, const TestContext& ctx)
|
||||
{
|
||||
out << "Test context:\n";
|
||||
for ( const auto& kv : ctx )
|
||||
out << "\t" << kv.first << ": " << kv.second << "\n";
|
||||
out << std::endl;
|
||||
return out;
|
||||
}
|
||||
|
||||
bool is_valid_etag(const std::string& etag)
|
||||
{
|
||||
return etag.size() >= 2 &&
|
||||
etag.front() == '"' &&
|
||||
etag.back() == '"';
|
||||
}
|
||||
|
||||
template<class T1, class T2>
|
||||
T1 concat(T1 a, const T2& b)
|
||||
{
|
||||
a.insert(a.end(), b.begin(), b.end());
|
||||
return a;
|
||||
}
|
||||
|
||||
typedef httplib::Headers Headers;
|
||||
|
||||
Headers invariantHeaders(Headers headers)
|
||||
{
|
||||
headers.erase("Date");
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
class ZimFileServer
|
||||
{
|
||||
public: // types
|
||||
typedef httplib::Headers Headers;
|
||||
typedef std::shared_ptr<httplib::Response> Response;
|
||||
|
||||
public: // functions
|
||||
|
@ -77,8 +115,12 @@ protected:
|
|||
}
|
||||
};
|
||||
|
||||
const bool WITH_ETAG = true;
|
||||
const bool NO_ETAG = false;
|
||||
|
||||
struct Resource
|
||||
{
|
||||
bool etag_expected;
|
||||
const char* url;
|
||||
};
|
||||
|
||||
|
@ -88,69 +130,59 @@ std::ostream& operator<<(std::ostream& out, const Resource& r)
|
|||
return out;
|
||||
}
|
||||
|
||||
Resource resources200Compressible[] = {
|
||||
{ "/" },
|
||||
typedef std::vector<Resource> ResourceCollection;
|
||||
|
||||
{ "/skin/jquery-ui/jquery-ui.structure.min.css" },
|
||||
{ "/skin/jquery-ui/jquery-ui.min.js" },
|
||||
{ "/skin/jquery-ui/external/jquery/jquery.js" },
|
||||
{ "/skin/jquery-ui/jquery-ui.theme.min.css" },
|
||||
{ "/skin/jquery-ui/jquery-ui.min.css" },
|
||||
{ "/skin/taskbar.js" },
|
||||
{ "/skin/taskbar.css" },
|
||||
{ "/skin/block_external.js" },
|
||||
const ResourceCollection resources200Compressible{
|
||||
{ WITH_ETAG, "/" },
|
||||
|
||||
{ "/search?content=zimfile&pattern=abcd" },
|
||||
{ WITH_ETAG, "/skin/jquery-ui/jquery-ui.structure.min.css" },
|
||||
{ WITH_ETAG, "/skin/jquery-ui/jquery-ui.min.js" },
|
||||
{ WITH_ETAG, "/skin/jquery-ui/external/jquery/jquery.js" },
|
||||
{ WITH_ETAG, "/skin/jquery-ui/jquery-ui.theme.min.css" },
|
||||
{ WITH_ETAG, "/skin/jquery-ui/jquery-ui.min.css" },
|
||||
{ WITH_ETAG, "/skin/taskbar.js" },
|
||||
{ WITH_ETAG, "/skin/taskbar.css" },
|
||||
{ WITH_ETAG, "/skin/block_external.js" },
|
||||
|
||||
{ "/suggest?content=zimfile&term=ray" },
|
||||
{ NO_ETAG, "/search?content=zimfile&pattern=abcd" },
|
||||
|
||||
{ "/catch/external?source=www.example.com" },
|
||||
{ NO_ETAG, "/suggest?content=zimfile&term=ray" },
|
||||
|
||||
{ "/zimfile/A/index" },
|
||||
{ "/zimfile/A/Ray_Charles" },
|
||||
{ NO_ETAG, "/catch/external?source=www.example.com" },
|
||||
|
||||
{ WITH_ETAG, "/zimfile/A/index" },
|
||||
{ WITH_ETAG, "/zimfile/A/Ray_Charles" },
|
||||
};
|
||||
|
||||
Resource resources200Uncompressible[] = {
|
||||
{ "/skin/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png" },
|
||||
{ "/skin/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png" },
|
||||
{ "/skin/jquery-ui/images/ui-icons_222222_256x240.png" },
|
||||
{ "/skin/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png" },
|
||||
{ "/skin/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png" },
|
||||
{ "/skin/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png" },
|
||||
{ "/skin/jquery-ui/images/ui-icons_2e83ff_256x240.png" },
|
||||
{ "/skin/jquery-ui/images/ui-icons_cd0a0a_256x240.png" },
|
||||
{ "/skin/jquery-ui/images/ui-icons_888888_256x240.png" },
|
||||
{ "/skin/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png" },
|
||||
{ "/skin/jquery-ui/images/animated-overlay.gif" },
|
||||
{ "/skin/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png" },
|
||||
{ "/skin/jquery-ui/images/ui-icons_454545_256x240.png" },
|
||||
{ "/skin/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png" },
|
||||
{ "/skin/caret.png" },
|
||||
const ResourceCollection resources200Uncompressible{
|
||||
{ WITH_ETAG, "/skin/jquery-ui/images/animated-overlay.gif" },
|
||||
{ WITH_ETAG, "/skin/caret.png" },
|
||||
|
||||
{ "/catalog/root.xml" },
|
||||
{ "/catalog/searchdescription.xml" },
|
||||
{ "/catalog/search" },
|
||||
{ NO_ETAG, "/catalog/root.xml" },
|
||||
{ NO_ETAG, "/catalog/searchdescription.xml" },
|
||||
{ NO_ETAG, "/catalog/search" },
|
||||
|
||||
{ "/meta?content=zimfile&name=title" },
|
||||
{ "/meta?content=zimfile&name=description" },
|
||||
{ "/meta?content=zimfile&name=language" },
|
||||
{ "/meta?content=zimfile&name=name" },
|
||||
{ "/meta?content=zimfile&name=tags" },
|
||||
{ "/meta?content=zimfile&name=date" },
|
||||
{ "/meta?content=zimfile&name=creator" },
|
||||
{ "/meta?content=zimfile&name=publisher" },
|
||||
{ "/meta?content=zimfile&name=favicon" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=title" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=description" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=language" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=name" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=tags" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=date" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=creator" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=publisher" },
|
||||
{ WITH_ETAG, "/meta?content=zimfile&name=favicon" },
|
||||
|
||||
{ "/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
|
||||
{ WITH_ETAG, "/zimfile/I/m/Ray_Charles_classic_piano_pose.jpg" },
|
||||
};
|
||||
|
||||
ResourceCollection all200Resources()
|
||||
{
|
||||
return concat(resources200Compressible, resources200Uncompressible);
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, 200)
|
||||
{
|
||||
for ( const Resource& res : resources200Compressible )
|
||||
EXPECT_EQ(200, zfs1_->GET(res.url)->status) << "res.url: " << res.url;
|
||||
|
||||
for ( const Resource& res : resources200Uncompressible )
|
||||
for ( const Resource& res : all200Resources() )
|
||||
EXPECT_EQ(200, zfs1_->GET(res.url)->status) << "res.url: " << res.url;
|
||||
}
|
||||
|
||||
|
@ -221,3 +253,156 @@ TEST_F(ServerTest, BookMainPageIsRedirectedToArticleIndex)
|
|||
ASSERT_TRUE(g->has_header("Location"));
|
||||
ASSERT_EQ("/zimfile/A/index", g->get_header_value("Location"));
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, HeadMethodIsSupported)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() )
|
||||
EXPECT_EQ(200, zfs1_->HEAD(res.url)->status) << res;
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, TheResponseToHeadRequestHasNoBody)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() )
|
||||
EXPECT_TRUE(zfs1_->HEAD(res.url)->body.empty()) << res;
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, HeadersAreTheSameInResponsesToHeadAndGetRequests)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
httplib::Headers g = zfs1_->GET(res.url)->headers;
|
||||
httplib::Headers h = zfs1_->HEAD(res.url)->headers;
|
||||
EXPECT_EQ(invariantHeaders(g), invariantHeaders(h)) << res;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, ETagHeaderIsSetAsNeeded)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
const auto responseToGet = zfs1_->GET(res.url);
|
||||
EXPECT_EQ(res.etag_expected, responseToGet->has_header("ETag")) << res;
|
||||
if ( res.etag_expected )
|
||||
EXPECT_TRUE(is_valid_etag(responseToGet->get_header_value("ETag")));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, ETagIsTheSameInResponsesToDifferentRequestsOfTheSameURL)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
const auto h1 = zfs1_->HEAD(res.url);
|
||||
const auto h2 = zfs1_->HEAD(res.url);
|
||||
EXPECT_EQ(h1->get_header_value("ETag"), h2->get_header_value("ETag"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, ETagIsTheSameAcrossHeadAndGet)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
const auto g = zfs1_->GET(res.url);
|
||||
const auto h = zfs1_->HEAD(res.url);
|
||||
EXPECT_EQ(h->get_header_value("ETag"), g->get_header_value("ETag"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, DifferentServerInstancesProduceDifferentETags)
|
||||
{
|
||||
ZimFileServer zfs2(PORT + 1, ZIMFILE);
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
if ( !res.etag_expected ) continue;
|
||||
const auto h1 = zfs1_->HEAD(res.url);
|
||||
const auto h2 = zfs2.HEAD(res.url);
|
||||
EXPECT_NE(h1->get_header_value("ETag"), h2->get_header_value("ETag"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, CompressionInfluencesETag)
|
||||
{
|
||||
for ( const Resource& res : resources200Compressible ) {
|
||||
if ( ! res.etag_expected ) continue;
|
||||
const auto g1 = zfs1_->GET(res.url);
|
||||
const auto g2 = zfs1_->GET(res.url, { {"Accept-Encoding", ""} } );
|
||||
const auto g3 = zfs1_->GET(res.url, { {"Accept-Encoding", "deflate"} } );
|
||||
const auto etag = g1->get_header_value("ETag");
|
||||
EXPECT_EQ(etag, g2->get_header_value("ETag"));
|
||||
EXPECT_NE(etag, g3->get_header_value("ETag"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, ETagOfUncompressibleContentIsNotAffectedByAcceptEncoding)
|
||||
{
|
||||
for ( const Resource& res : resources200Uncompressible ) {
|
||||
if ( ! res.etag_expected ) continue;
|
||||
const auto g1 = zfs1_->GET(res.url);
|
||||
const auto g2 = zfs1_->GET(res.url, { {"Accept-Encoding", ""} } );
|
||||
const auto g3 = zfs1_->GET(res.url, { {"Accept-Encoding", "deflate"} } );
|
||||
const auto etag = g1->get_header_value("ETag");
|
||||
EXPECT_EQ(etag, g2->get_header_value("ETag")) << res;
|
||||
EXPECT_EQ(etag, g3->get_header_value("ETag")) << res;
|
||||
}
|
||||
}
|
||||
|
||||
// Pick from the response those headers that are required to be present in the
|
||||
// 304 (Not Modified) response if they would be set in the 200 (OK) response.
|
||||
// NOTE: The "Date" header (which should belong to that list as required
|
||||
// NOTE: by RFC 7232) is not included (since the result of this function
|
||||
// NOTE: will be used to check the equality of headers from the 200 and 304
|
||||
// NOTe: responses).
|
||||
Headers special304Headers(const httplib::Response& r)
|
||||
{
|
||||
Headers result;
|
||||
std::copy_if(
|
||||
r.headers.begin(), r.headers.end(),
|
||||
std::inserter(result, result.end()),
|
||||
[](const Headers::value_type& x) {
|
||||
return x.first == "Cache-Control"
|
||||
|| x.first == "Content-Location"
|
||||
|| x.first == "ETag"
|
||||
|| x.first == "Expires"
|
||||
|| x.first == "Vary";
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// make a list of three etags with the given one in the middle
|
||||
std::string make_etag_list(const std::string& etag)
|
||||
{
|
||||
return "\"x" + etag.substr(1) + ", "
|
||||
+ etag + ", "
|
||||
+ etag.substr(0, etag.size()-2) + "\"";
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, IfNoneMatchRequestsWithMatchingETagResultIn304Responses)
|
||||
{
|
||||
const char* const encodings[] = { "", "deflate" };
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
for ( const char* enc: encodings ) {
|
||||
if ( ! res.etag_expected ) continue;
|
||||
const TestContext ctx{ {"url", res.url}, {"encoding", enc} };
|
||||
|
||||
const auto g = zfs1_->GET(res.url, { {"Accept-Encoding", enc} });
|
||||
const auto etag = g->get_header_value("ETag");
|
||||
|
||||
const std::string etags = make_etag_list(etag);
|
||||
const Headers headers{{"If-None-Match", etags}, {"Accept-Encoding", enc}};
|
||||
const auto g2 = zfs1_->GET(res.url, headers );
|
||||
const auto h = zfs1_->HEAD(res.url, headers );
|
||||
EXPECT_EQ(304, h->status) << ctx;
|
||||
EXPECT_EQ(304, g2->status) << ctx;
|
||||
EXPECT_EQ(special304Headers(*g), special304Headers(*g2)) << ctx;
|
||||
EXPECT_EQ(special304Headers(*g2), special304Headers(*h)) << ctx;
|
||||
EXPECT_TRUE(g2->body.empty()) << ctx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ServerTest, IfNoneMatchRequestsWithMismatchingETagResultIn200Responses)
|
||||
{
|
||||
for ( const Resource& res : all200Resources() ) {
|
||||
const auto g = zfs1_->GET(res.url);
|
||||
const auto etag = g->get_header_value("ETag");
|
||||
const auto etag2 = etag.substr(0, etag.size() - 1) + "x\"";
|
||||
const auto h = zfs1_->HEAD(res.url, { {"If-None-Match", etag2} } );
|
||||
const auto g2 = zfs1_->GET(res.url, { {"If-None-Match", etag2} } );
|
||||
EXPECT_EQ(200, h->status);
|
||||
EXPECT_EQ(200, g2->status);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue