Skip to content

Commit

Permalink
Refactor experimental filesystem route for testing purposes
Browse files Browse the repository at this point in the history
with some new features inspired by Apache mod_dir DirectoryIndex, DirectoryIndexRedirect and DirectorySlash directives

(cherry picked from commit bafa46118d1017099c5b4e2bbaec230262a2800c)
  • Loading branch information
garethsb committed Jun 21, 2019
1 parent 7c7538c commit 52a98f1
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 45 deletions.
1 change: 1 addition & 0 deletions Development/bst/filesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ namespace bst
using bst_filesystem::directory_iterator;
using bst_filesystem::recursive_directory_iterator;
using bst_filesystem::exists;
using bst_filesystem::is_regular_file;
using bst_filesystem::is_directory;
using bst_filesystem::file_size;
using bst_filesystem::create_directory;
Expand Down
9 changes: 4 additions & 5 deletions Development/nmos/admin_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ namespace nmos
return pplx::task_from_result(true);
});

admin_ui.support(U("/") + nmos::experimental::patterns::admin_ui.pattern + U("/?"), methods::GET, [](http_request, http_response res, const string_t&, const route_parameters&)
admin_ui.support(U("/") + nmos::experimental::patterns::admin_ui.pattern, methods::GET, [](http_request, http_response res, const string_t&, const route_parameters&)
{
// temporarily hard-coded hack to redirect admin root to the index.html
set_reply(res, status_codes::TemporaryRedirect);
res.headers().add(web::http::header_names::location, U("/admin/index.html"));
set_reply(res, status_codes::TemporaryRedirect); // or status_codes::MovedPermanently?
res.headers().add(web::http::header_names::location, U("/admin/"));
return pplx::task_from_result(true);
});

Expand All @@ -43,7 +42,7 @@ namespace nmos
{ U("png"), U("image/png") }
};

admin_ui.mount(U("/") + nmos::experimental::patterns::admin_ui.pattern, web::http::methods::GET, nmos::experimental::make_filesystem_route(filesystem_root, nmos::experimental::make_relative_path_content_type_validator(valid_extensions), gate));
admin_ui.mount(U("/") + nmos::experimental::patterns::admin_ui.pattern, web::http::methods::GET, nmos::experimental::make_filesystem_route(filesystem_root, nmos::experimental::make_relative_path_content_type_handler(valid_extensions), gate));

return admin_ui;
}
Expand Down
97 changes: 76 additions & 21 deletions Development/nmos/filesystem_route.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "nmos/filesystem_route.h"

#include <boost/algorithm/string/erase.hpp>
#include "bst/filesystem.h"

#include "cpprest/filestream.h"
Expand All @@ -16,47 +17,101 @@ namespace nmos
#else
using native_path = bst_filesystem::wpath;
#endif

// could now be replaced with native_path::extension
inline utility::string_t extension(const utility::string_t& pathname)
{
utility::string_t::size_type dot = pathname.find_last_of(U('.')) + 1;
utility::string_t::size_type slash = pathname.find_last_of(U("\\/")) + 1;
return slash < dot ? pathname.substr(dot) : utility::string_t();
}
}

// determines content type based only on file extension, and rejects unexpected file types
// (another option would be to return "application/octet-stream")
relative_path_content_type_handler make_relative_path_content_type_handler(const content_types& valid_extensions)
{
return [valid_extensions](const utility::string_t& relative_path)
{
const auto found = valid_extensions.find(details::extension(relative_path));
return valid_extensions.end() != found ? found->second : U("");
};
}

web::http::experimental::listener::api_router make_filesystem_route(const utility::string_t& filesystem_root, const relative_path_content_type_validator& validate, slog::base_gate& gate_)
// if the path ends with a slash, tries the specified index files in turn and returns the first one found (or empty string if path is invalid for any reason)
// otherwise returns an external redirect with a trailing slash
directory_index_redirect_handler make_directory_index_redirect_handler(const utility::string_t& filesystem_root, const directory_index_files& valid_index_files, bool external_redirect)
{
return [=](const utility::string_t& relative_path) -> std::pair<utility::string_t, bool>
{
if (relative_path.empty() || relative_path.back() != U('/')) return{ relative_path + U('/'), true };

const auto filesystem_path = filesystem_root + relative_path;
for (const auto& index : valid_index_files)
{
if (bst::filesystem::is_regular_file(details::native_path(filesystem_path + index)))
{
return{ relative_path + index, external_redirect };
}
}
return{};
};
}

web::http::experimental::listener::api_router make_filesystem_route(const utility::string_t& filesystem_root, const relative_path_content_type_handler& content_type_handler, const directory_index_redirect_handler& index_redirect_handler, slog::base_gate& gate_)
{
using namespace web::http::experimental::listener::api_router_using_declarations;

api_router filesystem_route;

filesystem_route.support(U("(?<filesystem-relative-path>/.+)"), web::http::methods::GET, [filesystem_root, validate, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters)
filesystem_route.support(U("(?<filesystem-relative-path>/.*)"), web::http::methods::GET, [filesystem_root, content_type_handler, index_redirect_handler, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters)
{
nmos::api_gate gate(gate_, req, parameters);
slog::log<slog::severities::more_info>(gate, SLOG_FLF) << "Filesystem request received";
const auto relative_path = web::uri::decode(parameters.at(U("filesystem-relative-path")));
auto relative_path = web::uri::decode(parameters.at(U("filesystem-relative-path")));
const bool naughty = string_t::npos != relative_path.find(U("/.."));
const auto content_type = validate(relative_path);
if (naughty || content_type.empty())
if (!naughty)
{
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unexpected request forbidden";
set_reply(res, status_codes::Forbidden);
}
else
{
const auto filesystem_path = filesystem_root + relative_path;
// relative path will begin with a slash
auto filesystem_path = filesystem_root + relative_path;

if (bst::filesystem::exists(details::native_path(filesystem_path)))
if (bst::filesystem::is_directory(details::native_path(filesystem_path)))
{
const utility::size64_t content_length = bst::filesystem::file_size(details::native_path(filesystem_path));
return concurrency::streams::fstream::open_istream(filesystem_path, std::ios::in).then([res, content_length, content_type](concurrency::streams::istream is) mutable
const auto index_redirect = index_redirect_handler(relative_path);
if (!index_redirect.first.empty())
{
set_reply(res, status_codes::OK, is, content_length, content_type);
return true;
});
if (index_redirect.second)
{
set_reply(res, status_codes::TemporaryRedirect); // or status_codes::MovedPermanently?
res.headers().add(web::http::header_names::location, boost::algorithm::erase_tail_copy(req.request_uri().path(), (int)relative_path.size()) + index_redirect.first);

return pplx::task_from_result(true);
}

relative_path = index_redirect.first;
filesystem_path = filesystem_root + relative_path;
}
}
else

if (bst::filesystem::is_regular_file(details::native_path(filesystem_path)))
{
// unless requests are for files of a supported file type that are actually found in the filesystem, let's just report Forbidden
slog::log<slog::severities::error>(gate, SLOG_FLF) << "Unexpected request forbidden";
set_reply(res, status_codes::Forbidden);
const auto content_type = content_type_handler(relative_path);

if (!content_type.empty())
{
const utility::size64_t content_length = bst::filesystem::file_size(details::native_path(filesystem_path));
return concurrency::streams::fstream::open_istream(filesystem_path, std::ios::in).then([res, content_length, content_type](concurrency::streams::istream is) mutable
{
set_reply(res, status_codes::OK, is, content_length, content_type);
return true;
});
}
}
}

// unless requests are for files of a supported file type that are actually found in the filesystem, just report not found
set_reply(res, status_codes::NotFound);

return pplx::task_from_result(true);
});

Expand Down
40 changes: 21 additions & 19 deletions Development/nmos/filesystem_route.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define NMOS_FILESYSTEM_ROUTE_H

#include <map>
#include <set>
#include "cpprest/api_router.h"

namespace slog
Expand All @@ -15,32 +16,33 @@ namespace nmos
namespace experimental
{
// function from relative path to content type (or empty string if path is invalid for any reason)
typedef std::function<utility::string_t(const utility::string_t&)> relative_path_content_type_validator;
// the path argument will begin with a slash ('/')
typedef std::function<utility::string_t(const utility::string_t&)> relative_path_content_type_handler;

// map from file extensions to content types (e.g. from "html" to "text/html")
typedef std::map<utility::string_t, utility::string_t> content_types;

namespace details
{
inline utility::string_t extension(const utility::string_t& pathname)
{
utility::string_t::size_type dot = pathname.find_last_of(U('.')) + 1;
utility::string_t::size_type slash = pathname.find_last_of(U("\\/")) + 1;
return slash < dot ? pathname.substr(dot) : utility::string_t();
}
}
// determines content type based only on file extension, and rejects unexpected file types
// (another option would be to return "application/octet-stream")
relative_path_content_type_handler make_relative_path_content_type_handler(const content_types& valid_extensions);

// function from directory path to pair of index path (or empty string if path is invalid for any reason) and external redirect flag
// the path argument will begin with a slash and may end with or without a slash
typedef std::function<std::pair<utility::string_t, bool>(const utility::string_t&)> directory_index_redirect_handler;

// set of index files to look for when the client requests a directory
typedef std::set<utility::string_t> directory_index_files;

// A reasonable default validator is to reject unexpected file types (another option would be to return "application/octet-stream")
inline relative_path_content_type_validator make_relative_path_content_type_validator(const content_types& valid_extensions)
// if the path ends with a slash, tries the specified index files in turn and returns the first one found (or empty string if path is invalid for any reason)
// otherwise returns an external redirect with a trailing slash
directory_index_redirect_handler make_directory_index_redirect_handler(const utility::string_t& filesystem_root, const directory_index_files& valid_index_files = { U("index.html") }, bool external_redirect = false);

web::http::experimental::listener::api_router make_filesystem_route(const utility::string_t& filesystem_root, const relative_path_content_type_handler& content_type_handler, const directory_index_redirect_handler& index_redirect_handler, slog::base_gate& gate);

inline web::http::experimental::listener::api_router make_filesystem_route(const utility::string_t& filesystem_root, const relative_path_content_type_handler& content_type_handler, slog::base_gate& gate)
{
return [valid_extensions](const utility::string_t& relative_path)
{
const auto found = valid_extensions.find(details::extension(relative_path));
return valid_extensions.end() != found ? found->second : U("");
};
return make_filesystem_route(filesystem_root, content_type_handler, make_directory_index_redirect_handler(filesystem_root), gate);
}

web::http::experimental::listener::api_router make_filesystem_route(const utility::string_t& filesystem_root, const relative_path_content_type_validator& validate, slog::base_gate& gate);
}
}

Expand Down

0 comments on commit 52a98f1

Please sign in to comment.