/* -*- 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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, ::std::optional>> HeaderFields; CURL* pCurl; ResponseHeaders(CURL* const i_pCurl) : pCurl(i_pCurl) { } }; auto GetResponseCode(ResponseHeaders const& rHeaders) -> ::std::optional { return (rHeaders.HeaderFields.empty()) ? ::std::optional{} : rHeaders.HeaderFields.back().second; } struct DownloadTarget { uno::Reference xOutStream; ResponseHeaders const& rHeaders; DownloadTarget(uno::Reference const& i_xOutStream, ResponseHeaders const& i_rHeaders) : xOutStream(i_xOutStream) , rHeaders(i_rHeaders) { } }; struct UploadSource { uno::Reference xInStream; ResponseHeaders const& rHeaders; UploadSource(uno::Reference const& i_xInStream, ResponseHeaders const& i_rHeaders) : xInStream(i_xInStream) , rHeaders(i_rHeaders) { } }; 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); } } }; /// 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 const m_Options; ::http_dav_ucp::CurlUri const& m_rURI; CURL* const m_pCurl; public: explicit Guard(::std::mutex& rMutex, ::std::vector 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: #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: SAL_INFO("ucb.ucp.webdav.curl", "CURLINFO_HEADER_IN: " << handle << ": " << OString(data, size)); return 0; case CURLINFO_HEADER_OUT: { OString tmp(data, size); if (tmp.startsWith("Authorization: ")) { tmp = "Authorization: " + OString::number(tmp.getLength() - 15) + " 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; } #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(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 const data(reinterpret_cast(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(userdata)); assert(pSource); assert(pSource->xInStream.is()); size_t const nBytes(size * nitems); size_t nRet(0); try { uno::Sequence 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(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 const& rHeaders) -> ::std::map { ::std::map 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 const&, DAVResource&> const* const pRequestedHeaders) -> void { ::std::map 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 { ::std::map 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 const& xContext, ::rtl::Reference const& rpFactory, OUString const& rURI, uno::Sequence 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 " 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(2L, ::std::min(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_xContext)); m_nReadTimeout = ::std::max(20, ::std::min(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); // 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(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 } 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()) { // 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 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, curl_slist* pRequestHeaderList, uno::Reference const* pxOutStream, uno::Reference const* pxInStream, ::std::pair<::std::vector const&, DAVResource&> const* pRequestedHeaders, ResponseHeaders& rHeaders) -> void; static auto ProcessRequest( CurlSession& rSession, CurlUri const& rURI, ::std::vector const& rOptions, DAVRequestEnvironment const* pEnv, ::std::unique_ptr> pRequestHeaderList, uno::Reference const* pxOutStream, uno::Reference const* pxInStream, ::std::pair<::std::vector const&, DAVResource&> const* pRequestedHeaders) -> void; static auto PropFind(CurlSession& rSession, CurlUri const& rURI, Depth depth, ::std::tuple<::std::vector const&, ::std::vector* const, ::std::vector* const> const* o_pRequestedProperties, ::std::vector* 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> pRequestHeaderList, uno::Reference const* pxInStream) -> ::std::vector<::std::pair>; 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, curl_slist* const pRequestHeaderList, uno::Reference const* const pxOutStream, uno::Reference const* const pxInStream, ::std::pair<::std::vector 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 (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); } }); 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 oDownloadTarget; if (pxOutStream) { oDownloadTarget.emplace(*pxOutStream, rHeaders); rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_WRITEDATA, &*oDownloadTarget); assert(rc == CURLE_OK); } ::std::optional oUploadSource; if (pxInStream) { oUploadSource.emplace(*pxInStream, rHeaders); 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'; // 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_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(rHeaders, pRequestedHeaders); } else { switch (statusCode) { case SC_NONE: assert(false); // ??? should be error returned from perform? 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) { // 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, "", 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 const propertyNames{ DAVProperties::LOCKDISCOVERY }; ::std::vector locks; ::std::tuple<::std::vector const&, ::std::vector* const, ::std::vector* 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, ::std::vector const& rOptions, DAVRequestEnvironment const* const pEnv, ::std::unique_ptr> pRequestHeaderList, uno::Reference const* const pxOutStream, uno::Reference const* const pxInStream, ::std::pair<::std::vector 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"); } } } // 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 oAuth; ::std::optional 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); // 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()); try { ProcessRequestImpl(rSession, rURI, pRequestHeaderList.get(), pxOutStream, pxInStream, pRequestedHeaders, headers); } catch (DAVException const& rException) { // 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: { if (pEnv && pEnv->m_xAuthListener) { ::std::optional const oRealm(ExtractRealm( headers, statusCode == SC_UNAUTHORIZED ? "WWW-Authenticate" : "Proxy-Authenticate")); ::std::optional& 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; bool const isSystemCredSupported((authAvail & authSystem) != 0); // 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) { roAuth.emplace(userName, passWord, authAvail & ((userName.isEmpty() && passWord.isEmpty()) ? authSystem : ~authSystem)); 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; } } } 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 const headerNames{ "allow", "dav" }; DAVResource result; ::std::pair<::std::vector const&, DAVResource&> const headers(headerNames, result); ::std::vector const options{ { CURLOPT_NOBODY, 1L, nullptr }, { CURLOPT_CUSTOMREQUEST, "OPTIONS", "CURLOPT_CUSTOMREQUEST" } }; CurlProcessor::ProcessRequest(*this, uri, 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 , // , // and // 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 const&, ::std::vector* const, ::std::vector* const> const* const o_pRequestedProperties, ::std::vector* 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> 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"); } ::std::vector const options{ { CURLOPT_CUSTOMREQUEST, "PROPFIND", "CURLOPT_CUSTOMREQUEST" } }; uno::Reference const xRequestInStream(io::Pipe::create(rSession.m_xContext)); uno::Reference const xRequestOutStream(xRequestInStream, uno::UNO_QUERY); assert(xRequestInStream.is()); assert(xRequestOutStream.is()); uno::Reference 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 const xResponseInStream(io::Pipe::create(rSession.m_xContext)); uno::Reference const xResponseOutStream(xResponseInStream, uno::UNO_QUERY); assert(xResponseInStream.is()); assert(xResponseOutStream.is()); CurlProcessor::ProcessRequest(rSession, rURI, 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 const& rPropertyNames, ::std::vector& 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 const&, ::std::vector* const, ::std::vector* 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& 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 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> 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"); } ::std::vector const options{ { CURLOPT_CUSTOMREQUEST, "PROPPATCH", "CURLOPT_CUSTOMREQUEST" } }; // generate XML document for PROPPATCH uno::Reference const xRequestInStream(io::Pipe::create(m_xContext)); uno::Reference const xRequestOutStream(xRequestInStream, uno::UNO_QUERY); assert(xRequestInStream.is()); assert(xRequestOutStream.is()); uno::Reference 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> 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(); xRequestOutStream->closeOutput(); CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, ::std::move(pList), nullptr, &xRequestInStream, nullptr); } auto CurlSession::HEAD(OUString const& rURIReference, ::std::vector 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 const options{ { CURLOPT_NOBODY, 1L, nullptr } }; ::std::pair<::std::vector const&, DAVResource&> const headers(rHeaderNames, io_rResource); CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, nullptr, nullptr, nullptr, &headers); } auto CurlSession::GET(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> uno::Reference { 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 const xSeqOutStream( io::SequenceOutputStream::create(m_xContext)); uno::Reference const xResponseOutStream(xSeqOutStream); assert(xResponseOutStream.is()); ::std::vector const options{ { CURLOPT_HTTPGET, 1L, nullptr } }; CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, nullptr, &xResponseOutStream, nullptr, nullptr); uno::Reference const xResponseInStream( io::SequenceInputStream::createStreamFromSequence(m_xContext, xSeqOutStream->getWrittenBytes())); assert(xResponseInStream.is()); return xResponseInStream; } auto CurlSession::GET(OUString const& rURIReference, uno::Reference& rxOutStream, DAVRequestEnvironment const& rEnv) -> void { SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference); CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); ::std::vector const options{ { CURLOPT_HTTPGET, 1L, nullptr } }; CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, nullptr, &rxOutStream, nullptr, nullptr); } auto CurlSession::GET(OUString const& rURIReference, ::std::vector const& rHeaderNames, DAVResource& io_rResource, DAVRequestEnvironment const& rEnv) -> uno::Reference { SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference); CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference)); ::std::vector const options{ { CURLOPT_HTTPGET, 1L, nullptr } }; uno::Reference const xSeqOutStream( io::SequenceOutputStream::create(m_xContext)); uno::Reference const xResponseOutStream(xSeqOutStream); assert(xResponseOutStream.is()); ::std::pair<::std::vector const&, DAVResource&> const headers(rHeaderNames, io_rResource); CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, nullptr, &xResponseOutStream, nullptr, &headers); uno::Reference const xResponseInStream( io::SequenceInputStream::createStreamFromSequence(m_xContext, xSeqOutStream->getWrittenBytes())); assert(xResponseInStream.is()); return xResponseInStream; } auto CurlSession::GET(OUString const& rURIReference, uno::Reference& rxOutStream, ::std::vector 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 const options{ { CURLOPT_HTTPGET, 1L, nullptr } }; ::std::pair<::std::vector const&, DAVResource&> const headers(rHeaderNames, io_rResource); CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, nullptr, &rxOutStream, nullptr, &headers); } auto CurlSession::PUT(OUString const& rURIReference, uno::Reference 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 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> 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 ::std::vector const options{ { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT } }; CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, ::std::move(pList), nullptr, &rxInStream, nullptr); } auto CurlSession::POST(OUString const& rURIReference, OUString const& rContentType, OUString const& rReferer, uno::Reference const& rxInStream, DAVRequestEnvironment const& rEnv) -> uno::Reference { 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> 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 const options{ { CURLOPT_POST, 1L, nullptr } }; uno::Reference const xSeqOutStream( io::SequenceOutputStream::create(m_xContext)); uno::Reference const xResponseOutStream(xSeqOutStream); assert(xResponseOutStream.is()); CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, ::std::move(pList), &xResponseOutStream, &rxInStream, nullptr); uno::Reference 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 const& rxInStream, uno::Reference& 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> 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 const options{ { CURLOPT_POST, 1L, nullptr } }; CurlProcessor::ProcessRequest(*this, uri, 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 const options{ { CURLOPT_NOBODY, 1L, nullptr }, { CURLOPT_CUSTOMREQUEST, "MKCOL", "CURLOPT_CUSTOMREQUEST" } }; CurlProcessor::ProcessRequest(*this, uri, 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> 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 const options{ { CURLOPT_NOBODY, 1L, nullptr }, { CURLOPT_CUSTOMREQUEST, pMethod, "CURLOPT_CUSTOMREQUEST" } }; CurlProcessor::ProcessRequest(rSession, uriSource, 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 const options{ { CURLOPT_NOBODY, 1L, nullptr }, { CURLOPT_CUSTOMREQUEST, "DELETE", "CURLOPT_CUSTOMREQUEST" } }; CurlProcessor::ProcessRequest(*this, uri, options, &rEnv, nullptr, nullptr, nullptr, nullptr); } auto CurlProcessor::Lock( CurlSession& rSession, CurlUri const& rURI, DAVRequestEnvironment const* const pEnv, ::std::unique_ptr> pRequestHeaderList, uno::Reference const* const pxRequestInStream) -> ::std::vector<::std::pair> { ::std::vector const options{ { CURLOPT_CUSTOMREQUEST, "LOCK", "CURLOPT_CUSTOMREQUEST" } }; // stream for response uno::Reference const xResponseInStream(io::Pipe::create(rSession.m_xContext)); uno::Reference const xResponseOutStream(xResponseInStream, uno::UNO_QUERY); assert(xResponseInStream.is()); assert(xResponseOutStream.is()); TimeValue startTime; osl_getSystemTime(&startTime); CurlProcessor::ProcessRequest(rSession, rURI, options, pEnv, ::std::move(pRequestHeaderList), &xResponseOutStream, pxRequestInStream, nullptr); ::std::vector 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> 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 const xRequestInStream(io::Pipe::create(m_xContext)); uno::Reference const xRequestOutStream(xRequestInStream, uno::UNO_QUERY); assert(xRequestInStream.is()); assert(xRequestOutStream.is()); uno::Reference 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> 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, 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> pList( curl_slist_append(nullptr, utf8LockToken.getStr())); if (!pList) { throw uno::RuntimeException("curl_slist_append failed"); } ::std::vector const options{ { CURLOPT_CUSTOMREQUEST, "UNLOCK", "CURLOPT_CUSTOMREQUEST" } }; CurlProcessor::ProcessRequest(rSession, rURI, 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> 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: */