mpd/src/storage/plugins/CurlStorage.cxx

604 lines
14 KiB
C++
Raw Normal View History

2017-01-03 07:25:14 +01:00
/*
* Copyright 2003-2016 The Music Player Daemon Project
* http://www.musicpd.org
*
* 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 2 of the License, or
* (at your option) 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 "config.h"
#include "CurlStorage.hxx"
#include "storage/StoragePlugin.hxx"
#include "storage/StorageInterface.hxx"
#include "storage/FileInfo.hxx"
#include "storage/MemoryDirectoryReader.hxx"
#include "lib/curl/Global.hxx"
#include "lib/curl/Slist.hxx"
#include "lib/curl/Request.hxx"
#include "lib/curl/Handler.hxx"
#include "lib/expat/ExpatParser.hxx"
#include "fs/Traits.hxx"
#include "event/Call.hxx"
#include "event/DeferredMonitor.hxx"
#include "thread/Mutex.hxx"
#include "thread/Cond.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringCompare.hxx"
#include "util/TimeParser.hxx"
#include "util/UriUtil.hxx"
#include <algorithm>
#include <memory>
#include <string>
#include <list>
#include <assert.h>
class CurlStorage final : public Storage {
const std::string base;
CurlGlobal *const curl;
public:
CurlStorage(EventLoop &_loop, const char *_base)
:base(_base),
curl(new CurlGlobal(_loop)) {}
~CurlStorage() {
BlockingCall(curl->GetEventLoop(), [this](){ delete curl; });
}
/* virtual methods from class Storage */
StorageFileInfo GetInfo(const char *uri_utf8, bool follow) override;
StorageDirectoryReader *OpenDirectory(const char *uri_utf8) override;
std::string MapUTF8(const char *uri_utf8) const noexcept override;
2017-01-03 07:25:14 +01:00
const char *MapToRelativeUTF8(const char *uri_utf8) const noexcept override;
2017-01-03 07:25:14 +01:00
};
std::string
CurlStorage::MapUTF8(const char *uri_utf8) const noexcept
2017-01-03 07:25:14 +01:00
{
assert(uri_utf8 != nullptr);
if (StringIsEmpty(uri_utf8))
return base;
// TODO: escape the given URI
return PathTraitsUTF8::Build(base.c_str(), uri_utf8);
}
const char *
CurlStorage::MapToRelativeUTF8(const char *uri_utf8) const noexcept
2017-01-03 07:25:14 +01:00
{
// TODO: escape/unescape?
return PathTraitsUTF8::Relative(base.c_str(), uri_utf8);
}
class BlockingHttpRequest : protected CurlResponseHandler, DeferredMonitor {
std::exception_ptr postponed_error;
bool done = false;
protected:
CurlRequest request;
Mutex mutex;
Cond cond;
public:
BlockingHttpRequest(CurlGlobal &curl, const char *uri)
:DeferredMonitor(curl.GetEventLoop()),
request(curl, uri, *this) {
// TODO: use CurlInputStream's configuration
/* start the transfer inside the IOThread */
DeferredMonitor::Schedule();
}
void Wait() {
const std::lock_guard<Mutex> lock(mutex);
while (!done)
cond.wait(mutex);
if (postponed_error)
std::rethrow_exception(postponed_error);
}
protected:
void SetDone() {
assert(!done);
request.Stop();
done = true;
cond.signal();
}
void LockSetDone() {
const std::lock_guard<Mutex> lock(mutex);
SetDone();
}
private:
/* virtual methods from DeferredMonitor */
void RunDeferred() final {
assert(!done);
request.Start();
}
/* virtual methods from CurlResponseHandler */
void OnError(std::exception_ptr e) final {
const std::lock_guard<Mutex> lock(mutex);
postponed_error = std::move(e);
SetDone();
}
};
/**
* A helper class which feeds a (foreign) memory buffer into the
* CURLOPT_READFUNCTION.
*/
class CurlRequestBody {
ConstBuffer<char> data;
public:
explicit CurlRequestBody(ConstBuffer<void> _data)
:data(ConstBuffer<char>::FromVoid(_data)) {}
explicit constexpr CurlRequestBody(StringView _data)
:data(_data) {}
template<typename T>
CurlRequestBody(CurlRequest &request, T _data)
:CurlRequestBody(_data) {
request.SetOption(CURLOPT_READFUNCTION, Callback);
request.SetOption(CURLOPT_READDATA, this);
}
private:
size_t Read(char *buffer, size_t size) {
size_t n = std::min(size, data.size);
std::copy_n(data.begin(), n, buffer);
return n;
}
static size_t Callback(char *buffer, size_t size, size_t nitems,
void *instream) {
auto &rb = *(CurlRequestBody *)instream;
return rb.Read(buffer, size * nitems);
}
};
/**
* The (relevant) contents of a "<D:response>" element.
*/
struct DavResponse {
std::string href;
unsigned status = 0;
bool collection = false;
std::chrono::system_clock::time_point mtime =
std::chrono::system_clock::time_point::min();
uint64_t length = 0;
bool Check() const {
return !href.empty();
}
};
static unsigned
ParseStatus(const char *s)
{
/* skip the "HTTP/1.1" prefix */
const char *space = strchr(s, ' ');
if (space == nullptr)
return 0;
return strtoul(space + 1, nullptr, 10);
}
static unsigned
ParseStatus(const char *s, size_t length)
{
return ParseStatus(std::string(s, length).c_str());
}
static std::chrono::system_clock::time_point
ParseTimeStamp(const char *s)
{
try {
// TODO: make this more robust
return ParseTimePoint(s, "%a, %d %b %Y %T %Z");
} catch (const std::runtime_error &) {
return std::chrono::system_clock::time_point::min();
}
}
static std::chrono::system_clock::time_point
ParseTimeStamp(const char *s, size_t length)
{
return ParseTimeStamp(std::string(s, length).c_str());
}
static uint64_t
ParseU64(const char *s)
{
return strtoull(s, nullptr, 10);
}
static uint64_t
ParseU64(const char *s, size_t length)
{
return ParseU64(std::string(s, length).c_str());
}
gcc_pure
static bool
IsXmlContentType(const char *content_type) noexcept
{
return strncmp(content_type, "text/xml", 8) == 0;
}
gcc_pure
static bool
IsXmlContentType(const std::multimap<std::string, std::string> &headers) noexcept
{
auto i = headers.find("content-type");
return i != headers.end() && IsXmlContentType(i->second.c_str());
}
2017-01-03 07:25:14 +01:00
/**
* A WebDAV PROPFIND request. Each "response" element will be passed
* to OnDavResponse() (to be implemented by a derived class).
*/
class PropfindOperation : BlockingHttpRequest, CommonExpatParser {
CurlSlist request_headers;
CurlRequestBody request_body;
enum class State {
ROOT,
RESPONSE,
HREF,
STATUS,
TYPE,
MTIME,
LENGTH,
} state = State::ROOT;
DavResponse response;
public:
PropfindOperation(CurlGlobal &_curl, const char *_uri, unsigned depth)
:BlockingHttpRequest(_curl, _uri),
CommonExpatParser(ExpatNamespaceSeparator{'|'}),
request_body(request,
"<?xml version=\"1.0\"?>\n"
"<a:propfind xmlns:a=\"DAV:\">"
"<a:prop><a:getcontenttype/></a:prop>"
"<a:prop><a:getcontentlength/></a:prop>"
"</a:propfind>")
{
request.SetOption(CURLOPT_CUSTOMREQUEST, "PROPFIND");
char buffer[40];
sprintf(buffer, "depth: %u", depth);
request_headers.Append(buffer);
request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
// TODO: send request body
}
using BlockingHttpRequest::Wait;
protected:
virtual void OnDavResponse(DavResponse &&r) = 0;
private:
void FinishResponse() {
if (response.Check())
OnDavResponse(std::move(response));
response = DavResponse();
}
/* virtual methods from CurlResponseHandler */
void OnHeaders(unsigned status,
std::multimap<std::string, std::string> &&headers) final {
if (status != 207)
throw FormatRuntimeError("Status %d from WebDAV server; expected \"207 Multi-Status\"",
status);
if (!IsXmlContentType(headers))
2017-01-03 07:25:14 +01:00
throw std::runtime_error("Unexpected Content-Type from WebDAV server");
}
void OnData(ConstBuffer<void> _data) final {
const auto data = ConstBuffer<char>::FromVoid(_data);
Parse(data.data, data.size, false);
}
void OnEnd() final {
Parse("", 0, true);
LockSetDone();
}
/* virtual methods from CommonExpatParser */
void StartElement(const XML_Char *name,
gcc_unused const XML_Char **attrs) final {
switch (state) {
case State::ROOT:
if (strcmp(name, "DAV:|response") == 0)
state = State::RESPONSE;
break;
case State::RESPONSE:
if (strcmp(name, "DAV:|href") == 0)
state = State::HREF;
else if (strcmp(name, "DAV:|status") == 0)
state = State::STATUS;
else if (strcmp(name, "DAV:|resourcetype") == 0)
state = State::TYPE;
else if (strcmp(name, "DAV:|getlastmodified") == 0)
state = State::MTIME;
else if (strcmp(name, "DAV:|getcontentlength") == 0)
state = State::LENGTH;
break;
case State::TYPE:
if (strcmp(name, "DAV:|collection") == 0)
response.collection = true;
break;
case State::HREF:
case State::STATUS:
case State::LENGTH:
case State::MTIME:
break;
}
}
void EndElement(const XML_Char *name) final {
switch (state) {
case State::ROOT:
break;
case State::RESPONSE:
if (strcmp(name, "DAV:|response") == 0) {
FinishResponse();
state = State::ROOT;
}
break;
case State::HREF:
if (strcmp(name, "DAV:|href") == 0)
state = State::RESPONSE;
break;
case State::STATUS:
if (strcmp(name, "DAV:|status") == 0)
state = State::RESPONSE;
break;
case State::TYPE:
if (strcmp(name, "DAV:|resourcetype") == 0)
state = State::RESPONSE;
break;
case State::MTIME:
if (strcmp(name, "DAV:|getlastmodified") == 0)
state = State::RESPONSE;
break;
case State::LENGTH:
if (strcmp(name, "DAV:|getcontentlength") == 0)
state = State::RESPONSE;
break;
}
}
void CharacterData(const XML_Char *s, int len) final {
switch (state) {
case State::ROOT:
case State::RESPONSE:
case State::TYPE:
break;
case State::HREF:
response.href.assign(s, len);
break;
case State::STATUS:
response.status = ParseStatus(s, len);
break;
case State::MTIME:
response.mtime = ParseTimeStamp(s, len);
break;
case State::LENGTH:
response.length = ParseU64(s, len);
break;
}
}
};
/**
* Obtain information about a single file using WebDAV PROPFIND.
*/
class HttpGetInfoOperation final : public PropfindOperation {
StorageFileInfo info;
public:
HttpGetInfoOperation(CurlGlobal &curl, const char *uri)
:PropfindOperation(curl, uri, 0) {
info.type = StorageFileInfo::Type::OTHER;
info.size = 0;
info.mtime = 0;
info.device = info.inode = 0;
}
const StorageFileInfo &Perform() {
Wait();
return info;
}
protected:
/* virtual methods from PropfindOperation */
void OnDavResponse(DavResponse &&r) override {
if (r.status != 200)
return;
info.type = r.collection
? StorageFileInfo::Type::DIRECTORY
: StorageFileInfo::Type::REGULAR;
info.size = r.length;
info.mtime = r.mtime > std::chrono::system_clock::time_point()
? std::chrono::system_clock::to_time_t(r.mtime)
: 0;
info.device = info.inode = 0;
}
};
StorageFileInfo
CurlStorage::GetInfo(const char *uri_utf8, gcc_unused bool follow)
{
// TODO: escape the given URI
std::string uri = base;
uri += uri_utf8;
return HttpGetInfoOperation(*curl, uri.c_str()).Perform();
}
gcc_pure
static const char *
UriPathOrSlash(const char *uri) noexcept
2017-01-03 07:25:14 +01:00
{
const char *path = uri_get_path(uri);
if (path == nullptr)
path = "/";
return path;
}
/**
* Obtain a directory listing using WebDAV PROPFIND.
*/
class HttpListDirectoryOperation final : public PropfindOperation {
const std::string base_path;
MemoryStorageDirectoryReader::List entries;
public:
HttpListDirectoryOperation(CurlGlobal &curl, const char *uri)
:PropfindOperation(curl, uri, 1),
base_path(UriPathOrSlash(uri)) {}
StorageDirectoryReader *Perform() {
Wait();
return ToReader();
}
private:
StorageDirectoryReader *ToReader() {
return new MemoryStorageDirectoryReader(std::move(entries));
}
/**
* Convert a "href" attribute (which may be an absolute URI)
* to the base file name.
*/
gcc_pure
StringView HrefToEscapedName(const char *href) const noexcept {
2017-01-03 07:25:14 +01:00
const char *path = uri_get_path(href);
if (path == nullptr)
return nullptr;
path = StringAfterPrefix(path, base_path.c_str());
if (path == nullptr || *path == 0)
return nullptr;
const char *slash = strchr(path, '/');
if (slash == nullptr)
/* regular file */
return path;
else if (slash[1] == 0)
/* trailing slash: collection; strip the slash */
return {path, slash};
else
/* strange, better ignore it */
return nullptr;
}
protected:
/* virtual methods from PropfindOperation */
void OnDavResponse(DavResponse &&r) override {
if (r.status != 200)
return;
const auto escaped_name = HrefToEscapedName(r.href.c_str());
if (escaped_name.IsNull())
return;
// TODO: unescape
const auto name = escaped_name;
entries.emplace_front(std::string(name.data, name.size));
auto &info = entries.front().info;
info.type = r.collection
? StorageFileInfo::Type::DIRECTORY
: StorageFileInfo::Type::REGULAR;
info.size = r.length;
info.mtime = r.mtime > std::chrono::system_clock::time_point()
? std::chrono::system_clock::to_time_t(r.mtime)
: 0;
info.device = info.inode = 0;
}
};
StorageDirectoryReader *
CurlStorage::OpenDirectory(const char *uri_utf8)
{
// TODO: escape the given URI
std::string uri = base;
uri += uri_utf8;
/* collection URIs must end with a slash */
if (uri.back() != '/')
uri.push_back('/');
return HttpListDirectoryOperation(*curl, uri.c_str()).Perform();
}
static Storage *
CreateCurlStorageURI(EventLoop &event_loop, const char *uri)
{
if (strncmp(uri, "http://", 7) != 0 &&
strncmp(uri, "https://", 8) != 0)
return nullptr;
return new CurlStorage(event_loop, uri);
}
const StoragePlugin curl_storage_plugin = {
"curl",
CreateCurlStorageURI,
};