ucb/Library_ucpdav1.mk | 4 ucb/source/ucp/webdav-curl/CurlSession.cxx | 1751 +++++++++++++++++++- ucb/source/ucp/webdav-curl/CurlSession.hxx | 31 ucb/source/ucp/webdav-curl/CurlUri.hxx | 2 ucb/source/ucp/webdav-curl/DAVSessionFactory.cxx | 4 ucb/source/ucp/webdav-curl/UCBDeadPropertyValue.cxx | 1 6 files changed, 1773 insertions(+), 20 deletions(-)
New commits: commit d024b594e556ff9061930389230dba4625bd5405 Author: Michael Stahl <[email protected]> AuthorDate: Fri Sep 17 18:39:47 2021 +0200 Commit: Michael Stahl <[email protected]> CommitDate: Mon Nov 1 18:17:07 2021 +0100 ucb: webdav-curl: implement CurlSession Change-Id: Ib99ccc517f36db1bf98900a79685d510f2940e5d Reviewed-on: https://gerrit.libreoffice.org/c/core/+/122269 Tested-by: Jenkins Reviewed-by: Michael Stahl <[email protected]> diff --git a/ucb/Library_ucpdav1.mk b/ucb/Library_ucpdav1.mk index 2809a49cac54..bedea713b179 100644 --- a/ucb/Library_ucpdav1.mk +++ b/ucb/Library_ucpdav1.mk @@ -35,6 +35,10 @@ $(eval $(call gb_Library_use_externals,ucpdav1,\ curl \ )) +$(eval $(call gb_Library_use_custom_headers,ucpdav1,\ + officecfg/registry \ +)) + $(eval $(call gb_Library_add_exception_objects,ucpdav1,\ ucb/source/ucp/webdav-curl/ContentProperties \ ucb/source/ucp/webdav-curl/CurlSession \ diff --git a/ucb/source/ucp/webdav-curl/CurlSession.cxx b/ucb/source/ucp/webdav-curl/CurlSession.cxx index fcbf87a0ea75..c6ee6ae78a09 100644 --- a/ucb/source/ucp/webdav-curl/CurlSession.cxx +++ b/ucb/source/ucp/webdav-curl/CurlSession.cxx @@ -9,23 +9,1107 @@ #include "CurlSession.hxx" +#include "SerfLockStore.hxx" +#include "DAVProperties.hxx" +#include "webdavresponseparser.hxx" + +#include <comphelper/attributelist.hxx> +#include <comphelper/scopeguard.hxx> + +#include <officecfg/Inet.hxx> + +#include <com/sun/star/io/Pipe.hpp> +#include <com/sun/star/io/SequenceInputStream.hpp> +#include <com/sun/star/io/SequenceOutputStream.hpp> +#include <com/sun/star/xml/sax/Writer.hpp> + +#include <osl/time.h> +#include <sal/log.hxx> +#include <rtl/strbuf.hxx> +#include <rtl/ustrbuf.hxx> +#include <config_version.h> + +#include <map> +#include <optional> +#include <tuple> +#include <vector> + +using namespace ::com::sun::star; + +namespace +{ +/// globals container +struct Init +{ + /// note: LockStore has its own mutex and calls CurlSession from its thread + /// so don't call LockStore with m_Mutex held to prevent deadlock. + ::http_dav_ucp::SerfLockStore LockStore; + + Init() + { + if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) + { + assert(!"curl_global_init failed"); + } + } + // do not call curl_global_cleanup() - this is not the only client of curl +}; +Init g_Init; + +struct ResponseHeaders +{ + ::std::vector<::std::pair<::std::vector<OString>, ::std::optional<long>>> HeaderFields; + CURL* pCurl; + ResponseHeaders(CURL* const i_pCurl) + : pCurl(i_pCurl) + { + } +}; + +auto GetResponseCode(ResponseHeaders const& rHeaders) -> ::std::optional<long> +{ + return (rHeaders.HeaderFields.empty()) ? ::std::optional<long>{} + : rHeaders.HeaderFields.back().second; +} + +struct DownloadTarget +{ + uno::Reference<io::XOutputStream> xOutStream; + ResponseHeaders const& rHeaders; + DownloadTarget(uno::Reference<io::XOutputStream> const& i_xOutStream, + ResponseHeaders const& i_rHeaders) + : xOutStream(i_xOutStream) + , rHeaders(i_rHeaders) + { + } +}; + +struct UploadSource +{ + uno::Reference<io::XInputStream> xInStream; + ResponseHeaders const& rHeaders; + UploadSource(uno::Reference<io::XInputStream> const& i_xInStream, + ResponseHeaders const& i_rHeaders) + : xInStream(i_xInStream) + , rHeaders(i_rHeaders) + { + } +}; + +/// combined guard class to ensure things are released in correct order, +/// particularly in ProcessRequest() error handling +class Guard +{ +private: + /// mutex *first* because m_oGuard requires it + ::std::unique_lock<::std::mutex> m_Lock; + ::std::optional<::comphelper::ScopeGuard<::std::function<void()>>> m_oGuard; + +public: + explicit Guard(::std::mutex& rMutex) + : m_Lock(rMutex) + { + } + template <class Func> + explicit Guard(::std::mutex& rMutex, Func&& rFunc) + : m_Lock(rMutex) + , m_oGuard(::std::move(rFunc)) + { + } +#if 0 + void unlock() + { + m_oGuard.reset(); + m_Lock.unlock(); + } +#endif +}; + +} // namespace + namespace http_dav_ucp { -CurlSession::CurlSession(::rtl::Reference<DAVSessionFactory> const& rpFactory, OUString const& rURI, +static auto GetErrorString(CURLcode const rc, char const* const pErrorBuffer = nullptr) -> OString +{ + char const* const pMessage( // static fallback + (pErrorBuffer && pErrorBuffer[0] != '\0') ? pErrorBuffer : curl_easy_strerror(rc)); + return OString::Concat("(") + OString::number(sal_Int32(rc)) + ") " + pMessage; +} + + // libcurl callbacks: + +#if OSL_DEBUG_LEVEL > 0 +static int debug_callback(CURL* handle, curl_infotype type, char* data, size_t size, + void* /*userdata*/) +{ + char const* pType(nullptr); + switch (type) + { + case CURLINFO_TEXT: + SAL_INFO("ucb.ucp.webdav.curl", "debug log: " << handle << ": " << data); + return 0; + case CURLINFO_HEADER_IN: + pType = "CURLINFO_HEADER_IN"; + break; + case CURLINFO_HEADER_OUT: + pType = "CURLINFO_HEADER_OUT"; + break; + case CURLINFO_DATA_IN: + pType = "CURLINFO_DATA_IN"; + break; + case CURLINFO_DATA_OUT: + pType = "CURLINFO_DATA_OUT"; + break; + case CURLINFO_SSL_DATA_IN: + pType = "CURLINFO_SSL_DATA_IN"; + break; + case CURLINFO_SSL_DATA_OUT: + pType = "CURLINFO_SSL_DATA_OUT"; + break; + default: + SAL_WARN("ucb.ucp.webdav.curl", "unexpected debug log type"); + return 0; + } + SAL_INFO("ucb.ucp.webdav.curl", "debug log: " << handle << ": " << pType << " " << size); + return 0; +} +#endif + +static size_t write_callback(char* const ptr, size_t const size, size_t const nmemb, + void* const userdata) +{ + auto* const pTarget(static_cast<DownloadTarget*>(userdata)); + if (!pTarget) // looks like ~every request may have a response body + { + return nmemb; + } + assert(size == 1); // says the man page + (void)size; + assert(pTarget->xOutStream.is()); + auto const oResponseCode(GetResponseCode(pTarget->rHeaders)); + if (!oResponseCode) + { + return 0; // that is an error + } + if (200 <= *oResponseCode && *oResponseCode < 300) + { + try + { + uno::Sequence<sal_Int8> const data(reinterpret_cast<sal_Int8*>(ptr), nmemb); + pTarget->xOutStream->writeBytes(data); + } + catch (...) + { + SAL_WARN("ucb.ucp.webdav.curl", "exception in write_callback"); + return 0; // error + } + } + // else: ignore the body? CurlSession will check the status eventually + return nmemb; +} + +static size_t read_callback(char* const buffer, size_t const size, size_t const nitems, + void* const userdata) +{ + auto* const pSource(static_cast<UploadSource*>(userdata)); + assert(pSource); + assert(pSource->xInStream.is()); + size_t const nBytes(size * nitems); + size_t nRet(0); + try + { + uno::Sequence<sal_Int8> data; + data.realloc(nBytes); + nRet = pSource->xInStream->readSomeBytes(data, nBytes); + ::std::memcpy(buffer, data.getConstArray(), nRet); + } + catch (...) + { + SAL_WARN("ucb.ucp.webdav.curl", "exception in read_callback"); + return CURL_READFUNC_ABORT; // error + } + return nRet; +} + +static size_t header_callback(char* const buffer, size_t const size, size_t const nitems, + void* const userdata) +{ + auto* const pHeaders(static_cast<ResponseHeaders*>(userdata)); + assert(pHeaders); +#if 0 + if (!pHeaders) // TODO maybe not needed in every request? not sure + { + return nitems; + } +#endif + assert(size == 1); // says the man page + (void)size; + try + { + if (nitems <= 2) + { + // end of header, body follows... + if (pHeaders->HeaderFields.empty()) + { + SAL_WARN("ucb.ucp.webdav.curl", "header_callback: empty header?"); + return 0; // error + } + // unfortunately there's no separate callback when the body begins, + // so have to manually retrieve the status code here + long statusCode(SC_NONE); + auto rc = curl_easy_getinfo(pHeaders->pCurl, CURLINFO_RESPONSE_CODE, &statusCode); + assert(rc == CURLE_OK); + (void)rc; + // always put the current response code here - wasn't necessarily in this header + pHeaders->HeaderFields.back().second.emplace(statusCode); + } + else if (buffer[0] == ' ' || buffer[0] == '\t') // folded header field? + { + size_t i(0); + do + { + ++i; + } while (i == ' ' || i == '\t'); + if (pHeaders->HeaderFields.empty() || pHeaders->HeaderFields.back().second + || pHeaders->HeaderFields.back().first.empty()) + { + SAL_WARN("ucb.ucp.webdav.curl", + "header_callback: folded header field without start"); + return 0; // error + } + pHeaders->HeaderFields.back().first.back() + += OString::Concat(" ") + ::std::string_view(&buffer[i], nitems - i); + } + else + { + if (pHeaders->HeaderFields.empty() || pHeaders->HeaderFields.back().second) + { + pHeaders->HeaderFields.emplace_back(); + } + pHeaders->HeaderFields.back().first.emplace_back(OString(buffer, nitems)); + } + } + catch (...) + { + SAL_WARN("ucb.ucp.webdav.curl", "exception in header_callback"); + return 0; // error + } + return nitems; +} + +static auto ProcessHeaders(::std::vector<OString> const& rHeaders) -> ::std::map<OUString, OUString> +{ + ::std::map<OUString, OUString> ret; + for (OString const& rLine : rHeaders) + { + OString line; + if (!rLine.endsWith("\r\n", &line)) + { + SAL_WARN("ucb.ucp.webdav.curl", "invalid header field (no CRLF)"); + continue; + } + if (line.startsWith("HTTP/") // first line + || line.isEmpty()) // last line + { + continue; + } + auto const nColon(line.indexOf(':')); + if (nColon == -1) + { + { + SAL_WARN("ucb.ucp.webdav.curl", "invalid header field (no :)"); + } + continue; + } + if (nColon == 0) + { + SAL_WARN("ucb.ucp.webdav.curl", "invalid header field (empty name)"); + continue; + } + // case insensitive; must be ASCII + auto const name(::rtl::OStringToOUString(line.copy(0, nColon).toAsciiLowerCase(), + RTL_TEXTENCODING_ASCII_US)); + sal_Int32 nStart(nColon + 1); + while (nStart < line.getLength() && (line[nStart] == ' ' || line[nStart] == '\t')) + { + ++nStart; + } + sal_Int32 nEnd(line.getLength()); + while (nStart < nEnd && (line[nEnd - 1] == ' ' || line[nEnd - 1] == '\t')) + { + --nEnd; + } + // RFC 7230 says that only ASCII works reliably anyway (neon also did this) + auto const value(::rtl::OStringToOUString(line.subView(nStart, nEnd - nStart), + RTL_TEXTENCODING_ASCII_US)); + auto const it(ret.find(name)); + if (it != ret.end()) + { + it->second = it->second + "," + value; + } + else + { + ret[name] = value; + } + } + return ret; +} + +static auto ExtractRequestedHeaders( + ResponseHeaders const& rHeaders, + ::std::pair<::std::vector<OUString> const&, DAVResource&> const* const pRequestedHeaders) + -> void +{ + ::std::map<OUString, OUString> const headerMap( + ProcessHeaders(rHeaders.HeaderFields.back().first)); + if (pRequestedHeaders) + { + for (OUString const& rHeader : pRequestedHeaders->first) + { + auto const it(headerMap.find(rHeader.toAsciiLowerCase())); + if (it != headerMap.end()) + { + DAVPropertyValue value; + value.IsCaseSensitive = false; + value.Name = it->first; + value.Value <<= it->second; + pRequestedHeaders->second.properties.push_back(value); + } + } + } +} + +// this appears to be the only way to get the "realm" from libcurl +static auto ExtractRealm(ResponseHeaders const& rHeaders, char const* const pAuthHeaderName) + -> ::std::optional<OUString> +{ + ::std::map<OUString, OUString> const headerMap( + ProcessHeaders(rHeaders.HeaderFields.back().first)); + auto const it(headerMap.find(OUString::createFromAscii(pAuthHeaderName).toAsciiLowerCase())); + if (it == headerMap.end()) + { + SAL_WARN("ucb.ucp.webdav.curl", "cannot find auth header"); + return {}; + } + // It may be possible that the header contains multiple methods each with + // a different realm - extract only the first one bc the downstream API + // only supports one anyway. + // case insensitive! + auto i(it->second.toAsciiLowerCase().indexOf("realm=")); + // is optional + if (i == -1) + { + SAL_INFO("ucb.ucp.webdav.curl", "auth header has no realm"); + return {}; + } + // no whitespace allowed before or after = + i += ::std::strlen("realm="); + if (it->second.getLength() < i + 2 || it->second[i] != '\"') + { + SAL_WARN("ucb.ucp.webdav.curl", "no realm value"); + return {}; + } + ++i; + OUStringBuffer buf; + while (i < it->second.getLength() && it->second[i] != '\"') + { + if (it->second[i] == '\\') // quoted-pair escape + { + ++i; + if (it->second.getLength() <= i) + { + SAL_WARN("ucb.ucp.webdav.curl", "unterminated quoted-pair"); + return {}; + } + } + buf.append(it->second[i]); + ++i; + } + if (it->second.getLength() <= i) + { + SAL_WARN("ucb.ucp.webdav.curl", "unterminated realm"); + return {}; + } + return buf.makeStringAndClear(); +} + +CurlSession::CurlSession(uno::Reference<uno::XComponentContext> const& xContext, + ::rtl::Reference<DAVSessionFactory> const& rpFactory, OUString const& rURI, ::ucbhelper::InternetProxyDecider const& rProxyDecider) : DAVSession(rpFactory) + , m_xContext(xContext) , m_URI(rURI) - , m_rProxyDecider(rProxyDecider) + , m_Proxy(rProxyDecider.getProxy(m_URI.GetScheme(), m_URI.GetHost(), m_URI.GetPort())) { + assert(m_URI.GetScheme() == "http" || m_URI.GetScheme() == "https"); + m_pCurl.reset(curl_easy_init()); + if (!m_pCurl) + { + SAL_WARN("ucb.ucp.webdav.curl", "curl_easy_init failed"); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } + curl_version_info_data const* const pVersion(curl_version_info(CURLVERSION_NOW)); + assert(pVersion); + SAL_INFO("ucb.ucp.webdav.curl", + "curl version: " << pVersion->version << " " << pVersion->host + << " features: " << ::std::hex << pVersion->features << " ssl: " + << pVersion->ssl_version << " libz: " << pVersion->libz_version); + OString const useragent(OString::Concat("LibreOffice " LIBO_VERSION_DOTTED " curl/") + + ::std::string_view(pVersion->version, strlen(pVersion->version)) + " " + + pVersion->ssl_version); + // looks like an explicit "User-Agent" header in CURLOPT_HTTPHEADER + // will override CURLOPT_USERAGENT, see Curl_http_useragent(), so no need + // to check anything here + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_USERAGENT, useragent.getStr()); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_USERAGENT failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } + m_ErrorBuffer[0] = '\0'; + // this supposedly gives the highest quality error reporting + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_ERRORBUFFER, m_ErrorBuffer); + assert(rc == CURLE_OK); +#if OSL_DEBUG_LEVEL > 0 + // just for debugging... + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_DEBUGFUNCTION, debug_callback); + assert(rc == CURLE_OK); +#endif + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_VERBOSE, 1L); + assert(rc == CURLE_OK); + // accept any encoding supported by libcurl + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_ACCEPT_ENCODING, ""); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_ACCEPT_ENCODING failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } + auto const connectTimeout(officecfg::Inet::Settings::ConnectTimeout::get(m_xContext)); + // default is 300s + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_CONNECTTIMEOUT, + ::std::max<long>(2L, ::std::min<long>(connectTimeout, 180L))); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_CONNECTTIMEOUT failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } +#if 0 + auto const readTimeout(officecfg::Inet::Settings::ReadTimeout::get(m_xContext)); + // TODO: read timeout??? does not map to this value? +#endif + // default is infinite + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_TIMEOUT, 300L); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_TIMEOUT failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_WRITEFUNCTION, &write_callback); + assert(rc == CURLE_OK); + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_READFUNCTION, &read_callback); + assert(rc == CURLE_OK); + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HEADERFUNCTION, &header_callback); + // set this initially, may be overwritten during authentication + assert(rc == CURLE_OK); + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPAUTH, CURLAUTH_ANY); + assert(rc == CURLE_OK); // ANY is always available + if (!m_Proxy.aName.isEmpty()) + { + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_PROXYPORT, + static_cast<decltype(0L)>(m_Proxy.nPort)); + assert(rc == CURLE_OK); + OString const utf8Proxy(OUStringToOString(m_Proxy.aName, RTL_TEXTENCODING_UTF8)); + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_PROXY, utf8Proxy.getStr()); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_PROXY failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_Proxy.aName, m_Proxy.nPort)); + } + // set this initially, may be overwritten during authentication + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_PROXYAUTH, CURLAUTH_ANY); + assert(rc == CURLE_OK); // ANY is always available + } } CurlSession::~CurlSession() {} -auto CurlSession::CanUse(OUString const& rURI) -> bool {} +auto CurlSession::CanUse(OUString const& rURI) -> bool +{ + try + { + CurlUri const uri(rURI); + + return m_URI.GetScheme() == uri.GetScheme() && m_URI.GetHost() == uri.GetHost() + && m_URI.GetPort() == uri.GetPort(); + } + catch (DAVException const&) + { + return false; + } +} + +auto CurlSession::UsesProxy() -> bool +{ + assert(m_URI.GetScheme() == "http" || m_URI.GetScheme() == "https"); + return !m_Proxy.aName.isEmpty(); +} + +auto CurlSession::abort() -> void +{ + // this is using synchronous libcurl apis - no way to abort? + // might be possible with more complexity and CURLOPT_CONNECT_ONLY + // or curl_multi API, but is it worth the complexity? + // ... it looks like CURLOPT_CONNECT_ONLY would disable all HTTP handling. + // abort() was a no-op since OOo 3.2 and before that it crashed. +} + +/// this is just a bunch of static member functions called from CurlSession +struct CurlProcessor +{ + static auto ProcessRequest( + CurlSession& rSession, ::std::u16string_view rURIReference, + DAVRequestEnvironment const* pEnv, + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pRequestHeaderList, + uno::Reference<io::XOutputStream> const* pxOutStream, + uno::Reference<io::XInputStream> const* pxInStream, + ::std::pair<::std::vector<OUString> const&, DAVResource&> const* pRequestedHeaders) -> void; + + static auto + PropFind(CurlSession& rSession, ::std::u16string_view rURIReference, Depth depth, + ::std::tuple<::std::vector<OUString> const&, ::std::vector<DAVResource>* const, + ::std::vector<ucb::Lock>* const> const* o_pRequestedProperties, + ::std::vector<DAVResourceInfo>* const o_pResourceInfos, + DAVRequestEnvironment const& rEnv) -> void; + + static auto MoveOrCopy(CurlSession& rSession, ::std::u16string_view rSourceURIReference, + ::std::u16string_view rDestinationURI, DAVRequestEnvironment const& rEnv, + bool isOverwrite, char const* pMethod) -> void; + + static auto + Lock(CurlSession& rSession, OUString const& rURIReference, DAVRequestEnvironment const* pEnv, + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pRequestHeaderList, + uno::Reference<io::XInputStream> const* pxInStream) + -> ::std::vector<::std::pair<ucb::Lock, sal_Int32>>; + + static auto Unlock(CurlSession& rSession, OUString const& rURIReference, + DAVRequestEnvironment const* pEnv) -> void; +}; + +/// main function to initiate libcurl requests +auto CurlProcessor::ProcessRequest( + CurlSession& rSession, ::std::u16string_view const rURIReference, + DAVRequestEnvironment const* const pEnv, + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pRequestHeaderList, + uno::Reference<io::XOutputStream> const* const pxOutStream, + uno::Reference<io::XInputStream> const* const pxInStream, + ::std::pair<::std::vector<OUString> const&, DAVResource&> const* const pRequestedHeaders) + -> void +{ + if (pEnv) + { // add custom request headers passed by caller + for (auto const& rHeader : pEnv->m_aRequestHeaders) + { + OString const utf8Header( + OUStringToOString(rHeader.first, RTL_TEXTENCODING_ASCII_US) + ": " + + OUStringToOString(rHeader.second, RTL_TEXTENCODING_ASCII_US)); + pRequestHeaderList.reset( + curl_slist_append(pRequestHeaderList.release(), utf8Header.getStr())); + if (!pRequestHeaderList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + } + } + ::comphelper::ScopeGuard const g([&]() { + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HEADERDATA, nullptr); + assert(rc == CURLE_OK); + (void)rc; + if (pxOutStream) + { + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_WRITEDATA, nullptr); + assert(rc == CURLE_OK); + } + if (pxInStream) + { + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_READDATA, nullptr); + assert(rc == CURLE_OK); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_UPLOAD, 0L); + assert(rc == CURLE_OK); + } + if (pRequestHeaderList) + { + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPHEADER, nullptr); + assert(rc == CURLE_OK); + } + }); -auto CurlSession::UsesProxy() -> bool {} + if (pRequestHeaderList) + { + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPHEADER, + pRequestHeaderList.get()); + assert(rc == CURLE_OK); + (void)rc; + } -auto CurlSession::abort() -> void {} + ::std::unique_ptr<CURLU, deleter_from_fn<curl_url_cleanup>> const pUrl( + rSession.m_URI.CloneCURLU()); + OString const utf8URIRef(OUStringToOString(rURIReference, RTL_TEXTENCODING_UTF8)); + auto uc = curl_url_set(pUrl.get(), + // very odd, but see DAVResourceAccess::getRequestURI() :-/ + rSession.UsesProxy() ? CURLUPART_URL : CURLUPART_PATH, + utf8URIRef.getStr(), 0); + if (uc != CURLUE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "curl_url_set failed: " << uc); + throw DAVException(DAVException::DAV_INVALID_ARG); + } + + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CURLU, pUrl.get()); + assert(rc == CURLE_OK); // can't fail since 7.63.0 + + // authentication data may be in the URI, or requested via XInteractionHandler + // also + struct Auth + { + OUString UserName; + OUString PassWord; + decltype(CURLAUTH_ANY) AuthMask; ///< allowed auth methods + Auth(OUString const& rUserName, OUString const& rPassword, + decltype(CURLAUTH_ANY) const & rAuthMask) + : UserName(rUserName) + , PassWord(rPassword) + , AuthMask(rAuthMask) + { + } + }; + ::std::optional<Auth> oAuth; + ::std::optional<Auth> oAuthProxy; + if (pEnv && !rSession.m_isAuthenticatedProxy && !rSession.m_Proxy.aName.isEmpty()) + { + // the hope is that this must be a URI + CurlUri const uri(rSession.m_Proxy.aName); + if (!uri.GetUser().isEmpty() || !uri.GetPassword().isEmpty()) + { + oAuthProxy.emplace(uri.GetUser(), uri.GetPassword(), CURLAUTH_ANY); + } + } + decltype(CURLAUTH_ANY) const authSystem(CURLAUTH_NEGOTIATE | CURLAUTH_NTLM | CURLAUTH_NTLM_WB); + if (pRequestedHeaders || (pEnv && !rSession.m_isAuthenticated)) + { + // m_aRequestURI *may* be a path or *may* be URI - wtf + // TODO: why is there this m_aRequestURI and also rURIReference argument? + // ... only caller is DAVResourceAccess - always identical except MOVE/COPY + // which doesn't work if it's just a URI reference so let's just use + // rURIReference via pUrl instead +#if 0 + CurlUri const uri(pEnv->m_aRequestURI); +#endif + CurlUri const uri(*pUrl); + // note: due to parsing bug pwd didn't work in previous webdav ucps + if (pEnv && !rSession.m_isAuthenticated + && (!uri.GetUser().isEmpty() || !uri.GetPassword().isEmpty())) + { + oAuth.emplace(uri.GetUser(), uri.GetPassword(), CURLAUTH_ANY); + } + if (pRequestedHeaders) + { + // note: Previously this would be the rURIReference directly but + // that ends up in CurlUri anyway and curl is unhappy. + // But it looks like all consumers of this .uri are interested + // only in the path, so it shouldn't make a difference to give + // the entire URI when the caller extracts the path anyway. + pRequestedHeaders->second.uri = uri.GetURI(); + pRequestedHeaders->second.properties.clear(); + } + } + bool isRetry(false); + + // libcurl does not have an authentication callback so handle auth + // related status codes and requesting credentials via this loop + do + { + isRetry = false; + + if (oAuth) + { + assert(!rSession.m_isAuthenticated); + OString const utf8UserName(OUStringToOString(oAuth->UserName, RTL_TEXTENCODING_UTF8)); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_USERNAME, utf8UserName.getStr()); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_USERNAME failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_INVALID_ARG); + } + OString const utf8PassWord(OUStringToOString(oAuth->PassWord, RTL_TEXTENCODING_UTF8)); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_PASSWORD, utf8PassWord.getStr()); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_PASSWORD failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_INVALID_ARG); + } + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPAUTH, oAuth->AuthMask); + assert( + rc + == CURLE_OK); // it shouldn't be possible to reduce auth to 0 via the authSystem masks + } + + if (oAuthProxy) + { + assert(!rSession.m_isAuthenticatedProxy); + OString const utf8UserName( + OUStringToOString(oAuthProxy->UserName, RTL_TEXTENCODING_UTF8)); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_PROXYUSERNAME, + utf8UserName.getStr()); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", + "CURLOPT_PROXYUSERNAME failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_INVALID_ARG); + } + OString const utf8PassWord( + OUStringToOString(oAuthProxy->PassWord, RTL_TEXTENCODING_UTF8)); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_PROXYPASSWORD, + utf8PassWord.getStr()); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", + "CURLOPT_PROXYPASSWORD failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_INVALID_ARG); + } + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_PROXYAUTH, oAuthProxy->AuthMask); + assert( + rc + == CURLE_OK); // it shouldn't be possible to reduce auth to 0 via the authSystem masks + } + + ResponseHeaders headers(rSession.m_pCurl.get()); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HEADERDATA, &headers); + assert(rc == CURLE_OK); + ::std::optional<DownloadTarget> oDownloadTarget; + if (pxOutStream) + { + oDownloadTarget.emplace(*pxOutStream, headers); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_WRITEDATA, &*oDownloadTarget); + assert(rc == CURLE_OK); + } + ::std::optional<UploadSource> oUploadSource; + if (pxInStream) + { + oUploadSource.emplace(*pxInStream, headers); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_READDATA, &*oUploadSource); + assert(rc == CURLE_OK); + // libcurl won't upload without setting this + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_UPLOAD, 1L); + assert(rc == CURLE_OK); + } + rSession.m_ErrorBuffer[0] = '\0'; + // this is where libcurl actually does something + rc = curl_easy_perform(rSession.m_pCurl.get()); + // error handling part 1: libcurl errors + if (rc != CURLE_OK) + { + // TODO: is there any value in extracting CURLINFO_OS_ERRNO + SAL_WARN("ucb.ucp.webdav.curl", + "curl_easy_perform failed: " << GetErrorString(rc, rSession.m_ErrorBuffer)); + switch (rc) + { + case CURLE_COULDNT_RESOLVE_PROXY: + throw DAVException( + DAVException::DAV_HTTP_LOOKUP, + ConnectionEndPointString(rSession.m_Proxy.aName, rSession.m_Proxy.nPort)); + case CURLE_COULDNT_RESOLVE_HOST: + throw DAVException(DAVException::DAV_HTTP_LOOKUP, + ConnectionEndPointString(rSession.m_URI.GetHost(), + rSession.m_URI.GetPort())); + case CURLE_COULDNT_CONNECT: + case CURLE_SSL_CONNECT_ERROR: + case CURLE_SSL_CERTPROBLEM: + case CURLE_SSL_CIPHER: + case CURLE_PEER_FAILED_VERIFICATION: + case CURLE_SSL_ISSUER_ERROR: + case CURLE_SSL_PINNEDPUBKEYNOTMATCH: + case CURLE_SSL_INVALIDCERTSTATUS: + case CURLE_QUIC_CONNECT_ERROR: + throw DAVException(DAVException::DAV_HTTP_CONNECT, + ConnectionEndPointString(rSession.m_URI.GetHost(), + rSession.m_URI.GetPort())); + case CURLE_REMOTE_ACCESS_DENIED: + case CURLE_LOGIN_DENIED: + case CURLE_AUTH_ERROR: + throw DAVException(DAVException::DAV_HTTP_AUTH, // probably? + ConnectionEndPointString(rSession.m_URI.GetHost(), + rSession.m_URI.GetPort())); + case CURLE_WRITE_ERROR: + case CURLE_READ_ERROR: // error returned from our callbacks + case CURLE_OUT_OF_MEMORY: + case CURLE_ABORTED_BY_CALLBACK: + case CURLE_BAD_FUNCTION_ARGUMENT: + case CURLE_SEND_ERROR: + case CURLE_RECV_ERROR: + case CURLE_SSL_CACERT_BADFILE: + case CURLE_SSL_CRL_BADFILE: + case CURLE_RECURSIVE_API_CALL: + throw DAVException(DAVException::DAV_HTTP_FAILED, + ConnectionEndPointString(rSession.m_URI.GetHost(), + rSession.m_URI.GetPort())); + case CURLE_OPERATION_TIMEDOUT: + throw DAVException(DAVException::DAV_HTTP_TIMEOUT, + ConnectionEndPointString(rSession.m_URI.GetHost(), + rSession.m_URI.GetPort())); + default: // lots of generic errors + throw DAVException(DAVException::DAV_HTTP_ERROR, "", 0); + } + } + // error handling part 2: HTTP status codes + long statusCode(SC_NONE); + rc = curl_easy_getinfo(rSession.m_pCurl.get(), CURLINFO_RESPONSE_CODE, &statusCode); + assert(rc == CURLE_OK); + assert(statusCode != SC_NONE); // ??? should be error returned from perform? + SAL_INFO("ucb.ucp.webdav.curl", "HTTP status code: " << statusCode); + if (statusCode < 300) + { + // neon did this regardless of status or even error, which seems odd + ExtractRequestedHeaders(headers, pRequestedHeaders); + } + else + { + switch (statusCode) + { + case SC_NONE: + assert(false); // ??? should be error returned from perform? + break; + case SC_UNAUTHORIZED: + case SC_PROXY_AUTHENTICATION_REQUIRED: + { + if (pEnv && pEnv->m_xAuthListener) + { + ::std::optional<OUString> const oRealm(ExtractRealm( + headers, statusCode == SC_UNAUTHORIZED ? "WWW-Authenticate" + : "Proxy-Authenticate")); + + ::std::optional<Auth>& roAuth(statusCode == SC_UNAUTHORIZED ? oAuth + : oAuthProxy); + OUString userName(roAuth ? roAuth->UserName : OUString()); + OUString passWord(roAuth ? roAuth->PassWord : OUString()); + long authAvail(0); + rc = curl_easy_getinfo(rSession.m_pCurl.get(), + statusCode == SC_UNAUTHORIZED + ? CURLINFO_HTTPAUTH_AVAIL + : CURLINFO_PROXYAUTH_AVAIL, + &authAvail); + assert(rc == CURLE_OK); + bool const isSystemCredSupported((authAvail & authSystem) != 0); + + // ask user via XInteractionHandler + auto ret = pEnv->m_xAuthListener->authenticate( + oRealm ? *oRealm : "", + //statusCode == SC_UNAUTHORIZED ? uri.GetHost() : rSession.m_Proxy.aName, + statusCode == SC_UNAUTHORIZED ? rSession.m_URI.GetHost() + : rSession.m_Proxy.aName, + userName, passWord, isSystemCredSupported); + + if (ret == 0) + { + roAuth.emplace(userName, passWord, + authAvail + & ((userName.isEmpty() && passWord.isEmpty()) + ? authSystem + : ~authSystem)); + isRetry = true; + break; // break out of switch + } + // else: throw + } + SAL_INFO("ucb.ucp.webdav.curl", "no auth credentials provided"); + throw DAVException(DAVException::DAV_HTTP_NOAUTH, + ConnectionEndPointString(rSession.m_URI.GetHost(), + rSession.m_URI.GetPort())); + break; + } + case SC_REQUEST_TIMEOUT: + { + throw DAVException(DAVException::DAV_HTTP_TIMEOUT, + ConnectionEndPointString(rSession.m_URI.GetHost(), + rSession.m_URI.GetPort())); + break; + } + case SC_MOVED_PERMANENTLY: + case SC_MOVED_TEMPORARILY: + case SC_SEE_OTHER: + case SC_TEMPORARY_REDIRECT: + { + // could also use CURLOPT_FOLLOWLOCATION but apparently the + // upper layer wants to know about redirects? + char* pRedirectURL(nullptr); + rc = curl_easy_getinfo(rSession.m_pCurl.get(), CURLINFO_REDIRECT_URL, + &pRedirectURL); + assert(rc == CURLE_OK); + if (pRedirectURL) + { + throw DAVException(DAVException::DAV_HTTP_REDIRECT, + pRedirectURL + ? OUString(pRedirectURL, strlen(pRedirectURL), + RTL_TEXTENCODING_UTF8) + : OUString()); + } + [[fallthrough]]; + } + default: + throw DAVException(DAVException::DAV_HTTP_ERROR, "", statusCode); + } + } + } while (isRetry); + + if (oAuth) + { + // assume this worked, leave auth data as stored in m_pCurl + rSession.m_isAuthenticated = true; + } + if (oAuthProxy) + { + // assume this worked, leave auth data as stored in m_pCurl + rSession.m_isAuthenticatedProxy = true; + } + + if (pxOutStream) + { + (*pxOutStream)->closeOutput(); // signal EOF + } +} + +auto CurlProcessor::PropFind( + CurlSession& rSession, ::std::u16string_view const rURIReference, Depth const nDepth, + ::std::tuple<::std::vector<OUString> const&, ::std::vector<DAVResource>* const, + ::std::vector<ucb::Lock>* const> const* const o_pRequestedProperties, + ::std::vector<DAVResourceInfo>* const o_pResourceInfos, DAVRequestEnvironment const& rEnv) + -> void +{ + assert((o_pRequestedProperties != nullptr) != (o_pResourceInfos != nullptr)); + assert((o_pRequestedProperties == nullptr) + || (::std::get<1>(*o_pRequestedProperties) != nullptr) + != (::std::get<2>(*o_pRequestedProperties) != nullptr)); + + // TODO: either set CURLOPT_INFILESIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, "Transfer-Encoding: chunked")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + pList.reset(curl_slist_append(pList.release(), "Content-Type: application/xml")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + OString depth; + switch (nDepth) + { + case DAVZERO: + depth = "Depth: 0"; + break; + case DAVONE: + depth = "Depth: 1"; + break; + case DAVINFINITY: + depth = "Depth: infinity"; + break; + default: + assert(false); + } + pList.reset(curl_slist_append(pList.release(), depth.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + Guard g(rSession.m_Mutex, [&]() { + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CUSTOMREQUEST, nullptr); + assert(rc == CURLE_OK); + (void)rc; + }); + + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CUSTOMREQUEST, "PROPFIND"); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_CUSTOMREQUEST failed: " << GetErrorString(rc)); + throw DAVException( + DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort())); + } + + uno::Reference<io::XInputStream> const xRequestInStream(io::Pipe::create(rSession.m_xContext)); + uno::Reference<io::XOutputStream> const xRequestOutStream(xRequestInStream, uno::UNO_QUERY); + assert(xRequestInStream.is()); + assert(xRequestOutStream.is()); + uno::Reference<xml::sax::XWriter> const xWriter(xml::sax::Writer::create(rSession.m_xContext)); + xWriter->setOutputStream(xRequestOutStream); + xWriter->startDocument(); + rtl::Reference<::comphelper::AttributeList> const pAttrList(new ::comphelper::AttributeList); + pAttrList->AddAttribute("xmlns", "CDATA", "DAV:"); + xWriter->startElement("propfind", pAttrList); + if (o_pResourceInfos) + { + xWriter->startElement("propname", nullptr); + xWriter->endElement("propname"); + } + else + { + if (::std::get<0>(*o_pRequestedProperties).empty()) + { + xWriter->startElement("allprop", nullptr); + xWriter->endElement("allprop"); + } + else + { + xWriter->startElement("prop", nullptr); + for (OUString const& rName : ::std::get<0>(*o_pRequestedProperties)) + { + SerfPropName name; + DAVProperties::createSerfPropName(rName, name); + pAttrList->Clear(); + pAttrList->AddAttribute("xmlns", "CDATA", OUString::createFromAscii(name.nspace)); + xWriter->startElement(OUString::createFromAscii(name.name), pAttrList); + xWriter->endElement(OUString::createFromAscii(name.name)); + } + xWriter->endElement("prop"); + } + } + xWriter->endElement("propfind"); + xWriter->endDocument(); + xRequestOutStream->closeOutput(); + + // stream for response + uno::Reference<io::XInputStream> const xResponseInStream(io::Pipe::create(rSession.m_xContext)); + uno::Reference<io::XOutputStream> const xResponseOutStream(xResponseInStream, uno::UNO_QUERY); + assert(xResponseInStream.is()); + assert(xResponseOutStream.is()); + + CurlProcessor::ProcessRequest(rSession, rURIReference, &rEnv, ::std::move(pList), + &xResponseOutStream, &xRequestInStream, nullptr); + + if (o_pResourceInfos) + { + *o_pResourceInfos = parseWebDAVPropNameResponse(xResponseInStream); + } + else + { + if (::std::get<1>(*o_pRequestedProperties) != nullptr) + { + *::std::get<1>(*o_pRequestedProperties) + = parseWebDAVPropFindResponse(xResponseInStream); + } + else + { + *::std::get<2>(*o_pRequestedProperties) = parseWebDAVLockResponse(xResponseInStream); + } + } +} // DAV methods auto CurlSession::PROPFIND(OUString const& rURIReference, Depth const depth, @@ -33,57 +1117,318 @@ auto CurlSession::PROPFIND(OUString const& rURIReference, Depth const depth, ::std::vector<DAVResource>& o_rResources, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "PROPFIND: " << rURIReference << " " << depth); + ::std::tuple<::std::vector<OUString> const&, ::std::vector<DAVResource>* const, + ::std::vector<ucb::Lock>* const> const args(rPropertyNames, &o_rResources, + nullptr); + return CurlProcessor::PropFind(*this, rURIReference, depth, &args, nullptr, rEnv); } auto CurlSession::PROPFIND(OUString const& rURIReference, Depth const depth, ::std::vector<DAVResourceInfo>& o_rResourceInfos, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "PROPFIND: " << rURIReference << " " << depth); + return CurlProcessor::PropFind(*this, rURIReference, depth, nullptr, &o_rResourceInfos, rEnv); } auto CurlSession::PROPPATCH(OUString const& rURIReference, ::std::vector<ProppatchValue> const& rValues, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "PROPPATCH: " << rURIReference); + + //FIXME why does toXML encode stuff which parser ignores + //isUCBDeadProperty case not handled + + // TODO: either set CURLOPT_INFILESIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, "Transfer-Encoding: chunked")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + pList.reset(curl_slist_append(pList.release(), "Content-Type: application/xml")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_CUSTOMREQUEST, nullptr); + assert(rc == CURLE_OK); + (void)rc; + }); + + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_CUSTOMREQUEST, "PROPPATCH"); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_CUSTOMREQUEST failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } + + // generate XML document for PROPPATCH + uno::Reference<io::XInputStream> const xRequestInStream(io::Pipe::create(m_xContext)); + uno::Reference<io::XOutputStream> const xRequestOutStream(xRequestInStream, uno::UNO_QUERY); + assert(xRequestInStream.is()); + assert(xRequestOutStream.is()); + uno::Reference<xml::sax::XWriter> const xWriter(xml::sax::Writer::create(m_xContext)); + xWriter->setOutputStream(xRequestOutStream); + xWriter->startDocument(); + rtl::Reference<::comphelper::AttributeList> const pAttrList(new ::comphelper::AttributeList); + pAttrList->AddAttribute("xmlns", "CDATA", "DAV:"); + xWriter->startElement("propertyupdate", pAttrList); + for (ProppatchValue const& rPropValue : rValues) + { + assert(rPropValue.operation == PROPSET || rPropValue.operation == PROPREMOVE); + OUString const operation((rPropValue.operation == PROPSET) ? OUString("set") + : OUString("remove")); + xWriter->startElement(operation, nullptr); + xWriter->startElement("prop", nullptr); + SerfPropName name; + DAVProperties::createSerfPropName(rPropValue.name, name); + pAttrList->Clear(); + pAttrList->AddAttribute("xmlns", "CDATA", OUString::createFromAscii(name.nspace)); + xWriter->startElement(OUString::createFromAscii(name.name), pAttrList); + if (rPropValue.operation == PROPSET) + { + if (DAVProperties::isUCBDeadProperty(name)) + { + // TODO don't use UCBDeadPropertyValue::toXml, it's crazy + } + else + { + OUString value; + rPropValue.value >>= value; + xWriter->characters(value); + } + } + xWriter->endElement(OUString::createFromAscii(name.name)); + xWriter->endElement("prop"); + xWriter->endElement(operation); + } + xWriter->endElement("propertyupdate"); + xWriter->endDocument(); + xRequestOutStream->closeOutput(); + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, ::std::move(pList), nullptr, + &xRequestInStream, nullptr); } auto CurlSession::HEAD(OUString const& rURIReference, ::std::vector<OUString> const& rHeaderNames, DAVResource& io_rResource, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "HEAD: " << rURIReference); + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_NOBODY, 0L); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_NOBODY, 1L); + assert(rc == CURLE_OK); + (void)rc; + + ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(rHeaderNames, + io_rResource); + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, nullptr, nullptr, nullptr, &headers); } auto CurlSession::GET(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> uno::Reference<io::XInputStream> { + SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference); + + // could use either com.sun.star.io.Pipe or com.sun.star.io.SequenceInputStream? + // Pipe can just write into its XOuputStream, which is simpler. + // Both resize exponentially, so performance should be fine. + // However, Pipe doesn't implement XSeekable, which is required by filters. + + uno::Reference<io::XSequenceOutputStream> const xSeqOutStream( + io::SequenceOutputStream::create(m_xContext)); + uno::Reference<io::XOutputStream> const xResponseOutStream(xSeqOutStream); + assert(xResponseOutStream.is()); + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPGET, 0L); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPGET, 1L); + assert(rc == CURLE_OK); + (void)rc; + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, nullptr, &xResponseOutStream, + nullptr, nullptr); + + uno::Reference<io::XInputStream> const xResponseInStream( + io::SequenceInputStream::createStreamFromSequence(m_xContext, + xSeqOutStream->getWrittenBytes())); + assert(xResponseInStream.is()); + + return xResponseInStream; } auto CurlSession::GET(OUString const& rURIReference, uno::Reference<io::XOutputStream>& rxOutStream, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference); + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPGET, 0L); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPGET, 1L); + assert(rc == CURLE_OK); + (void)rc; + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, nullptr, &rxOutStream, nullptr, + nullptr); } auto CurlSession::GET(OUString const& rURIReference, ::std::vector<OUString> const& rHeaderNames, DAVResource& io_rResource, DAVRequestEnvironment const& rEnv) -> uno::Reference<io::XInputStream> { + SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference); + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPGET, 0L); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPGET, 1L); + assert(rc == CURLE_OK); + (void)rc; + + uno::Reference<io::XSequenceOutputStream> const xSeqOutStream( + io::SequenceOutputStream::create(m_xContext)); + uno::Reference<io::XOutputStream> const xResponseOutStream(xSeqOutStream); + assert(xResponseOutStream.is()); + + ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(rHeaderNames, + io_rResource); + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, nullptr, &xResponseOutStream, + nullptr, &headers); + + uno::Reference<io::XInputStream> const xResponseInStream( + io::SequenceInputStream::createStreamFromSequence(m_xContext, + xSeqOutStream->getWrittenBytes())); + assert(xResponseInStream.is()); + + return xResponseInStream; } auto CurlSession::GET(OUString const& rURIReference, uno::Reference<io::XOutputStream>& rxOutStream, ::std::vector<OUString> const& rHeaderNames, DAVResource& io_rResource, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference); + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPGET, 0L); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPGET, 1L); + assert(rc == CURLE_OK); + (void)rc; + + ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(rHeaderNames, + io_rResource); + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, nullptr, &rxOutStream, nullptr, + &headers); } auto CurlSession::PUT(OUString const& rURIReference, uno::Reference<io::XInputStream> const& rxInStream, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "PUT: " << rURIReference); + + // TODO: either set CURLOPT_INFILESIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, "Transfer-Encoding: chunked")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + // TODO: why is a *global* LockStore keyed by *path*? + OUString const token(g_Init.LockStore.getLockToken(rURIReference)); + if (!token.isEmpty()) + { + OString const utf8If("If: <" + OUStringToOString(rURIReference, RTL_TEXTENCODING_ASCII_US) + + "> (<" + OUStringToOString(token, RTL_TEXTENCODING_ASCII_US) + ">)"); + pList.reset(curl_slist_append(pList.release(), utf8If.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + } + + // lock m_Mutex after accessing global LockStore to avoid deadlock + Guard g(m_Mutex); + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, ::std::move(pList), nullptr, + &rxInStream, nullptr); } auto CurlSession::POST(OUString const& rURIReference, OUString const& rContentType, OUString const& rReferer, uno::Reference<io::XInputStream> const& rxInStream, DAVRequestEnvironment const& rEnv) -> uno::Reference<io::XInputStream> { + SAL_INFO("ucb.ucp.webdav.curl", "POST: " << rURIReference); + + // TODO: either set CURLOPT_POSTFIELDSIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, "Transfer-Encoding: chunked")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + OString const utf8ContentType("Content-Type: " + + OUStringToOString(rContentType, RTL_TEXTENCODING_ASCII_US)); + pList.reset(curl_slist_append(pList.release(), utf8ContentType.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + OString const utf8Referer("Referer: " + OUStringToOString(rReferer, RTL_TEXTENCODING_ASCII_US)); + pList.reset(curl_slist_append(pList.release(), utf8Referer.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_POST, 0L); + assert(rc == CURLE_OK); + (void)rc; + }); + + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_POST, 1L); + assert(rc == CURLE_OK); + (void)rc; + + uno::Reference<io::XSequenceOutputStream> const xSeqOutStream( + io::SequenceOutputStream::create(m_xContext)); + uno::Reference<io::XOutputStream> const xResponseOutStream(xSeqOutStream); + assert(xResponseOutStream.is()); + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, ::std::move(pList), + &xResponseOutStream, &rxInStream, nullptr); + + uno::Reference<io::XInputStream> const xResponseInStream( + io::SequenceInputStream::createStreamFromSequence(m_xContext, + xSeqOutStream->getWrittenBytes())); + assert(xResponseInStream.is()); + + return xResponseInStream; } auto CurlSession::POST(OUString const& rURIReference, OUString const& rContentType, @@ -91,47 +1436,429 @@ auto CurlSession::POST(OUString const& rURIReference, OUString const& rContentTy uno::Reference<io::XOutputStream>& rxOutStream, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "POST: " << rURIReference); + + // TODO: either set CURLOPT_POSTFIELDSIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, "Transfer-Encoding: chunked")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + OString const utf8ContentType("Content-Type: " + + OUStringToOString(rContentType, RTL_TEXTENCODING_ASCII_US)); + pList.reset(curl_slist_append(pList.release(), utf8ContentType.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + OString const utf8Referer("Referer: " + OUStringToOString(rReferer, RTL_TEXTENCODING_ASCII_US)); + pList.reset(curl_slist_append(pList.release(), utf8Referer.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_POST, 0L); + assert(rc == CURLE_OK); + (void)rc; + }); + + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_POST, 1L); + assert(rc == CURLE_OK); + (void)rc; + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, ::std::move(pList), &rxOutStream, + &rxInStream, nullptr); +} + +auto CurlSession::MKCOL(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> void +{ + SAL_INFO("ucb.ucp.webdav.curl", "MKCOL: " << rURIReference); + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_CUSTOMREQUEST, nullptr); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_CUSTOMREQUEST, "MKCOL"); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_CUSTOMREQUEST failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, nullptr, nullptr, nullptr, nullptr); } -auto CurlSession::MKCOL(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> void {} +auto CurlProcessor::MoveOrCopy(CurlSession& rSession, + ::std::u16string_view const rSourceURIReference, + ::std::u16string_view const rDestinationURI, + DAVRequestEnvironment const& rEnv, bool const isOverwrite, + char const* const pMethod) -> void +{ + OString const utf8Destination("Destination: " + + OUStringToOString(rDestinationURI, RTL_TEXTENCODING_ASCII_US)); + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, utf8Destination.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + OString const utf8Overwrite(OString::Concat("Overwrite: ") + (isOverwrite ? "T" : "F")); + pList.reset(curl_slist_append(pList.release(), utf8Overwrite.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + Guard g(rSession.m_Mutex, [&]() { + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CUSTOMREQUEST, nullptr); + assert(rc == CURLE_OK); + (void)rc; + }); + + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CUSTOMREQUEST, pMethod); + assert(rc == CURLE_OK); + (void)rc; + + CurlProcessor::ProcessRequest(rSession, rSourceURIReference, &rEnv, ::std::move(pList), nullptr, + nullptr, nullptr); +} auto CurlSession::COPY(OUString const& rSourceURIReference, OUString const& rDestinationURI, DAVRequestEnvironment const& rEnv, bool const isOverwrite) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "COPY: " << rSourceURIReference); + + return CurlProcessor::MoveOrCopy(*this, rSourceURIReference, rDestinationURI, rEnv, isOverwrite, + "COPY"); } auto CurlSession::MOVE(OUString const& rSourceURIReference, OUString const& rDestinationURI, DAVRequestEnvironment const& rEnv, bool const isOverwrite) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "MOVE: " << rSourceURIReference); + + return CurlProcessor::MoveOrCopy(*this, rSourceURIReference, rDestinationURI, rEnv, isOverwrite, + "MOVE"); } auto CurlSession::DESTROY(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "DESTROY: " << rURIReference); + + Guard g(m_Mutex, [&]() { + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_CUSTOMREQUEST, nullptr); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_CUSTOMREQUEST, "DELETE"); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_CUSTOMREQUEST failed: " << GetErrorString(rc)); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } + + CurlProcessor::ProcessRequest(*this, rURIReference, &rEnv, nullptr, nullptr, nullptr, nullptr); +} + +auto CurlProcessor::Lock( + CurlSession& rSession, OUString const& rURIReference, DAVRequestEnvironment const* const pEnv, + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pRequestHeaderList, + uno::Reference<io::XInputStream> const* const pxRequestInStream) + -> ::std::vector<::std::pair<ucb::Lock, sal_Int32>> +{ + Guard g(rSession.m_Mutex, [&]() { + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CUSTOMREQUEST, nullptr); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CUSTOMREQUEST, "LOCK"); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_CUSTOMREQUEST failed: " << GetErrorString(rc)); + throw DAVException( + DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort())); + } + + // stream for response + uno::Reference<io::XInputStream> const xResponseInStream(io::Pipe::create(rSession.m_xContext)); + uno::Reference<io::XOutputStream> const xResponseOutStream(xResponseInStream, uno::UNO_QUERY); + assert(xResponseInStream.is()); + assert(xResponseOutStream.is()); + + TimeValue startTime; + osl_getSystemTime(&startTime); + + CurlProcessor::ProcessRequest(rSession, rURIReference, pEnv, ::std::move(pRequestHeaderList), + &xResponseOutStream, pxRequestInStream, nullptr); + + ::std::vector<ucb::Lock> const acquiredLocks(parseWebDAVLockResponse(xResponseInStream)); + SAL_WARN_IF(acquiredLocks.empty(), "ucb.ucp.webdav.curl", + "could not get LOCK for " << rURIReference); + + TimeValue endTime; + osl_getSystemTime(&endTime); + auto const elapsedSeconds(endTime.Seconds - startTime.Seconds); + + // determine expiration time (seconds from endTime) for each acquired lock + ::std::vector<::std::pair<ucb::Lock, sal_Int32>> ret; + ret.reserve(acquiredLocks.size()); + for (auto const& rLock : acquiredLocks) + { + sal_Int32 lockExpirationTimeSeconds; + if (rLock.Timeout == -1) + { + lockExpirationTimeSeconds = -1; + } + else if (rLock.Timeout <= elapsedSeconds) + { + SAL_WARN("ucb.ucp.webdav.curl", + "LOCK timeout already expired when receiving LOCK response for " + << rURIReference); + lockExpirationTimeSeconds = 0; + } + else + { + lockExpirationTimeSeconds = startTime.Seconds + rLock.Timeout; + } + ret.emplace_back(rLock, lockExpirationTimeSeconds); + } + + return ret; } auto CurlSession::LOCK(OUString const& rURIReference, ucb::Lock /*const*/& rLock, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "LOCK: " << rURIReference); + + // note: no m_Mutex lock needed here, only in CurlProcessor::Lock() + + // generate XML document for acquiring new LOCK + uno::Reference<io::XInputStream> const xRequestInStream(io::Pipe::create(m_xContext)); + uno::Reference<io::XOutputStream> const xRequestOutStream(xRequestInStream, uno::UNO_QUERY); + assert(xRequestInStream.is()); + assert(xRequestOutStream.is()); + uno::Reference<xml::sax::XWriter> const xWriter(xml::sax::Writer::create(m_xContext)); + xWriter->setOutputStream(xRequestOutStream); + xWriter->startDocument(); + rtl::Reference<::comphelper::AttributeList> const pAttrList(new ::comphelper::AttributeList); + pAttrList->AddAttribute("xmlns", "CDATA", "DAV:"); + xWriter->startElement("lockinfo", pAttrList); + xWriter->startElement("lockscope", nullptr); + switch (rLock.Scope) + { + case ucb::LockScope_EXCLUSIVE: + xWriter->startElement("exclusive", nullptr); + xWriter->endElement("exclusive"); + break; + case ucb::LockScope_SHARED: + xWriter->startElement("shared", nullptr); + xWriter->endElement("shared"); + break; + default: + assert(false); + } + xWriter->endElement("lockscope"); + xWriter->startElement("locktype", nullptr); + xWriter->startElement("write", nullptr); + xWriter->endElement("write"); + xWriter->endElement("locktype"); + OUString owner; + if ((rLock.Owner >>= owner) && !owner.isEmpty()) + { + xWriter->startElement("owner", nullptr); + xWriter->characters(owner); + xWriter->endElement("owner"); + } + xWriter->endElement("lockinfo"); + xWriter->endDocument(); + xRequestOutStream->closeOutput(); + + // TODO: either set CURLOPT_INFILESIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, "Transfer-Encoding: chunked")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + pList.reset(curl_slist_append(pList.release(), "Content-Type: application/xml")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + OString depth; + switch (rLock.Depth) + { + case ucb::LockDepth_ZERO: + depth = "Depth: 0"; + break; + case ucb::LockDepth_ONE: + depth = "Depth: 1"; + break; + case ucb::LockDepth_INFINITY: + depth = "Depth: infinity"; + break; + default: + assert(false); + } + pList.reset(curl_slist_append(pList.release(), depth.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + OString timeout; + switch (rLock.Timeout) + { + case -1: + timeout = "Timeout: Infinite"; + break; + case 0: + timeout = "Timeout: Second-180"; + break; + default: + timeout = "Timeout: Second-" + OString::number(rLock.Timeout); + assert(0 < rLock.Timeout); + break; + } + pList.reset(curl_slist_append(pList.release(), timeout.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + auto const acquiredLocks + = CurlProcessor::Lock(*this, rURIReference, &rEnv, ::std::move(pList), &xRequestInStream); + + for (auto const& rAcquiredLock : acquiredLocks) + { + g_Init.LockStore.addLock(rURIReference, rAcquiredLock.first.LockTokens[0], this, + rAcquiredLock.second); + SAL_INFO("ucb.ucp.webdav.curl", "created LOCK for " << rURIReference); + } } -auto CurlSession::LOCK(OUString const& rURIReference, sal_Int64 const nTimeout, - DAVRequestEnvironment const& rEnv) -> sal_Int64 +auto CurlProcessor::Unlock(CurlSession& rSession, OUString const& rURIReference, + DAVRequestEnvironment const* const pEnv) -> void { + // TODO: why is a *global* LockStore keyed by *path*? + OUString const token(g_Init.LockStore.getLockToken(rURIReference)); + if (token.isEmpty()) + { + SAL_WARN("ucb.ucp.webdav.curl", "attempt to unlock but not locked"); + throw DAVException(DAVException::DAV_NOT_LOCKED); + } + OString const utf8LockToken("Lock-Token: <" + + OUStringToOString(token, RTL_TEXTENCODING_ASCII_US) + ">"); + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, utf8LockToken.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + // lock m_Mutex after accessing global LockStore to avoid deadlock + Guard g(rSession.m_Mutex, [&]() { + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CUSTOMREQUEST, nullptr); + assert(rc == CURLE_OK); + (void)rc; + }); + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CUSTOMREQUEST, "UNLOCK"); + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_CUSTOMREQUEST failed: " << GetErrorString(rc)); + throw DAVException( + DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort())); + } + + CurlProcessor::ProcessRequest(rSession, rURIReference, pEnv, ::std::move(pList), nullptr, + nullptr, nullptr); +} + +auto CurlSession::LOCK(OUString const& /*rURIReference*/, sal_Int64 const /*nTimeout*/, + DAVRequestEnvironment const & /*rEnv*/) -> sal_Int64 +{ + // FIXME unused? + assert(false); + return 1234567890; } auto CurlSession::UNLOCK(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> void { + SAL_INFO("ucb.ucp.webdav.curl", "UNLOCK: " << rURIReference); + + // note: no m_Mutex lock needed here, only in CurlProcessor::Unlock() + + CurlProcessor::Unlock(*this, rURIReference, &rEnv); + + g_Init.LockStore.removeLock(rURIReference); } -auto CurlSession::NonInteractive_LOCK(::std::u16string_view const rURIReference, +auto CurlSession::NonInteractive_LOCK(OUString const& rURIReference, sal_Int32& o_rLastChanceToSendRefreshRequest) -> bool { - (void)this; + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK: " << rURIReference); + + // note: no m_Mutex lock needed here, only in CurlProcessor::Lock() + + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist_free_all>> pList( + curl_slist_append(nullptr, "Timeout: Second-180")); + + // TODO: why is a *global* LockStore keyed by *path*? + OUString const token(g_Init.LockStore.getLockToken(rURIReference)); + assert(!token.isEmpty()); // LockStore is the caller + OString const utf8If("If: (<" + OUStringToOString(token, RTL_TEXTENCODING_ASCII_US) + ">)"); + pList.reset(curl_slist_append(pList.release(), utf8If.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + try + { + auto const acquiredLocks + = CurlProcessor::Lock(*this, rURIReference, nullptr, ::std::move(pList), nullptr); + + SAL_WARN_IF(1 < acquiredLocks.size(), "ucb.ucp.webdav.curl", + "multiple locks acquired on refresh for " << rURIReference); + if (!acquiredLocks.empty()) + { + o_rLastChanceToSendRefreshRequest = acquiredLocks.begin()->second; + } + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK succeeded on " << rURIReference); + return true; + } + catch (...) + { + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK failed on " << rURIReference); + return false; + } } -auto CurlSession::NonInteractive_UNLOCK(::std::u16string_view const rURIReference) -> void +auto CurlSession::NonInteractive_UNLOCK(OUString const& rURIReference) -> void { - (void)this; + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK: " << rURIReference); + + // note: no m_Mutex lock needed here, only in CurlProcessor::Unlock() + + try + { + CurlProcessor::Unlock(*this, rURIReference, nullptr); + + // the only caller is the dtor of the LockStore, don't call remove! + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK succeeded on " << rURIReference); + } + catch (...) + { + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK failed on " << rURIReference); + } } } // namespace http_dav_ucp diff --git a/ucb/source/ucp/webdav-curl/CurlSession.hxx b/ucb/source/ucp/webdav-curl/CurlSession.hxx index 22947c774420..4c33c795d940 100644 --- a/ucb/source/ucp/webdav-curl/CurlSession.hxx +++ b/ucb/source/ucp/webdav-curl/CurlSession.hxx @@ -12,16 +12,37 @@ #include "DAVSession.hxx" #include "CurlUri.hxx" +#include <curl/curl.h> + +#include <mutex> + namespace http_dav_ucp { +/// implementation of libcurl HTTP/DAV back-end class CurlSession : public DAVSession { private: - CurlUri m_URI; - ::ucbhelper::InternetProxyDecider const& m_rProxyDecider; + /// mutex required to access all other non-const members + ::std::mutex m_Mutex; + css::uno::Reference<css::uno::XComponentContext> const m_xContext; + CurlUri const m_URI; + /// buffer for libcurl detailed error messages + char m_ErrorBuffer[CURL_ERROR_SIZE]; + /// proxy is used if aName is non-empty + ::ucbhelper::InternetProxyServer const m_Proxy; + /// once authentication was successful, rely on m_pCurl's data + bool m_isAuthenticated = false; + bool m_isAuthenticatedProxy = false; + + /// libcurl easy handle + ::std::unique_ptr<CURL, deleter_from_fn<curl_easy_cleanup>> m_pCurl; + + // this class exists just to hide the implementation details in cxx file + friend struct CurlProcessor; public: - explicit CurlSession(::rtl::Reference<DAVSessionFactory> const& rpFactory, OUString const& rURI, + explicit CurlSession(css::uno::Reference<css::uno::XComponentContext> const& xContext, + ::rtl::Reference<DAVSessionFactory> const& rpFactory, OUString const& rURI, ::ucbhelper::InternetProxyDecider const& rProxyDecider); virtual ~CurlSession() override; @@ -102,9 +123,9 @@ public: virtual auto abort() -> void override; - auto NonInteractive_LOCK(::std::u16string_view rURIReference, + auto NonInteractive_LOCK(OUString const& rURIReference, sal_Int32& o_rLastChanceToSendRefreshRequest) -> bool; - auto NonInteractive_UNLOCK(::std::u16string_view rURIReference) -> void; + auto NonInteractive_UNLOCK(OUString const& rURIReference) -> void; }; } // namespace http_dav_ucp diff --git a/ucb/source/ucp/webdav-curl/CurlUri.hxx b/ucb/source/ucp/webdav-curl/CurlUri.hxx index 7418bd8a6f41..13859666bc79 100644 --- a/ucb/source/ucp/webdav-curl/CurlUri.hxx +++ b/ucb/source/ucp/webdav-curl/CurlUri.hxx @@ -59,7 +59,7 @@ public: bool operator==(CurlUri const& rOther) const; - CURLU* GetCURLU() { return m_pUrl.get(); } + CURLU* CloneCURLU() const { return curl_url_dup(m_pUrl.get()); } OUString const& GetURI() const { return m_URI; } OUString const& GetScheme() const { return m_Scheme; } OUString const& GetUser() const { return m_User; } diff --git a/ucb/source/ucp/webdav-curl/DAVSessionFactory.cxx b/ucb/source/ucp/webdav-curl/DAVSessionFactory.cxx index 5de6edd198d4..25113e2eb2a7 100644 --- a/ucb/source/ucp/webdav-curl/DAVSessionFactory.cxx +++ b/ucb/source/ucp/webdav-curl/DAVSessionFactory.cxx @@ -46,7 +46,7 @@ rtl::Reference< DAVSession > DAVSessionFactory::createDAVSession( CurlUri const aURI( inUri ); std::unique_ptr< DAVSession > xElement( - new CurlSession(this, inUri, *m_xProxyDecider) ); + new CurlSession(rxContext, this, inUri, *m_xProxyDecider) ); aIt = m_aMap.emplace( inUri, xElement.get() ).first; aIt->second->m_aContainerIt = aIt; @@ -69,7 +69,7 @@ rtl::Reference< DAVSession > DAVSessionFactory::createDAVSession( // call a little: CurlUri const aURI( inUri ); - aIt->second = new CurlSession(this, inUri, *m_xProxyDecider); + aIt->second = new CurlSession(rxContext, this, inUri, *m_xProxyDecider); aIt->second->m_aContainerIt = aIt; return aIt->second; } diff --git a/ucb/source/ucp/webdav-curl/UCBDeadPropertyValue.cxx b/ucb/source/ucp/webdav-curl/UCBDeadPropertyValue.cxx index 0b3fc082136f..987662313cfd 100644 --- a/ucb/source/ucp/webdav-curl/UCBDeadPropertyValue.cxx +++ b/ucb/source/ucp/webdav-curl/UCBDeadPropertyValue.cxx @@ -154,6 +154,7 @@ static OUString encodeValue( const OUString & rValue ) // PROPFIND: // - parser replaces < by > ==> error (not well formed) + //FIXME this violates https://www.w3.org/TR/REC-xml/#indtd OUStringBuffer aResult; const sal_Unicode * pValue = rValue.getStr();
