diff options
Diffstat (limited to '')
-rw-r--r-- | ucb/source/ucp/webdav-curl/CurlSession.cxx | 2442 |
1 files changed, 2442 insertions, 0 deletions
diff --git a/ucb/source/ucp/webdav-curl/CurlSession.cxx b/ucb/source/ucp/webdav-curl/CurlSession.cxx new file mode 100644 index 000000000..f05797b76 --- /dev/null +++ b/ucb/source/ucp/webdav-curl/CurlSession.cxx @@ -0,0 +1,2442 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "CurlSession.hxx" + +#include "SerfLockStore.hxx" +#include "DAVProperties.hxx" +#include "UCBDeadPropertyValue.hxx" +#include "webdavresponseparser.hxx" + +#include <comphelper/attributelist.hxx> +#include <comphelper/scopeguard.hxx> +#include <comphelper/string.hxx> + +#include <o3tl/safeint.hxx> +#include <o3tl/string_view.hxx> + +#include <officecfg/Inet.hxx> + +#include <com/sun/star/beans/NamedValue.hpp> +#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/uri.hxx> +#include <rtl/strbuf.hxx> +#include <rtl/ustrbuf.hxx> +#include <config_version.h> + +#include <map> +#include <optional> +#include <tuple> + +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::Sequence<sal_Int8> const& rInData; + size_t nPosition; + UploadSource(uno::Sequence<sal_Int8> const& i_rInData) + : rInData(i_rInData) + , nPosition(0) + { + } +}; + +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; +} + +auto GetErrorStringMulti(CURLMcode const mc) -> OString +{ + return OString::Concat("(") + OString::number(sal_Int32(mc)) + ") " + curl_multi_strerror(mc); +} + +/// represent an option to be passed to curl_easy_setopt() +struct CurlOption +{ + CURLoption const Option; + enum class Type + { + Pointer, + Long, + CurlOffT + }; + Type const Tag; + union { + void const* const pValue; + long /*const*/ lValue; + curl_off_t /*const*/ cValue; + }; + char const* const pExceptionString; + + CurlOption(CURLoption const i_Option, void const* const i_Value, + char const* const i_pExceptionString) + : Option(i_Option) + , Tag(Type::Pointer) + , pValue(i_Value) + , pExceptionString(i_pExceptionString) + { + } + // Depending on platform, curl_off_t may be "long" or a larger type + // so cannot use overloading to distinguish these cases. + CurlOption(CURLoption const i_Option, curl_off_t const i_Value, + char const* const i_pExceptionString, Type const type = Type::Long) + : Option(i_Option) + , Tag(type) + , pExceptionString(i_pExceptionString) + { + static_assert(sizeof(long) <= sizeof(curl_off_t)); + switch (type) + { + case Type::Long: + lValue = i_Value; + break; + case Type::CurlOffT: + cValue = i_Value; + break; + default: + assert(false); + } + } +}; + +// NOBODY will prevent logging the response body in ProcessRequest() exception +// handler, so only use it if logging is disabled +const CurlOption g_NoBody{ CURLOPT_NOBODY, + sal_detail_log_report(SAL_DETAIL_LOG_LEVEL_INFO, "ucb.ucp.webdav.curl") + == SAL_DETAIL_LOG_ACTION_IGNORE + ? 1L + : 0L, + nullptr }; + +/// 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::vector<CurlOption> const m_Options; + ::http_dav_ucp::CurlUri const& m_rURI; + CURL* const m_pCurl; + +public: + explicit Guard(::std::mutex& rMutex, ::std::vector<CurlOption> const& rOptions, + ::http_dav_ucp::CurlUri const& rURI, CURL* const pCurl) + : m_Lock(rMutex, ::std::defer_lock) + , m_Options(rOptions) + , m_rURI(rURI) + , m_pCurl(pCurl) + { + Acquire(); + } + ~Guard() + { + if (m_Lock.owns_lock()) + { + Release(); + } + } + + void Acquire() + { + assert(!m_Lock.owns_lock()); + m_Lock.lock(); + for (auto const& it : m_Options) + { + CURLcode rc(CURL_LAST); // warning C4701 + if (it.Tag == CurlOption::Type::Pointer) + { + rc = curl_easy_setopt(m_pCurl, it.Option, it.pValue); + } + else if (it.Tag == CurlOption::Type::Long) + { + rc = curl_easy_setopt(m_pCurl, it.Option, it.lValue); + } + else if (it.Tag == CurlOption::Type::CurlOffT) + { + rc = curl_easy_setopt(m_pCurl, it.Option, it.cValue); + } + else + { + assert(false); + } + if (it.pExceptionString != nullptr) + { + if (rc != CURLE_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", + "set " << it.pExceptionString << " failed: " << GetErrorString(rc)); + throw ::http_dav_ucp::DAVException( + ::http_dav_ucp::DAVException::DAV_SESSION_CREATE, + ::http_dav_ucp::ConnectionEndPointString(m_rURI.GetHost(), + m_rURI.GetPort())); + } + } + else // many of the options cannot fail + { + assert(rc == CURLE_OK); + } + } + } + void Release() + { + assert(m_Lock.owns_lock()); + for (auto const& it : m_Options) + { + CURLcode rc(CURL_LAST); // warning C4701 + if (it.Tag == CurlOption::Type::Pointer) + { + rc = curl_easy_setopt(m_pCurl, it.Option, nullptr); + } + else if (it.Tag == CurlOption::Type::Long) + { + rc = curl_easy_setopt(m_pCurl, it.Option, 0L); + } + else if (it.Tag == CurlOption::Type::CurlOffT) + { + rc = curl_easy_setopt(m_pCurl, it.Option, curl_off_t(-1)); + } + else + { + assert(false); + } + assert(rc == CURLE_OK); + (void)rc; + } + m_Lock.unlock(); + } +}; + +} // namespace + +namespace http_dav_ucp +{ +// libcurl callbacks: + +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: + SAL_INFO("ucb.ucp.webdav.curl", + "CURLINFO_HEADER_IN: " << handle << ": " << OString(data, size)); + return 0; + case CURLINFO_HEADER_OUT: + { + // unlike IN, this is all headers in one call + OString tmp(data, size); + sal_Int32 const start(tmp.indexOf("Authorization: ")); + if (start != -1) + { + sal_Int32 const end(tmp.indexOf("\r\n", start)); + assert(end != -1); + sal_Int32 const len(SAL_N_ELEMENTS("Authorization: ") - 1); + tmp = tmp.replaceAt( + start + len, end - start - len, + OStringConcatenation(OString::number(end - start - len) + " bytes redacted")); + } + SAL_INFO("ucb.ucp.webdav.curl", "CURLINFO_HEADER_OUT: " << handle << ": " << tmp); + return 0; + } + 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; +} + +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 + } + // always write, for exception handler in ProcessRequest() + // 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); + size_t const nBytes(size * nitems); + size_t nRet(0); + try + { + assert(pSource->nPosition <= o3tl::make_unsigned(pSource->rInData.getLength())); + nRet = ::std::min<size_t>(pSource->rInData.getLength() - pSource->nPosition, nBytes); + ::std::memcpy(buffer, pSource->rInData.getConstArray() + pSource->nPosition, nRet); + pSource->nPosition += 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, + uno::Sequence<beans::NamedValue> const& rFlags, + ::ucbhelper::InternetProxyDecider const& rProxyDecider) + : DAVSession(rpFactory) + , m_xContext(xContext) + , m_Flags(rFlags) + , m_URI(rURI) + , m_Proxy(rProxyDecider.getProxy(m_URI.GetScheme(), m_URI.GetHost(), m_URI.GetPort())) +{ + assert(m_URI.GetScheme() == "http" || m_URI.GetScheme() == "https"); + m_pCurlMulti.reset(curl_multi_init()); + if (!m_pCurlMulti) + { + SAL_WARN("ucb.ucp.webdav.curl", "curl_multi_init failed"); + throw DAVException(DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort())); + } + 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); + // Make sure a User-Agent header is always included, as at least + // en.wikipedia.org:80 forces back 403 "Scripts should use an informative + // User-Agent string with contact information, or they may be IP-blocked + // without notice" otherwise: + OString const useragent( + OString::Concat("LibreOffice " LIBO_VERSION_DOTTED " denylistedbackend/") + + ::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 1 + // 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()); + // 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())); + } + auto const readTimeout(officecfg::Inet::Settings::ReadTimeout::get()); + m_nReadTimeout = ::std::max<int>(20, ::std::min<long>(readTimeout, 180)) * 1000; + // 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); + assert(rc == CURLE_OK); + // tdf#149921 by default, with schannel (WNT) connection fails if revocation + // lists cannot be checked; try to limit the checking to when revocation + // lists can actually be retrieved (usually not the case for self-signed CA) +#if CURL_AT_LEAST_VERSION(7, 70, 0) + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_SSL_OPTIONS, CURLSSLOPT_REVOKE_BEST_EFFORT); + assert(rc == CURLE_OK); + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_PROXY_SSL_OPTIONS, CURLSSLOPT_REVOKE_BEST_EFFORT); + assert(rc == CURLE_OK); +#endif + // set this initially, may be overwritten during authentication + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPAUTH, CURLAUTH_ANY); + assert(rc == CURLE_OK); // ANY is always available + // always set CURLOPT_PROXY to suppress proxy detection in libcurl + 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)); + } + if (!m_Proxy.aName.isEmpty()) + { + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_PROXYPORT, static_cast<long>(m_Proxy.nPort)); + assert(rc == CURLE_OK); + // 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 + } + auto const it(::std::find_if(m_Flags.begin(), m_Flags.end(), + [](auto const& rFlag) { return rFlag.Name == "KeepAlive"; })); + if (it != m_Flags.end() && it->Value.get<bool>()) + { + // neon would close the connection from ne_end_request(), this seems + // to be the equivalent and not CURLOPT_TCP_KEEPALIVE + rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_FORBID_REUSE, 1L); + assert(rc == CURLE_OK); + } +} + +CurlSession::~CurlSession() {} + +auto CurlSession::CanUse(OUString const& rURI, uno::Sequence<beans::NamedValue> const& rFlags) + -> bool +{ + try + { + CurlUri const uri(rURI); + + return m_URI.GetScheme() == uri.GetScheme() && m_URI.GetHost() == uri.GetHost() + && m_URI.GetPort() == uri.GetPort() && m_Flags == rFlags; + } + 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 +{ + // note: abort() was a no-op since OOo 3.2 and before that it crashed. + bool expected(false); + // it would be pointless to lock m_Mutex here as the other thread holds it + if (m_AbortFlag.compare_exchange_strong(expected, true)) + { + // This function looks safe to call without m_Mutex as long as the + // m_pCurlMulti handle is not destroyed, and the caller must own a ref + // to this object which keeps it alive; it should cause poll to return. + curl_multi_wakeup(m_pCurlMulti.get()); + } +} + +/// this is just a bunch of static member functions called from CurlSession +struct CurlProcessor +{ + static auto URIReferenceToURI(CurlSession& rSession, OUString const& rURIReference) -> CurlUri; + + static auto ProcessRequestImpl( + CurlSession& rSession, CurlUri const& rURI, OUString const& rMethod, + curl_slist* pRequestHeaderList, uno::Reference<io::XOutputStream> const* pxOutStream, + uno::Sequence<sal_Int8> const* pInData, + ::std::pair<::std::vector<OUString> const&, DAVResource&> const* pRequestedHeaders, + ResponseHeaders& rHeaders) -> void; + + static auto ProcessRequest( + CurlSession& rSession, CurlUri const& rURI, OUString const& rMethod, + ::std::vector<CurlOption> const& rOptions, DAVRequestEnvironment const* pEnv, + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, 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, CurlUri const& rURI, 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, OUString const& rSourceURIReference, + ::std::u16string_view rDestinationURI, DAVRequestEnvironment const& rEnv, + bool isOverwrite, char const* pMethod) -> void; + + static auto Lock(CurlSession& rSession, CurlUri const& rURI, DAVRequestEnvironment const* pEnv, + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> + pRequestHeaderList, + uno::Reference<io::XInputStream> const* pxInStream) + -> ::std::vector<::std::pair<ucb::Lock, sal_Int32>>; + + static auto Unlock(CurlSession& rSession, CurlUri const& rURI, + DAVRequestEnvironment const* pEnv) -> void; +}; + +auto CurlProcessor::URIReferenceToURI(CurlSession& rSession, OUString const& rURIReference) + -> CurlUri +{ + // No need to acquire rSession.m_Mutex because accessed members are const. + if (rSession.UsesProxy()) + // very odd, but see DAVResourceAccess::getRequestURI() :-/ + { + assert(rURIReference.startsWith("http://") || rURIReference.startsWith("https://")); + return CurlUri(rURIReference); + } + else + { + assert(rURIReference.startsWith("/")); + return rSession.m_URI.CloneWithRelativeRefPathAbsolute(rURIReference); + } +} + +/// main function to initiate libcurl requests +auto CurlProcessor::ProcessRequestImpl( + CurlSession& rSession, CurlUri const& rURI, OUString const& rMethod, + curl_slist* const pRequestHeaderList, + uno::Reference<io::XOutputStream> const* const pxOutStream, + uno::Sequence<sal_Int8> const* const pInData, + ::std::pair<::std::vector<OUString> const&, DAVResource&> const* const pRequestedHeaders, + ResponseHeaders& rHeaders) -> void +{ + ::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 (pInData) + { + 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); + } + }); + + if (pRequestHeaderList) + { + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPHEADER, pRequestHeaderList); + assert(rc == CURLE_OK); + (void)rc; + } + + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CURLU, rURI.GetCURLU()); + assert(rc == CURLE_OK); // can't fail since 7.63.0 + + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HEADERDATA, &rHeaders); + assert(rc == CURLE_OK); + ::std::optional<DownloadTarget> oDownloadTarget; + if (pxOutStream) + { + oDownloadTarget.emplace(*pxOutStream, rHeaders); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_WRITEDATA, &*oDownloadTarget); + assert(rc == CURLE_OK); + } + ::std::optional<UploadSource> oUploadSource; + if (pInData) + { + oUploadSource.emplace(*pInData); + rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_READDATA, &*oUploadSource); + assert(rc == CURLE_OK); + } + rSession.m_ErrorBuffer[0] = '\0'; + + // note: easy handle must be added for *every* transfer! + // otherwise it gets stuck in MSTATE_MSGSENT forever after 1st transfer + auto mc = curl_multi_add_handle(rSession.m_pCurlMulti.get(), rSession.m_pCurl.get()); + if (mc != CURLM_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", + "curl_multi_add_handle failed: " << GetErrorStringMulti(mc)); + throw DAVException( + DAVException::DAV_SESSION_CREATE, + ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort())); + } + ::comphelper::ScopeGuard const gg([&]() { + mc = curl_multi_remove_handle(rSession.m_pCurlMulti.get(), rSession.m_pCurl.get()); + if (mc != CURLM_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", + "curl_multi_remove_handle failed: " << GetErrorStringMulti(mc)); + } + }); + + // this is where libcurl actually does something + rc = CURL_LAST; // clear current value + int nRunning; + do + { + mc = curl_multi_perform(rSession.m_pCurlMulti.get(), &nRunning); + if (mc != CURLM_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", + "curl_multi_perform failed: " << GetErrorStringMulti(mc)); + throw DAVException( + DAVException::DAV_HTTP_CONNECT, + ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort())); + } + if (nRunning == 0) + { // short request like HEAD on loopback could be done in first call + break; + } + int nFDs; + mc = curl_multi_poll(rSession.m_pCurlMulti.get(), nullptr, 0, rSession.m_nReadTimeout, + &nFDs); + if (mc != CURLM_OK) + { + SAL_WARN("ucb.ucp.webdav.curl", "curl_multi_poll failed: " << GetErrorStringMulti(mc)); + throw DAVException( + DAVException::DAV_HTTP_CONNECT, + ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort())); + } + if (rSession.m_AbortFlag.load()) + { // flag was set by abort() -> not sure what exception to throw? + throw DAVException(DAVException::DAV_HTTP_ERROR, "abort() was called", 0); + } + } while (nRunning != 0); + // there should be exactly 1 CURLMsg now, but the interface is + // extensible so future libcurl versions could yield additional things + do + { + CURLMsg const* const pMsg = curl_multi_info_read(rSession.m_pCurlMulti.get(), &nRunning); + if (pMsg && pMsg->msg == CURLMSG_DONE) + { + assert(pMsg->easy_handle == rSession.m_pCurl.get()); + rc = pMsg->data.result; + } + else + { + SAL_WARN("ucb.ucp.webdav.curl", "curl_multi_info_read unexpected result"); + } + } while (nRunning != 0); + + // 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_UNSUPPORTED_PROTOCOL: + throw DAVException(DAVException::DAV_UNSUPPORTED); + 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_FAILED_INIT: +#if CURL_AT_LEAST_VERSION(7, 69, 0) + case CURLE_QUIC_CONNECT_ERROR: +#endif + 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(rHeaders, pRequestedHeaders); + } + else + { + // create message containing the HTTP method and response status line + OUString statusLine("\n" + rMethod + "\n=>\n"); + if (!rHeaders.HeaderFields.empty() && !rHeaders.HeaderFields.back().first.empty() + && rHeaders.HeaderFields.back().first.front().startsWith("HTTP")) + { + statusLine += ::rtl::OStringToOUString( + ::o3tl::trim(rHeaders.HeaderFields.back().first.front()), + RTL_TEXTENCODING_ASCII_US); + } + switch (statusCode) + { + 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) + { + // Sharepoint 2016 workaround: contains unencoded U+0020 + OUString const redirectURL(::rtl::Uri::encode( + pRedirectURL + ? OUString(pRedirectURL, strlen(pRedirectURL), RTL_TEXTENCODING_UTF8) + : OUString(), + rtl_UriCharClassUric, rtl_UriEncodeKeepEscapes, RTL_TEXTENCODING_UTF8)); + + throw DAVException(DAVException::DAV_HTTP_REDIRECT, redirectURL); + } + [[fallthrough]]; + } + default: + throw DAVException(DAVException::DAV_HTTP_ERROR, statusLine, statusCode); + } + } + + if (pxOutStream) + { + (*pxOutStream)->closeOutput(); // signal EOF + } +} + +static auto TryRemoveExpiredLockToken(CurlSession& rSession, CurlUri const& rURI, + DAVRequestEnvironment const* const pEnv) -> bool +{ + if (!pEnv) + { + // caller was a NonInteractive_*LOCK function anyway, its caller is LockStore + return false; + } + OUString const* const pToken(g_Init.LockStore.getLockTokenForURI(rURI.GetURI(), nullptr)); + if (!pToken) + { + return false; + } + try + { + // determine validity of existing lock via lockdiscovery request + ::std::vector<OUString> const propertyNames{ DAVProperties::LOCKDISCOVERY }; + ::std::vector<ucb::Lock> locks; + ::std::tuple<::std::vector<OUString> const&, ::std::vector<DAVResource>* const, + ::std::vector<ucb::Lock>* const> const args(propertyNames, nullptr, &locks); + + CurlProcessor::PropFind(rSession, rURI, DAVZERO, &args, nullptr, *pEnv); + + // https://datatracker.ietf.org/doc/html/rfc4918#section-15.8 + // The response MAY not contain tokens, but hopefully it + // will if client is properly authenticated. + if (::std::any_of(locks.begin(), locks.end(), [pToken](ucb::Lock const& rLock) { + return ::std::any_of( + rLock.LockTokens.begin(), rLock.LockTokens.end(), + [pToken](OUString const& rToken) { return *pToken == rToken; }); + })) + { + return false; // still have the lock + } + + SAL_INFO("ucb.ucp.webdav.curl", + "lock token expired, removing: " << rURI.GetURI() << " " << *pToken); + g_Init.LockStore.removeLock(rURI.GetURI()); + return true; + } + catch (DAVException const&) + { + return false; // ignore, the caller already has a better exception + } +} + +auto CurlProcessor::ProcessRequest( + CurlSession& rSession, CurlUri const& rURI, OUString const& rMethod, + ::std::vector<CurlOption> const& rOptions, DAVRequestEnvironment const* const pEnv, + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, 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"); + } + } + } + + uno::Sequence<sal_Int8> data; + if (pxInStream) + { + uno::Reference<io::XSeekable> const xSeekable(*pxInStream, uno::UNO_QUERY); + if (xSeekable.is()) + { + auto const len(xSeekable->getLength() - xSeekable->getPosition()); + if ((**pxInStream).readBytes(data, len) != len) + { + throw uno::RuntimeException("short readBytes"); + } + } + else + { + ::std::vector<uno::Sequence<sal_Int8>> bufs; + bool isDone(false); + do + { + bufs.emplace_back(); + isDone = (**pxInStream).readSomeBytes(bufs.back(), 65536) == 0; + } while (!isDone); + sal_Int32 nSize(0); + for (auto const& rBuf : bufs) + { + if (o3tl::checked_add(nSize, rBuf.getLength(), nSize)) + { + throw std::bad_alloc(); // too large for Sequence + } + } + data.realloc(nSize); + size_t nCopied(0); + for (auto const& rBuf : bufs) + { + ::std::memcpy(data.getArray() + nCopied, rBuf.getConstArray(), rBuf.getLength()); + nCopied += rBuf.getLength(); // can't overflow + } + } + } + + // Clear flag before transfer starts; only a transfer started before + // calling abort() will be aborted, not one started later. + rSession.m_AbortFlag.store(false); + + Guard guard(rSession.m_Mutex, rOptions, rURI, rSession.m_pCurl.get()); + + // authentication data may be in the URI, or requested via XInteractionHandler + 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 rURI instead +#if 0 + CurlUri const uri(pEnv->m_aRequestURI); +#endif + // note: due to parsing bug pwd didn't work in previous webdav ucps + if (pEnv && !rSession.m_isAuthenticated + && (!rURI.GetUser().isEmpty() || !rURI.GetPassword().isEmpty())) + { + oAuth.emplace(rURI.GetUser(), rURI.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 = rURI.GetURI(); + pRequestedHeaders->second.properties.clear(); + } + } + bool isRetry(false); + bool isFallbackHTTP10(false); + int nAuthRequests(0); + int nAuthRequestsProxy(0); + + // libcurl does not have an authentication callback so handle auth + // related status codes and requesting credentials via this loop + do + { + isRetry = false; + + // re-check m_isAuthenticated flags every time, could have been set + // by re-entrant call + if (oAuth && !rSession.m_isAuthenticated) + { + OString const utf8UserName(OUStringToOString(oAuth->UserName, RTL_TEXTENCODING_UTF8)); + auto 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 && !rSession.m_isAuthenticatedProxy) + { + OString const utf8UserName( + OUStringToOString(oAuthProxy->UserName, RTL_TEXTENCODING_UTF8)); + auto 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()); + // always pass a stream for debug logging, buffer the result body + uno::Reference<io::XSequenceOutputStream> const xSeqOutStream( + io::SequenceOutputStream::create(rSession.m_xContext)); + uno::Reference<io::XOutputStream> const xTempOutStream(xSeqOutStream); + assert(xTempOutStream.is()); + + try + { + ProcessRequestImpl(rSession, rURI, rMethod, pRequestHeaderList.get(), &xTempOutStream, + pxInStream ? &data : nullptr, pRequestedHeaders, headers); + if (pxOutStream) + { // only copy to result stream if transfer was successful + (*pxOutStream)->writeBytes(xSeqOutStream->getWrittenBytes()); + (*pxOutStream)->closeOutput(); // signal EOF + } + } + catch (DAVException const& rException) + { + // log start of request body if there was any + auto const bytes(xSeqOutStream->getWrittenBytes()); + auto const len(::std::min<sal_Int32>(bytes.getLength(), 10000)); + SAL_INFO("ucb.ucp.webdav.curl", + "DAVException; (first) " << len << " bytes of data received:"); + if (0 < len) + { + OStringBuffer buf(len); + for (sal_Int32 i = 0; i < len; ++i) + { + if (bytes[i] < 0x20) // also if negative + { + static char const hexDigit[16] = { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + buf.append("\\x"); + buf.append(hexDigit[static_cast<sal_uInt8>(bytes[i]) >> 4]); + buf.append(hexDigit[bytes[i] & 0x0F]); + } + else + { + buf.append(static_cast<char>(bytes[i])); + } + } + SAL_INFO("ucb.ucp.webdav.curl", buf.makeStringAndClear()); + } + + // error handling part 3: special HTTP status codes + // that require unlocking m_Mutex to handle + if (rException.getError() == DAVException::DAV_HTTP_ERROR) + { + auto const statusCode(rException.getStatus()); + switch (statusCode) + { + case SC_LOCKED: + { + guard.Release(); // release m_Mutex before accessing LockStore + if (g_Init.LockStore.getLockTokenForURI(rURI.GetURI(), nullptr)) + { + throw DAVException(DAVException::DAV_LOCKED_SELF); + } + else // locked by third party + { + throw DAVException(DAVException::DAV_LOCKED); + } + break; + } + case SC_PRECONDITION_FAILED: + case SC_BAD_REQUEST: + { + guard.Release(); // release m_Mutex before accessing LockStore + // Not obvious but apparently these codes may indicate + // the expiration of a lock. + // Initiate a new request *outside* ProcessRequestImpl + // *after* rGuard.unlock() to avoid messing up m_pCurl state. + if (TryRemoveExpiredLockToken(rSession, rURI, pEnv)) + { + throw DAVException(DAVException::DAV_LOCK_EXPIRED); + } + break; + } + case SC_UNAUTHORIZED: + case SC_PROXY_AUTHENTICATION_REQUIRED: + { + auto& rnAuthRequests(statusCode == SC_UNAUTHORIZED ? nAuthRequests + : nAuthRequestsProxy); + if (rnAuthRequests == 10) + { + SAL_INFO("ucb.ucp.webdav.curl", "aborting authentication after " + << rnAuthRequests << " attempts"); + } + else 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); + auto const rc = curl_easy_getinfo(rSession.m_pCurl.get(), + statusCode == SC_UNAUTHORIZED + ? CURLINFO_HTTPAUTH_AVAIL + : CURLINFO_PROXYAUTH_AVAIL, + &authAvail); + assert(rc == CURLE_OK); + (void)rc; + // only allow SystemCredentials once - the + // PasswordContainer may have stored it in the + // Config (TrySystemCredentialsFirst or + // AuthenticateUsingSystemCredentials) and then it + // will always force its use no matter how hopeless + bool const isSystemCredSupported((authAvail & authSystem) != 0 + && rnAuthRequests == 0); + ++rnAuthRequests; + + // Ask user via XInteractionHandler. + // Warning: This likely runs an event loop which may + // end up calling back into this instance, so all + // changes to m_pCurl must be undone now and + // restored after return. + guard.Release(); + + auto const ret = pEnv->m_xAuthListener->authenticate( + oRealm ? *oRealm : "", + statusCode == SC_UNAUTHORIZED ? rSession.m_URI.GetHost() + : rSession.m_Proxy.aName, + userName, passWord, isSystemCredSupported); + + if (ret == 0) + { + // NTLM may either use a password requested + // from the user, or from the system via SSPI + // so i guess it should not be disabled here + // regardless of the state of the system auth + // checkbox, particularly since SSPI is only + // available on WNT. + // Additionally, "Negotiate" has a "legacy" + // mode that is actually just NTLM according to + // https://curl.se/rfc/ntlm.html#ntlmHttpAuthentication + // so there's nothing in authSystem that can be + // disabled here. + roAuth.emplace(userName, passWord, + ((userName.isEmpty() && passWord.isEmpty()) + ? (authAvail & authSystem) + : authAvail)); + isRetry = true; + // Acquire is only necessary in case of success. + guard.Acquire(); + 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; + } + } + } + else if (rException.getError() == DAVException::DAV_UNSUPPORTED) + { + // tdf#152493 libcurl can't handle "Transfer-Encoding: chunked" + // in HTTP/1.1 100 Continue response. + // workaround: if HTTP/1.1 didn't work, try HTTP/1.0 + // (but fallback only once - to prevent infinite loop) + if (isFallbackHTTP10) + { + throw DAVException(DAVException::DAV_HTTP_ERROR); + } + isFallbackHTTP10 = true; + // note: this is not reset - future requests to this URI use it! + auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTP_VERSION, + CURL_HTTP_VERSION_1_0); + if (rc != CURLE_OK) + { + throw DAVException(DAVException::DAV_HTTP_ERROR); + } + SAL_INFO("ucb.ucp.webdav.curl", "attempting fallback to HTTP/1.0"); + isRetry = true; + } + if (!isRetry) + { + throw; // everything else: re-throw + } + } + } 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; + } +} + +auto CurlSession::OPTIONS(OUString const& rURIReference, + + DAVOptions& rOptions, DAVRequestEnvironment const& rEnv) -> void +{ + SAL_INFO("ucb.ucp.webdav.curl", "OPTIONS: " << rURIReference); + + rOptions.init(); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + ::std::vector<OUString> const headerNames{ "allow", "dav" }; + DAVResource result; + ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(headerNames, result); + + ::std::vector<CurlOption> const options{ + g_NoBody, { CURLOPT_CUSTOMREQUEST, "OPTIONS", "CURLOPT_CUSTOMREQUEST" } + }; + + CurlProcessor::ProcessRequest(*this, uri, "OPTIONS", options, &rEnv, nullptr, nullptr, nullptr, + &headers); + + for (auto const& it : result.properties) + { + OUString value; + it.Value >>= value; + SAL_INFO("ucb.ucp.webdav.curl", "OPTIONS: header: " << it.Name << ": " << value); + if (it.Name.equalsIgnoreAsciiCase("allow")) + { + rOptions.setAllowedMethods(value); + } + else if (it.Name.equalsIgnoreAsciiCase("dav")) + { + // see <http://tools.ietf.org/html/rfc4918#section-10.1>, + // <http://tools.ietf.org/html/rfc4918#section-18>, + // and <http://tools.ietf.org/html/rfc7230#section-3.2> + // we detect the class (1, 2 and 3), other elements (token, URL) + // are not used for now + auto const list(::comphelper::string::convertCommaSeparated(value)); + for (OUString const& v : list) + { + if (v == "1") + { + rOptions.setClass1(); + } + else if (v == "2") + { + rOptions.setClass2(); + } + else if (v == "3") + { + rOptions.setClass3(); + } + } + } + } + if (rOptions.isClass2() || rOptions.isClass3()) + { + if (g_Init.LockStore.getLockTokenForURI(uri.GetURI(), nullptr)) + { + rOptions.setLocked(); + } + } +} + +auto CurlProcessor::PropFind( + CurlSession& rSession, CurlUri const& rURI, 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)); + + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList; + 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"); + } + + uno::Reference<io::XSequenceOutputStream> const xSeqOutStream( + io::SequenceOutputStream::create(rSession.m_xContext)); + uno::Reference<io::XOutputStream> const xRequestOutStream(xSeqOutStream); + 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(); + + uno::Reference<io::XInputStream> const xRequestInStream( + io::SequenceInputStream::createStreamFromSequence(rSession.m_xContext, + xSeqOutStream->getWrittenBytes())); + assert(xRequestInStream.is()); + + curl_off_t const len(xSeqOutStream->getWrittenBytes().getLength()); + + ::std::vector<CurlOption> const options{ + { CURLOPT_UPLOAD, 1L, nullptr }, + { CURLOPT_CUSTOMREQUEST, "PROPFIND", "CURLOPT_CUSTOMREQUEST" }, + // note: Sharepoint cannot handle "Transfer-Encoding: chunked" + { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT } + }; + + // 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, rURI, "PROPFIND", options, &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); + for (DAVResource& it : *::std::get<1>(*o_pRequestedProperties)) + { + // caller will give these uris to CurlUri so can't be relative + if (it.uri.startsWith("/")) + { + try + { + it.uri = rSession.m_URI.CloneWithRelativeRefPathAbsolute(it.uri).GetURI(); + } + catch (DAVException const&) + { + SAL_INFO("ucb.ucp.webdav.curl", + "PROPFIND: exception parsing uri " << it.uri); + } + } + } + } + else + { + *::std::get<2>(*o_pRequestedProperties) = parseWebDAVLockResponse(xResponseInStream); + } + } +} + +// DAV methods +auto CurlSession::PROPFIND(OUString const& rURIReference, Depth const depth, + ::std::vector<OUString> const& rPropertyNames, + ::std::vector<DAVResource>& o_rResources, + DAVRequestEnvironment const& rEnv) -> void +{ + SAL_INFO("ucb.ucp.webdav.curl", "PROPFIND: " << rURIReference << " " << depth); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + ::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, uri, 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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + return CurlProcessor::PropFind(*this, uri, 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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + // TODO: either set CURLOPT_INFILESIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList; + pList.reset(curl_slist_append(pList.release(), "Content-Type: application/xml")); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + // generate XML document for PROPPATCH + uno::Reference<io::XSequenceOutputStream> const xSeqOutStream( + io::SequenceOutputStream::create(m_xContext)); + uno::Reference<io::XOutputStream> const xRequestOutStream(xSeqOutStream); + 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)) + { + ::std::optional<::std::pair<OUString, OUString>> const oProp( + UCBDeadPropertyValue::toXML(rPropValue.value)); + if (oProp) + { + xWriter->startElement("ucbprop", nullptr); + xWriter->startElement("type", nullptr); + xWriter->characters(oProp->first); + xWriter->endElement("type"); + xWriter->startElement("value", nullptr); + xWriter->characters(oProp->second); + xWriter->endElement("value"); + xWriter->endElement("ucbprop"); + } + } + 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(); + + uno::Reference<io::XInputStream> const xRequestInStream( + io::SequenceInputStream::createStreamFromSequence(m_xContext, + xSeqOutStream->getWrittenBytes())); + assert(xRequestInStream.is()); + + curl_off_t const len(xSeqOutStream->getWrittenBytes().getLength()); + + ::std::vector<CurlOption> const options{ + { CURLOPT_UPLOAD, 1L, nullptr }, + { CURLOPT_CUSTOMREQUEST, "PROPPATCH", "CURLOPT_CUSTOMREQUEST" }, + // note: Sharepoint cannot handle "Transfer-Encoding: chunked" + { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT } + }; + + CurlProcessor::ProcessRequest(*this, uri, "PROPPATCH", options, &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + ::std::vector<CurlOption> const options{ g_NoBody }; + + ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(rHeaderNames, + io_rResource); + + CurlProcessor::ProcessRequest(*this, uri, "HEAD", options, &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, 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()); + + ::std::vector<CurlOption> const options{ { CURLOPT_HTTPGET, 1L, nullptr } }; + + CurlProcessor::ProcessRequest(*this, uri, "GET", options, &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + ::std::vector<CurlOption> const options{ { CURLOPT_HTTPGET, 1L, nullptr } }; + + CurlProcessor::ProcessRequest(*this, uri, "GET", options, &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + ::std::vector<CurlOption> const options{ { CURLOPT_HTTPGET, 1L, nullptr } }; + + 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, uri, "GET", options, &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + ::std::vector<CurlOption> const options{ { CURLOPT_HTTPGET, 1L, nullptr } }; + + ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(rHeaderNames, + io_rResource); + + CurlProcessor::ProcessRequest(*this, uri, "GET", options, &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + // NextCloud silently fails with chunked encoding + uno::Reference<io::XSeekable> const xSeekable(rxInStream, uno::UNO_QUERY); + if (!xSeekable.is()) + { + throw uno::RuntimeException("TODO: not seekable"); + } + curl_off_t const len(xSeekable->getLength() - xSeekable->getPosition()); + + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList; + OUString const* const pToken(g_Init.LockStore.getLockTokenForURI(uri.GetURI(), nullptr)); + if (pToken) + { + OString const utf8If("If: " + // disabled as Sharepoint 2013 workaround, it accepts only + // "No-Tag-List", see fed2984281a85a5a2f308841ec810f218c75f2ab +#if 0 + "<" + OUStringToOString(rURIReference, RTL_TEXTENCODING_ASCII_US) + + "> " +#endif + "(<" + + OUStringToOString(*pToken, 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 + + // note: Nextcloud 20 cannot handle "Transfer-Encoding: chunked" + ::std::vector<CurlOption> const options{ + { CURLOPT_UPLOAD, 1L, nullptr }, // libcurl won't upload without setting this + { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT } + }; + + CurlProcessor::ProcessRequest(*this, uri, "PUT", options, &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + // TODO: either set CURLOPT_POSTFIELDSIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, 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"); + } + + ::std::vector<CurlOption> const options{ { CURLOPT_POST, 1L, nullptr } }; + + uno::Reference<io::XSequenceOutputStream> const xSeqOutStream( + io::SequenceOutputStream::create(m_xContext)); + uno::Reference<io::XOutputStream> const xResponseOutStream(xSeqOutStream); + assert(xResponseOutStream.is()); + + CurlProcessor::ProcessRequest(*this, uri, "POST", options, &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, + OUString const& rReferer, uno::Reference<io::XInputStream> const& rxInStream, + uno::Reference<io::XOutputStream>& rxOutStream, + DAVRequestEnvironment const& rEnv) -> void +{ + SAL_INFO("ucb.ucp.webdav.curl", "POST: " << rURIReference); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + // TODO: either set CURLOPT_POSTFIELDSIZE_LARGE or chunked? + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, 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"); + } + + ::std::vector<CurlOption> const options{ { CURLOPT_POST, 1L, nullptr } }; + + CurlProcessor::ProcessRequest(*this, uri, "POST", options, &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + ::std::vector<CurlOption> const options{ + g_NoBody, { CURLOPT_CUSTOMREQUEST, "MKCOL", "CURLOPT_CUSTOMREQUEST" } + }; + + CurlProcessor::ProcessRequest(*this, uri, "MKCOL", options, &rEnv, nullptr, nullptr, nullptr, + nullptr); +} + +auto CurlProcessor::MoveOrCopy(CurlSession& rSession, OUString const& rSourceURIReference, + ::std::u16string_view const rDestinationURI, + DAVRequestEnvironment const& rEnv, bool const isOverwrite, + char const* const pMethod) -> void +{ + CurlUri const uriSource(CurlProcessor::URIReferenceToURI(rSession, rSourceURIReference)); + + OString const utf8Destination("Destination: " + + OUStringToOString(rDestinationURI, RTL_TEXTENCODING_ASCII_US)); + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, 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"); + } + + ::std::vector<CurlOption> const options{ + g_NoBody, { CURLOPT_CUSTOMREQUEST, pMethod, "CURLOPT_CUSTOMREQUEST" } + }; + + CurlProcessor::ProcessRequest(rSession, uriSource, OUString::createFromAscii(pMethod), options, + &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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + ::std::vector<CurlOption> const options{ + g_NoBody, { CURLOPT_CUSTOMREQUEST, "DELETE", "CURLOPT_CUSTOMREQUEST" } + }; + + CurlProcessor::ProcessRequest(*this, uri, "DESTROY", options, &rEnv, nullptr, nullptr, nullptr, + nullptr); +} + +auto CurlProcessor::Lock( + CurlSession& rSession, CurlUri const& rURI, DAVRequestEnvironment const* const pEnv, + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> + pRequestHeaderList, + uno::Reference<io::XInputStream> const* const pxRequestInStream) + -> ::std::vector<::std::pair<ucb::Lock, sal_Int32>> +{ + curl_off_t len(0); + if (pxRequestInStream) + { + uno::Reference<io::XSeekable> const xSeekable(*pxRequestInStream, uno::UNO_QUERY); + assert(xSeekable.is()); + len = xSeekable->getLength(); + } + + ::std::vector<CurlOption> const options{ + { CURLOPT_UPLOAD, 1L, nullptr }, + { CURLOPT_CUSTOMREQUEST, "LOCK", "CURLOPT_CUSTOMREQUEST" }, + // note: Sharepoint cannot handle "Transfer-Encoding: chunked" + { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT } + }; + + // 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, rURI, "LOCK", options, 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 " << rURI.GetURI()); + + 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 " + << rURI.GetURI()); + 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); + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + if (g_Init.LockStore.getLockTokenForURI(uri.GetURI(), &rLock)) + { + // already have a lock that covers the requirement + // TODO: maybe use DAV:lockdiscovery to ensure it's valid + return; + } + + // note: no m_Mutex lock needed here, only in CurlProcessor::Lock() + + // generate XML document for acquiring new LOCK + uno::Reference<io::XSequenceOutputStream> const xSeqOutStream( + io::SequenceOutputStream::create(m_xContext)); + uno::Reference<io::XOutputStream> const xRequestOutStream(xSeqOutStream); + 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(); + + uno::Reference<io::XInputStream> const xRequestInStream( + io::SequenceInputStream::createStreamFromSequence(m_xContext, + xSeqOutStream->getWrittenBytes())); + assert(xRequestInStream.is()); + + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList; + 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, uri, &rEnv, ::std::move(pList), &xRequestInStream); + + for (auto const& rAcquiredLock : acquiredLocks) + { + g_Init.LockStore.addLock(uri.GetURI(), rAcquiredLock.first, + rAcquiredLock.first.LockTokens[0], this, rAcquiredLock.second); + SAL_INFO("ucb.ucp.webdav.curl", "created LOCK for " << rURIReference); + } +} + +auto CurlProcessor::Unlock(CurlSession& rSession, CurlUri const& rURI, + DAVRequestEnvironment const* const pEnv) -> void +{ + OUString const* const pToken(g_Init.LockStore.getLockTokenForURI(rURI.GetURI(), nullptr)); + if (!pToken) + { + SAL_WARN("ucb.ucp.webdav.curl", "attempt to unlock but not locked"); + throw DAVException(DAVException::DAV_NOT_LOCKED); + } + OString const utf8LockToken("Lock-Token: <" + + OUStringToOString(*pToken, RTL_TEXTENCODING_ASCII_US) + ">"); + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList( + curl_slist_append(nullptr, utf8LockToken.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + ::std::vector<CurlOption> const options{ { CURLOPT_CUSTOMREQUEST, "UNLOCK", + "CURLOPT_CUSTOMREQUEST" } }; + + CurlProcessor::ProcessRequest(rSession, rURI, "UNLOCK", options, pEnv, ::std::move(pList), + nullptr, nullptr, nullptr); +} + +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() + + CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); + + CurlProcessor::Unlock(*this, uri, &rEnv); + + g_Init.LockStore.removeLock(uri.GetURI()); +} + +auto CurlSession::NonInteractive_LOCK(OUString const& rURI, ::std::u16string_view const rLockToken, + sal_Int32& o_rLastChanceToSendRefreshRequest, + bool& o_rIsAuthFailed) -> bool +{ + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK: " << rURI); + + // note: no m_Mutex lock needed here, only in CurlProcessor::Lock() + + try + { + CurlUri const uri(rURI); + ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList( + curl_slist_append(nullptr, "Timeout: Second-180")); + + assert(!rLockToken.empty()); // LockStore is the caller + OString const utf8If("If: (<" + OUStringToOString(rLockToken, RTL_TEXTENCODING_ASCII_US) + + ">)"); + pList.reset(curl_slist_append(pList.release(), utf8If.getStr())); + if (!pList) + { + throw uno::RuntimeException("curl_slist_append failed"); + } + + auto const acquiredLocks + = CurlProcessor::Lock(*this, uri, nullptr, ::std::move(pList), nullptr); + + SAL_WARN_IF(1 < acquiredLocks.size(), "ucb.ucp.webdav.curl", + "multiple locks acquired on refresh for " << rURI); + if (!acquiredLocks.empty()) + { + o_rLastChanceToSendRefreshRequest = acquiredLocks.begin()->second; + } + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK succeeded on " << rURI); + return true; + } + catch (DAVException const& rException) + { + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK failed on " << rURI); + switch (rException.getError()) + { + case DAVException::DAV_HTTP_AUTH: + case DAVException::DAV_HTTP_NOAUTH: + o_rIsAuthFailed = true; + break; + default: + break; + } + return false; + } + catch (...) + { + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK failed on " << rURI); + return false; + } +} + +auto CurlSession::NonInteractive_UNLOCK(OUString const& rURI) -> void +{ + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK: " << rURI); + + // note: no m_Mutex lock needed here, only in CurlProcessor::Unlock() + + try + { + CurlUri const uri(rURI); + + CurlProcessor::Unlock(*this, uri, nullptr); + + // the only caller is the dtor of the LockStore, don't call remove! + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK succeeded on " << rURI); + } + catch (...) + { + SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK failed on " << rURI); + } +} + +} // namespace http_dav_ucp + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |