diff --git a/src/lib/curl/Adapter.cxx b/src/lib/curl/Adapter.cxx new file mode 100644 index 000000000..e41587bff --- /dev/null +++ b/src/lib/curl/Adapter.cxx @@ -0,0 +1,191 @@ +/* + * Copyright 2008-2021 Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "Adapter.hxx" +#include "Easy.hxx" +#include "Handler.hxx" +#include "util/CharUtil.hxx" +#include "util/RuntimeError.hxx" +#include "util/StringStrip.hxx" +#include "util/StringView.hxx" + +#include +#include + +void +CurlResponseHandlerAdapter::Install(CurlEasy &easy) +{ + assert(state == State::UNINITIALISED); + + error_buffer[0] = 0; + easy.SetErrorBuffer(error_buffer); + + easy.SetHeaderFunction(_HeaderFunction, this); + easy.SetWriteFunction(WriteFunction, this); + + curl = easy.Get(); + + state = State::HEADERS; +} + +void +CurlResponseHandlerAdapter::FinishHeaders() +{ + assert(state >= State::HEADERS); + + if (state != State::HEADERS) + return; + + state = State::BODY; + + long status = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); + + handler.OnHeaders(status, std::move(headers)); +} + +void +CurlResponseHandlerAdapter::FinishBody() +{ + FinishHeaders(); + + if (state != State::BODY) + return; + + state = State::CLOSED; + handler.OnEnd(); +} + +void +CurlResponseHandlerAdapter::Done(CURLcode result) noexcept +{ + try { + if (result != CURLE_OK) { + StripRight(error_buffer); + const char *msg = error_buffer; + if (*msg == 0) + msg = curl_easy_strerror(result); + throw FormatRuntimeError("CURL failed: %s", msg); + } + + FinishBody(); + } catch (...) { + state = State::CLOSED; + handler.OnError(std::current_exception()); + } +} + +[[gnu::pure]] +static bool +IsResponseBoundaryHeader(StringView s) noexcept +{ + return s.size > 5 && (s.StartsWith("HTTP/") || + /* the proprietary "ICY 200 OK" is + emitted by Shoutcast */ + s.StartsWith("ICY 2")); +} + +inline void +CurlResponseHandlerAdapter::HeaderFunction(StringView s) noexcept +{ + if (state > State::HEADERS) + return; + + if (IsResponseBoundaryHeader(s)) { + /* this is the boundary to a new response, for example + after a redirect */ + headers.clear(); + return; + } + + const char *header = s.data; + const char *end = StripRight(header, header + s.size); + + const char *value = s.Find(':'); + if (value == nullptr) + return; + + std::string name(header, value); + std::transform(name.begin(), name.end(), name.begin(), + static_cast(ToLowerASCII)); + + /* skip the colon */ + + ++value; + + /* strip the value */ + + value = StripLeft(value, end); + end = StripRight(value, end); + + headers.emplace(std::move(name), std::string(value, end)); +} + +std::size_t +CurlResponseHandlerAdapter::_HeaderFunction(char *ptr, std::size_t size, + std::size_t nmemb, + void *stream) noexcept +{ + CurlResponseHandlerAdapter &c = *(CurlResponseHandlerAdapter *)stream; + + size *= nmemb; + + c.HeaderFunction({ptr, size}); + return size; +} + +inline std::size_t +CurlResponseHandlerAdapter::DataReceived(const void *ptr, + std::size_t received_size) noexcept +{ + assert(received_size > 0); + + try { + FinishHeaders(); + handler.OnData({ptr, received_size}); + return received_size; + } catch (CurlResponseHandler::Pause) { + return CURL_WRITEFUNC_PAUSE; + } + +} + +std::size_t +CurlResponseHandlerAdapter::WriteFunction(char *ptr, std::size_t size, + std::size_t nmemb, + void *stream) noexcept +{ + CurlResponseHandlerAdapter &c = *(CurlResponseHandlerAdapter *)stream; + + size *= nmemb; + if (size == 0) + return 0; + + return c.DataReceived(ptr, size); +} diff --git a/src/lib/curl/Adapter.hxx b/src/lib/curl/Adapter.hxx new file mode 100644 index 000000000..1d26f46e6 --- /dev/null +++ b/src/lib/curl/Adapter.hxx @@ -0,0 +1,87 @@ +/* + * Copyright 2008-2021 Max Kellermann + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef CURL_ADAPTER_HXX +#define CURL_ADAPTER_HXX + +#include + +#include +#include +#include + +struct StringView; +class CurlEasy; +class CurlResponseHandler; + +class CurlResponseHandlerAdapter { + CURL *curl; + + CurlResponseHandler &handler; + + std::multimap headers; + + /** error message provided by libcurl */ + char error_buffer[CURL_ERROR_SIZE]; + + enum class State { + UNINITIALISED, + HEADERS, + BODY, + CLOSED, + } state = State::UNINITIALISED; + +public: + explicit CurlResponseHandlerAdapter(CurlResponseHandler &_handler) noexcept + :handler(_handler) {} + + void Install(CurlEasy &easy); + + void Done(CURLcode result) noexcept; + +private: + void FinishHeaders(); + void FinishBody(); + + void HeaderFunction(StringView s) noexcept; + + /** called by curl when a new header is available */ + static std::size_t _HeaderFunction(char *ptr, + std::size_t size, std::size_t nmemb, + void *stream) noexcept; + + std::size_t DataReceived(const void *ptr, std::size_t size) noexcept; + + /** called by curl when new data is available */ + static std::size_t WriteFunction(char *ptr, + std::size_t size, std::size_t nmemb, + void *stream) noexcept; +}; + +#endif diff --git a/src/lib/curl/Request.cxx b/src/lib/curl/Request.cxx index 6f041af7e..7cdd10b12 100644 --- a/src/lib/curl/Request.cxx +++ b/src/lib/curl/Request.cxx @@ -30,20 +30,15 @@ #include "config.h" #include "Request.hxx" #include "Global.hxx" -#include "Handler.hxx" #include "event/Call.hxx" -#include "util/RuntimeError.hxx" #include "util/StringStrip.hxx" -#include "util/StringView.hxx" -#include "util/CharUtil.hxx" #include "Version.h" #include -#include #include -#include +#include CurlRequest::CurlRequest(CurlGlobal &_global, CurlEasy _easy, CurlResponseHandler &_handler) @@ -67,16 +62,14 @@ CurlRequest::~CurlRequest() noexcept void CurlRequest::SetupEasy() { - error_buffer[0] = 0; - easy.SetPrivate((void *)this); + + handler.Install(easy); + easy.SetUserAgent("Music Player Daemon " VERSION); - easy.SetHeaderFunction(_HeaderFunction, this); - easy.SetWriteFunction(WriteFunction, this); #if !defined(ANDROID) && !defined(_WIN32) easy.SetOption(CURLOPT_NETRC, 1L); #endif - easy.SetErrorBuffer(error_buffer); easy.SetNoProgress(); easy.SetNoSignal(); easy.SetConnectTimeout(10); @@ -138,135 +131,10 @@ CurlRequest::Resume() noexcept global.InvalidateSockets(); } -void -CurlRequest::FinishHeaders() -{ - if (state != State::HEADERS) - return; - - state = State::BODY; - - long status = 0; - easy.GetInfo(CURLINFO_RESPONSE_CODE, &status); - - handler.OnHeaders(status, std::move(headers)); -} - -void -CurlRequest::FinishBody() -{ - FinishHeaders(); - - if (state != State::BODY) - return; - - state = State::CLOSED; - handler.OnEnd(); -} - void CurlRequest::Done(CURLcode result) noexcept { Stop(); - try { - if (result != CURLE_OK) { - StripRight(error_buffer); - const char *msg = error_buffer; - if (*msg == 0) - msg = curl_easy_strerror(result); - throw FormatRuntimeError("CURL failed: %s", msg); - } - - FinishBody(); - } catch (...) { - state = State::CLOSED; - handler.OnError(std::current_exception()); - } -} - -[[gnu::pure]] -static bool -IsResponseBoundaryHeader(StringView s) noexcept -{ - return s.size > 5 && (s.StartsWith("HTTP/") || - /* the proprietary "ICY 200 OK" is - emitted by Shoutcast */ - s.StartsWith("ICY 2")); -} - -inline void -CurlRequest::HeaderFunction(StringView s) noexcept -{ - if (state > State::HEADERS) - return; - - if (IsResponseBoundaryHeader(s)) { - /* this is the boundary to a new response, for example - after a redirect */ - headers.clear(); - return; - } - - const char *header = s.data; - const char *end = StripRight(header, header + s.size); - - const char *value = s.Find(':'); - if (value == nullptr) - return; - - std::string name(header, value); - std::transform(name.begin(), name.end(), name.begin(), - static_cast(ToLowerASCII)); - - /* skip the colon */ - - ++value; - - /* strip the value */ - - value = StripLeft(value, end); - end = StripRight(value, end); - - headers.emplace(std::move(name), std::string(value, end)); -} - -std::size_t -CurlRequest::_HeaderFunction(char *ptr, std::size_t size, std::size_t nmemb, - void *stream) noexcept -{ - CurlRequest &c = *(CurlRequest *)stream; - - size *= nmemb; - - c.HeaderFunction({ptr, size}); - return size; -} - -inline std::size_t -CurlRequest::DataReceived(const void *ptr, std::size_t received_size) noexcept -{ - assert(received_size > 0); - - try { - FinishHeaders(); - handler.OnData({ptr, received_size}); - return received_size; - } catch (CurlResponseHandler::Pause) { - return CURL_WRITEFUNC_PAUSE; - } - -} - -std::size_t -CurlRequest::WriteFunction(char *ptr, std::size_t size, std::size_t nmemb, - void *stream) noexcept -{ - CurlRequest &c = *(CurlRequest *)stream; - - size *= nmemb; - if (size == 0) - return 0; - - return c.DataReceived(ptr, size); + handler.Done(result); } diff --git a/src/lib/curl/Request.hxx b/src/lib/curl/Request.hxx index fae401a4e..fcd68ca8e 100644 --- a/src/lib/curl/Request.hxx +++ b/src/lib/curl/Request.hxx @@ -31,9 +31,9 @@ #define CURL_REQUEST_HXX #include "Easy.hxx" +#include "Adapter.hxx" -#include -#include +#include struct StringView; class CurlGlobal; @@ -48,22 +48,11 @@ class CurlResponseHandler; class CurlRequest final { CurlGlobal &global; - CurlResponseHandler &handler; + CurlResponseHandlerAdapter handler; /** the curl handle */ CurlEasy easy; - enum class State { - HEADERS, - BODY, - CLOSED, - } state = State::HEADERS; - - std::multimap headers; - - /** error message provided by libcurl */ - char error_buffer[CURL_ERROR_SIZE]; - bool registered = false; public: @@ -164,18 +153,6 @@ private: void FinishHeaders(); void FinishBody(); - - std::size_t DataReceived(const void *ptr, std::size_t size) noexcept; - - void HeaderFunction(StringView s) noexcept; - - /** called by curl when new data is available */ - static std::size_t _HeaderFunction(char *ptr, std::size_t size, std::size_t nmemb, - void *stream) noexcept; - - /** called by curl when new data is available */ - static std::size_t WriteFunction(char *ptr, std::size_t size, std::size_t nmemb, - void *stream) noexcept; }; #endif diff --git a/src/lib/curl/meson.build b/src/lib/curl/meson.build index 3433997e0..284c688d8 100644 --- a/src/lib/curl/meson.build +++ b/src/lib/curl/meson.build @@ -18,6 +18,7 @@ curl = static_library( 'Init.cxx', 'Global.cxx', 'Request.cxx', + 'Adapter.cxx', 'Escape.cxx', 'Form.cxx', include_directories: inc,