1f1009aaf6
Google Calendar sometimes returns redirect requests to human-readable web sites (an "unavailable" page, a login form). This is of course bogus when the client is an automated CalDAV client. The "unavailable.html" case was already handled. Made it a bit more flexible to also catch possible variations of it (additional parameters, https instead of http). Added the https://accounts.google.com/ServiceLogin case. Not sure whether retrying will help in that case, but there's not much else that SyncEvolution can do.
913 lines
30 KiB
C++
913 lines
30 KiB
C++
/*
|
|
* Copyright (C) 2010 Patrick Ohly <patrick.ohly@gmx.de>
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#ifdef ENABLE_DAV
|
|
|
|
#include "NeonCXX.h"
|
|
#include <ne_socket.h>
|
|
#include <ne_auth.h>
|
|
#include <ne_xmlreq.h>
|
|
#include <ne_string.h>
|
|
|
|
#include <list>
|
|
#include <boost/algorithm/string/join.hpp>
|
|
#include <boost/algorithm/string/split.hpp>
|
|
#include <boost/bind.hpp>
|
|
|
|
#include <syncevo/util.h>
|
|
#include <syncevo/Logging.h>
|
|
#include <syncevo/LogRedirect.h>
|
|
#include <syncevo/SmartPtr.h>
|
|
#include <syncevo/SuspendFlags.h>
|
|
|
|
#include <sstream>
|
|
|
|
#include <dlfcn.h>
|
|
|
|
#include <syncevo/declarations.h>
|
|
SE_BEGIN_CXX
|
|
|
|
namespace Neon {
|
|
#if 0
|
|
}
|
|
#endif
|
|
|
|
std::string features()
|
|
{
|
|
std::list<std::string> res;
|
|
|
|
if (ne_has_support(NE_FEATURE_SSL)) { res.push_back("SSL"); }
|
|
if (ne_has_support(NE_FEATURE_ZLIB)) { res.push_back("ZLIB"); }
|
|
if (ne_has_support(NE_FEATURE_IPV6)) { res.push_back("IPV6"); }
|
|
if (ne_has_support(NE_FEATURE_LFS)) { res.push_back("LFS"); }
|
|
if (ne_has_support(NE_FEATURE_SOCKS)) { res.push_back("SOCKS"); }
|
|
if (ne_has_support(NE_FEATURE_TS_SSL)) { res.push_back("TS_SSL"); }
|
|
if (ne_has_support(NE_FEATURE_I18N)) { res.push_back("I18N"); }
|
|
return boost::join(res, ", ");
|
|
}
|
|
|
|
URI URI::parse(const std::string &url, bool collection)
|
|
{
|
|
ne_uri uri;
|
|
int error = ne_uri_parse(url.c_str(), &uri);
|
|
URI res = fromNeon(uri, collection);
|
|
if (!res.m_port) {
|
|
res.m_port = ne_uri_defaultport(res.m_scheme.c_str());
|
|
}
|
|
ne_uri_free(&uri);
|
|
if (error) {
|
|
SE_THROW_EXCEPTION(TransportException,
|
|
StringPrintf("invalid URL '%s' (parsed as '%s')",
|
|
url.c_str(),
|
|
res.toURL().c_str()));
|
|
}
|
|
return res;
|
|
}
|
|
|
|
URI URI::fromNeon(const ne_uri &uri, bool collection)
|
|
{
|
|
URI res;
|
|
|
|
if (uri.scheme) { res.m_scheme = uri.scheme; }
|
|
if (uri.host) { res.m_host = uri.host; }
|
|
if (uri.userinfo) { res.m_userinfo = uri.userinfo; }
|
|
if (uri.path) { res.m_path = normalizePath(uri.path, collection); }
|
|
if (uri.query) { res.m_query = uri.query; }
|
|
if (uri.fragment) { res.m_fragment = uri.fragment; }
|
|
res.m_port = uri.port;
|
|
|
|
return res;
|
|
}
|
|
|
|
URI URI::resolve(const std::string &path) const
|
|
{
|
|
ne_uri tmp[2];
|
|
ne_uri full;
|
|
memset(tmp, 0, sizeof(tmp));
|
|
tmp[0].path = const_cast<char *>(m_path.c_str());
|
|
tmp[1].path = const_cast<char *>(path.c_str());
|
|
ne_uri_resolve(tmp + 0, tmp + 1, &full);
|
|
URI res(*this);
|
|
res.m_path = full.path;
|
|
ne_uri_free(&full);
|
|
return res;
|
|
}
|
|
|
|
std::string URI::toURL() const
|
|
{
|
|
std::ostringstream buffer;
|
|
|
|
buffer << m_scheme << "://";
|
|
if (!m_userinfo.empty()) {
|
|
buffer << m_userinfo << "@";
|
|
}
|
|
buffer << m_host;
|
|
if (m_port) {
|
|
buffer << ":" << m_port;
|
|
}
|
|
buffer << m_path;
|
|
if (!m_query.empty()) {
|
|
buffer << "?" << m_query;
|
|
}
|
|
if (!m_fragment.empty()) {
|
|
buffer << "#" << m_fragment;
|
|
}
|
|
return buffer.str();
|
|
}
|
|
|
|
std::string URI::escape(const std::string &text)
|
|
{
|
|
SmartPtr<char *> tmp(ne_path_escape(text.c_str()));
|
|
// Fail gracefully. I have observed ne_path_escape returning NULL
|
|
// a couple of times, with input "%u". It makes sense, if the
|
|
// escaping fails, to just return the same string, because, well,
|
|
// it couldn't be escaped.
|
|
return tmp ? tmp.get() : text;
|
|
}
|
|
|
|
std::string URI::unescape(const std::string &text)
|
|
{
|
|
SmartPtr<char *> tmp(ne_path_unescape(text.c_str()));
|
|
// Fail gracefully. See also the similar comment for the escape() method.
|
|
return tmp ? tmp.get() : text;
|
|
}
|
|
|
|
std::string URI::normalizePath(const std::string &path, bool collection)
|
|
{
|
|
std::string res;
|
|
res.reserve(path.size() * 150 / 100);
|
|
|
|
// always start with one leading slash
|
|
res = "/";
|
|
|
|
typedef boost::split_iterator<string::const_iterator> string_split_iterator;
|
|
string_split_iterator it =
|
|
boost::make_split_iterator(path, boost::first_finder("/", boost::is_iequal()));
|
|
while (!it.eof()) {
|
|
if (it->begin() == it->end()) {
|
|
// avoid adding empty path components
|
|
++it;
|
|
} else {
|
|
std::string split(it->begin(), it->end());
|
|
// Let's have an exception here for "%u", since we use that to replace the
|
|
// actual username into the path. It's safe to ignore "%u" because it
|
|
// couldn't be in a valid URI anyway.
|
|
// TODO: we should find a neat way to remove the awareness of "%u" from
|
|
// NeonCXX.
|
|
std::string normalizedSplit = split;
|
|
if (split != "%u") {
|
|
normalizedSplit = escape(unescape(split));
|
|
}
|
|
res += normalizedSplit;
|
|
++it;
|
|
if (!it.eof()) {
|
|
res += '/';
|
|
}
|
|
}
|
|
}
|
|
if (collection && !boost::ends_with(res, "/")) {
|
|
res += '/';
|
|
}
|
|
return res;
|
|
}
|
|
|
|
std::string Status2String(const ne_status *status)
|
|
{
|
|
if (!status) {
|
|
return "<NULL status>";
|
|
}
|
|
return StringPrintf("<status %d.%d, code %d, class %d, %s>",
|
|
status->major_version,
|
|
status->minor_version,
|
|
status->code,
|
|
status->klass,
|
|
status->reason_phrase ? status->reason_phrase : "\"\"");
|
|
}
|
|
|
|
Session::Session(const boost::shared_ptr<Settings> &settings) :
|
|
m_forceAuthorizationOnce(false),
|
|
m_credentialsSent(false),
|
|
m_settings(settings),
|
|
m_debugging(false),
|
|
m_session(NULL),
|
|
m_attempt(0)
|
|
{
|
|
int logLevel = m_settings->logLevel();
|
|
if (logLevel >= 3) {
|
|
ne_debug_init(stderr,
|
|
NE_DBG_FLUSH|NE_DBG_HTTP|NE_DBG_HTTPAUTH|
|
|
(logLevel >= 4 ? NE_DBG_HTTPBODY : 0) |
|
|
(logLevel >= 5 ? (NE_DBG_LOCKS|NE_DBG_SSL) : 0)|
|
|
(logLevel >= 6 ? (NE_DBG_XML|NE_DBG_XMLPARSE) : 0)|
|
|
(logLevel >= 11 ? (NE_DBG_HTTPPLAIN) : 0));
|
|
m_debugging = true;
|
|
} else {
|
|
ne_debug_init(NULL, 0);
|
|
}
|
|
|
|
ne_sock_init();
|
|
m_uri = URI::parse(settings->getURL());
|
|
m_session = ne_session_create(m_uri.m_scheme.c_str(),
|
|
m_uri.m_host.c_str(),
|
|
m_uri.m_port);
|
|
ne_set_server_auth(m_session, getCredentials, this);
|
|
if (m_uri.m_scheme == "https") {
|
|
// neon only initializes session->ssl_context if
|
|
// using https and segfaults in ne_ssl_trust_default_ca()
|
|
// of ne_gnutls.c if ne_ssl_trust_default_ca()
|
|
// is called for non-https. So better call these
|
|
// functions only when needed.
|
|
ne_ssl_set_verify(m_session, sslVerify, this);
|
|
ne_ssl_trust_default_ca(m_session);
|
|
|
|
// hack for Yahoo: need a client certificate
|
|
ne_ssl_client_cert *cert = ne_ssl_clicert_read("client.p12");
|
|
SE_LOG_DEBUG(NULL, NULL, "client cert is %s", !cert ? "missing" : ne_ssl_clicert_encrypted(cert) ? "encrypted" : "unencrypted");
|
|
if (cert) {
|
|
if (ne_ssl_clicert_encrypted(cert)) {
|
|
if (ne_ssl_clicert_decrypt(cert, "meego")) {
|
|
SE_LOG_DEBUG(NULL, NULL, "decryption failed");
|
|
}
|
|
}
|
|
ne_ssl_set_clicert(m_session, cert);
|
|
}
|
|
}
|
|
|
|
m_proxyURL = settings->proxy();
|
|
if (m_proxyURL.empty()) {
|
|
#ifdef HAVE_LIBNEON_SYSTEM_PROXY
|
|
// hard compile-time dependency
|
|
ne_session_system_proxy(m_session, 0);
|
|
#else
|
|
// compiled against older libneon, but might run with more recent neon
|
|
typedef void (*session_system_proxy_t)(ne_session *sess, unsigned int flags);
|
|
session_system_proxy_t session_system_proxy =
|
|
(session_system_proxy_t)dlsym(RTLD_DEFAULT, "ne_session_system_proxy");
|
|
if (session_system_proxy) {
|
|
session_system_proxy(m_session, 0);
|
|
}
|
|
#endif
|
|
} else {
|
|
URI proxyuri = URI::parse(m_proxyURL);
|
|
ne_session_proxy(m_session, proxyuri.m_host.c_str(), proxyuri.m_port);
|
|
}
|
|
|
|
int seconds = settings->timeoutSeconds();
|
|
if (seconds < 0) {
|
|
seconds = 5 * 60;
|
|
}
|
|
ne_set_read_timeout(m_session, seconds);
|
|
ne_set_connect_timeout(m_session, seconds);
|
|
ne_hook_pre_send(m_session, preSendHook, this);
|
|
}
|
|
|
|
Session::~Session()
|
|
{
|
|
if (m_session) {
|
|
ne_session_destroy(m_session);
|
|
}
|
|
ne_sock_exit();
|
|
}
|
|
|
|
boost::shared_ptr<Session> Session::m_cachedSession;
|
|
|
|
boost::shared_ptr<Session> Session::create(const boost::shared_ptr<Settings> &settings)
|
|
{
|
|
URI uri = URI::parse(settings->getURL());
|
|
if (m_cachedSession &&
|
|
m_cachedSession->m_uri == uri &&
|
|
m_cachedSession->m_proxyURL == settings->proxy()) {
|
|
// reuse existing session with new settings pointer
|
|
m_cachedSession->m_settings = settings;
|
|
return m_cachedSession;
|
|
}
|
|
// create new session
|
|
m_cachedSession.reset(new Session(settings));
|
|
return m_cachedSession;
|
|
}
|
|
|
|
|
|
int Session::getCredentials(void *userdata, const char *realm, int attempt, char *username, char *password) throw()
|
|
{
|
|
try {
|
|
if (!attempt) {
|
|
// try again with credentials
|
|
Session *session = static_cast<Session *>(userdata);
|
|
std::string user, pw;
|
|
session->m_settings->getCredentials(realm, user, pw);
|
|
SyncEvo::Strncpy(username, user.c_str(), NE_ABUFSIZ);
|
|
SyncEvo::Strncpy(password, pw.c_str(), NE_ABUFSIZ);
|
|
session->m_credentialsSent = true;
|
|
SE_LOG_DEBUG(NULL, NULL, "retry request with credentials");
|
|
return 0;
|
|
} else {
|
|
// give up
|
|
return 1;
|
|
}
|
|
} catch (...) {
|
|
Exception::handle();
|
|
SE_LOG_ERROR(NULL, NULL, "no credentials for %s", realm);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
void Session::forceAuthorization(const std::string &username, const std::string &password)
|
|
{
|
|
m_forceAuthorizationOnce = true;
|
|
m_forceUsername = username;
|
|
m_forcePassword = password;
|
|
}
|
|
|
|
void Session::preSendHook(ne_request *req, void *userdata, ne_buffer *header) throw()
|
|
{
|
|
try {
|
|
static_cast<Session *>(userdata)->preSend(req, header);
|
|
} catch (...) {
|
|
Exception::handle();
|
|
}
|
|
}
|
|
|
|
void Session::preSend(ne_request *req, ne_buffer *header)
|
|
{
|
|
// sanity check: startOperation must have been called
|
|
if (m_operation.empty()) {
|
|
SE_THROW("internal error: startOperation() not called");
|
|
}
|
|
|
|
if (m_forceAuthorizationOnce) {
|
|
// only do this once
|
|
m_forceAuthorizationOnce = false;
|
|
|
|
// append "Authorization: Basic" header if not present already
|
|
if (!boost::starts_with(header->data, "Authorization:") &&
|
|
!strstr(header->data, "\nAuthorization:")) {
|
|
std::string credentials = m_forceUsername + ":" + m_forcePassword;
|
|
SmartPtr<char *> blob(ne_base64((const unsigned char *)credentials.c_str(), credentials.size()));
|
|
ne_buffer_concat(header, "Authorization: Basic ", blob.get(), "\r\n", NULL);
|
|
}
|
|
|
|
// check for acceptance of credentials later
|
|
m_credentialsSent = true;
|
|
SE_LOG_DEBUG(NULL, NULL, "forced sending credentials");
|
|
}
|
|
}
|
|
|
|
|
|
int Session::sslVerify(void *userdata, int failures, const ne_ssl_certificate *cert) throw()
|
|
{
|
|
try {
|
|
Session *session = static_cast<Session *>(userdata);
|
|
|
|
static const Flag descr[] = {
|
|
{ NE_SSL_NOTYETVALID, "certificate not yet valid" },
|
|
{ NE_SSL_EXPIRED, "certificate has expired" },
|
|
{ NE_SSL_IDMISMATCH, "hostname mismatch" },
|
|
{ NE_SSL_UNTRUSTED, "untrusted certificate" },
|
|
{ 0, NULL }
|
|
};
|
|
|
|
SE_LOG_DEBUG(NULL, NULL,
|
|
"%s: SSL verification problem: %s",
|
|
session->getURL().c_str(),
|
|
Flags2String(failures, descr).c_str());
|
|
if (!session->m_settings->verifySSLCertificate()) {
|
|
SE_LOG_DEBUG(NULL, NULL, "ignoring bad certificate");
|
|
return 0;
|
|
}
|
|
if (failures == NE_SSL_IDMISMATCH &&
|
|
!session->m_settings->verifySSLHost()) {
|
|
SE_LOG_DEBUG(NULL, NULL, "ignoring hostname mismatch");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
} catch (...) {
|
|
Exception::handle();
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
#ifdef HAVE_LIBNEON_OPTIONS
|
|
unsigned int Session::options(const std::string &path)
|
|
{
|
|
unsigned int caps;
|
|
checkError(ne_options2(m_session, path.c_str(), &caps));
|
|
return caps;
|
|
}
|
|
#endif // HAVE_LIBNEON_OPTIONS
|
|
|
|
class PropFindDeleter
|
|
{
|
|
public:
|
|
void operator () (ne_propfind_handler *handler) { if (handler) { ne_propfind_destroy(handler); } }
|
|
};
|
|
|
|
void Session::propfindURI(const std::string &path, int depth,
|
|
const ne_propname *props,
|
|
const PropfindURICallback_t &callback,
|
|
const Timespec &deadline)
|
|
{
|
|
startOperation("PROPFIND", deadline);
|
|
|
|
retry:
|
|
boost::shared_ptr<ne_propfind_handler> handler;
|
|
int error;
|
|
|
|
handler = boost::shared_ptr<ne_propfind_handler>(ne_propfind_create(m_session, path.c_str(), depth),
|
|
PropFindDeleter());
|
|
if (props != NULL) {
|
|
error = ne_propfind_named(handler.get(), props,
|
|
propsResult, const_cast<void *>(static_cast<const void *>(&callback)));
|
|
} else {
|
|
error = ne_propfind_allprop(handler.get(),
|
|
propsResult, const_cast<void *>(static_cast<const void *>(&callback)));
|
|
}
|
|
|
|
// remain valid as long as "handler" is valid
|
|
ne_request *req = ne_propfind_get_request(handler.get());
|
|
const ne_status *status = ne_get_status(req);
|
|
const char *tmp = ne_get_response_header(req, "Location");
|
|
std::string location(tmp ? tmp : "");
|
|
|
|
if (!checkError(error, status->code, status, location)) {
|
|
goto retry;
|
|
}
|
|
}
|
|
|
|
void Session::propsResult(void *userdata, const ne_uri *uri,
|
|
const ne_prop_result_set *results) throw()
|
|
{
|
|
try {
|
|
PropfindURICallback_t *callback = static_cast<PropfindURICallback_t *>(userdata);
|
|
(*callback)(URI::fromNeon(*uri), results);
|
|
} catch (...) {
|
|
Exception::handle();
|
|
}
|
|
}
|
|
|
|
void Session::propfindProp(const std::string &path, int depth,
|
|
const ne_propname *props,
|
|
const PropfindPropCallback_t &callback,
|
|
const Timespec &deadline)
|
|
{
|
|
propfindURI(path, depth, props,
|
|
boost::bind(&Session::propsIterate, _1, _2, boost::cref(callback)),
|
|
deadline);
|
|
}
|
|
|
|
void Session::propsIterate(const URI &uri, const ne_prop_result_set *results,
|
|
const PropfindPropCallback_t &callback)
|
|
{
|
|
PropIteratorUserdata_t data(&uri, &callback);
|
|
ne_propset_iterate(results,
|
|
propIterator,
|
|
&data);
|
|
}
|
|
|
|
int Session::propIterator(void *userdata,
|
|
const ne_propname *pname,
|
|
const char *value,
|
|
const ne_status *status) throw()
|
|
{
|
|
try {
|
|
const PropIteratorUserdata_t *data = static_cast<const PropIteratorUserdata_t *>(userdata);
|
|
(*data->second)(*data->first, pname, value, status);
|
|
return 0;
|
|
} catch (...) {
|
|
Exception::handle();
|
|
return 1; // abort iterating
|
|
}
|
|
}
|
|
|
|
void Session::startOperation(const string &operation, const Timespec &deadline)
|
|
{
|
|
SE_LOG_DEBUG(NULL, NULL, "starting %s, credentials %s, %s",
|
|
operation.c_str(),
|
|
m_settings->getCredentialsOkay() ? "okay" : "unverified",
|
|
deadline ? StringPrintf("deadline in %.1lfs",
|
|
(deadline - Timespec::monotonic()).duration()).c_str() :
|
|
"no deadline");
|
|
|
|
// now is a good time to check for user abort
|
|
SuspendFlags::getSuspendFlags().checkForNormal();
|
|
|
|
// remember current operation attributes
|
|
m_operation = operation;
|
|
m_deadline = deadline;
|
|
|
|
// no credentials set yet for next request
|
|
m_credentialsSent = false;
|
|
// first attempt at request
|
|
m_attempt = 0;
|
|
}
|
|
|
|
void Session::flush()
|
|
{
|
|
if (m_debugging &&
|
|
LogRedirect::redirectingStderr()) {
|
|
// flush stderr and wait a bit: this might help to get
|
|
// the redirected output via LogRedirect
|
|
fflush(stderr);
|
|
Sleep(0.001);
|
|
}
|
|
}
|
|
|
|
bool Session::checkError(int error, int code, const ne_status *status, const string &location,
|
|
const std::set<int> *expectedCodes)
|
|
{
|
|
flush();
|
|
SuspendFlags &s = SuspendFlags::getSuspendFlags();
|
|
|
|
// unset operation, set it again only if the same operation is going to be retried
|
|
string operation = m_operation;
|
|
m_operation = "";
|
|
|
|
// determine error description, may be made more specific below
|
|
string descr;
|
|
if (code) {
|
|
descr = StringPrintf("%s: Neon error code %d, HTTP status %d: %s",
|
|
operation.c_str(),
|
|
error, code,
|
|
ne_get_error(m_session));
|
|
|
|
} else {
|
|
descr = StringPrintf("%s: Neon error code %d, no HTTP status: %s",
|
|
operation.c_str(),
|
|
error,
|
|
ne_get_error(m_session));
|
|
}
|
|
// true for specific errors which might go away after a retry
|
|
bool retry = false;
|
|
|
|
// detect redirect
|
|
if ((error == NE_ERROR || error == NE_OK) &&
|
|
(code >= 300 && code <= 399)) {
|
|
// special case Google: detect redirect to temporary error page
|
|
// and retry; same for redirect to login page
|
|
if (boost::starts_with(location, "http://www.google.com/googlecalendar/unavailable.html") ||
|
|
boost::starts_with(location, "https://www.google.com/googlecalendar/unavailable.html") ||
|
|
boost::starts_with(location, "https://accounts.google.com/ServiceLogin")) {
|
|
retry = true;
|
|
} else {
|
|
SE_THROW_EXCEPTION_2(RedirectException,
|
|
StringPrintf("%s: %d status: redirected to %s",
|
|
operation.c_str(),
|
|
code,
|
|
location.c_str()),
|
|
code,
|
|
location);
|
|
}
|
|
}
|
|
|
|
switch (error) {
|
|
case NE_OK:
|
|
// request itself completed, but might still have resulted in bad status
|
|
if (expectedCodes &&
|
|
expectedCodes->find(code) != expectedCodes->end()) {
|
|
// return to caller immediately as if we had succeeded,
|
|
// without throwing an exception and without retrying
|
|
return true;
|
|
}
|
|
if (code &&
|
|
(code < 200 || code >= 300)) {
|
|
if (status) {
|
|
descr = StringPrintf("%s: bad HTTP status: %s",
|
|
operation.c_str(),
|
|
Status2String(status).c_str());
|
|
} else {
|
|
descr = StringPrintf("%s: bad HTTP status: %d",
|
|
operation.c_str(),
|
|
code);
|
|
}
|
|
if (code >= 500 && code <= 599) {
|
|
// potentially temporary server failure, may try again
|
|
retry = true;
|
|
}
|
|
} else {
|
|
// all fine, no retry necessary: clean up
|
|
|
|
// remember completion time of request
|
|
m_lastRequestEnd = Timespec::monotonic();
|
|
|
|
// assume that credentials were valid, if sent
|
|
if (m_credentialsSent) {
|
|
SE_LOG_DEBUG(NULL, NULL, "credentials accepted");
|
|
m_settings->setCredentialsOkay(true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
break;
|
|
case NE_AUTH:
|
|
// tell caller what kind of transport error occurred
|
|
code = STATUS_UNAUTHORIZED;
|
|
descr = StringPrintf("%s: Neon error code %d = NE_AUTH, HTTP status %d: %s",
|
|
operation.c_str(),
|
|
error, code,
|
|
ne_get_error(m_session));
|
|
break;
|
|
case NE_ERROR:
|
|
if (code) {
|
|
descr = StringPrintf("%s: Neon error code %d: %s",
|
|
operation.c_str(),
|
|
error,
|
|
ne_get_error(m_session));
|
|
if (code >= 500 && code <= 599) {
|
|
// potentially temporary server failure, may try again
|
|
retry = true;
|
|
}
|
|
} else if (descr.find("Secure connection truncated") != descr.npos ||
|
|
descr.find("decryption failed or bad record mac") != descr.npos) {
|
|
// occasionally seen with Google server; let's retry
|
|
// For example: "Could not read status line: SSL error: decryption failed or bad record mac"
|
|
retry = true;
|
|
}
|
|
break;
|
|
case NE_LOOKUP:
|
|
case NE_TIMEOUT:
|
|
case NE_CONNECT:
|
|
retry = true;
|
|
break;
|
|
}
|
|
|
|
if (code == 401) {
|
|
if (m_settings->getCredentialsOkay()) {
|
|
SE_LOG_DEBUG(NULL, NULL, "credential error due to throttling (?), retry");
|
|
retry = true;
|
|
} else {
|
|
// give up without retrying
|
|
SE_LOG_DEBUG(NULL, NULL, "credential error, no success with them before => report it");
|
|
}
|
|
}
|
|
|
|
|
|
SE_LOG_DEBUG(NULL, NULL, "%s, %s",
|
|
descr.c_str(),
|
|
retry ? "might retry" : "must not retry");
|
|
if (retry) {
|
|
m_attempt++;
|
|
|
|
if (!m_deadline) {
|
|
SE_LOG_DEBUG(NULL, NULL, "retrying not allowed for %s (no deadline)",
|
|
operation.c_str());
|
|
} else {
|
|
Timespec now = Timespec::monotonic();
|
|
if (now < m_deadline) {
|
|
int retrySeconds = m_settings->retrySeconds();
|
|
if (retrySeconds >= 0) {
|
|
Timespec last = m_lastRequestEnd;
|
|
Timespec now = Timespec::monotonic();
|
|
if (!last) {
|
|
last = now;
|
|
}
|
|
int delay = retrySeconds * (1 << (m_attempt - 1));
|
|
Timespec next = last + delay;
|
|
if (next > m_deadline) {
|
|
// no point in waiting (potentially much) until after our
|
|
// deadline, do final attempt at that time
|
|
next = m_deadline;
|
|
}
|
|
if (next > now) {
|
|
double duration = (next - now).duration();
|
|
SE_LOG_DEBUG(NULL, NULL, "retry %s in %.1lfs, attempt #%d",
|
|
operation.c_str(),
|
|
duration,
|
|
m_attempt);
|
|
// Inform the user, because this will take a
|
|
// while and we don't want to give the
|
|
// impression of being stuck.
|
|
SE_LOG_INFO(NULL, NULL, "operation temporarily (?) failed, going to retry in %.1lfs before giving up in %.1lfs: %s",
|
|
duration,
|
|
(m_deadline - now).duration(),
|
|
descr.c_str());
|
|
Sleep(duration);
|
|
} else {
|
|
SE_LOG_DEBUG(NULL, NULL, "retry %s immediately (due already), attempt #%d",
|
|
operation.c_str(),
|
|
m_attempt);
|
|
}
|
|
} else {
|
|
SE_LOG_DEBUG(NULL, NULL, "retry %s immediately (retry interval <= 0), attempt #%d",
|
|
operation.c_str(),
|
|
m_attempt);
|
|
}
|
|
|
|
// try same operation again?
|
|
if (s.getState() == SuspendFlags::NORMAL) {
|
|
m_operation = operation;
|
|
return false;
|
|
}
|
|
} else {
|
|
SE_LOG_DEBUG(NULL, NULL, "retry %s would exceed deadline, bailing out",
|
|
m_operation.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (code == 401) {
|
|
// fatal credential error, remember that
|
|
SE_LOG_DEBUG(NULL, NULL, "credentials rejected");
|
|
m_settings->setCredentialsOkay(false);
|
|
}
|
|
|
|
if (code) {
|
|
// copy error code into exception
|
|
SE_THROW_EXCEPTION_STATUS(TransportStatusException,
|
|
descr,
|
|
SyncMLStatus(code));
|
|
} else {
|
|
SE_THROW_EXCEPTION(TransportException,
|
|
descr);
|
|
}
|
|
}
|
|
|
|
XMLParser::XMLParser()
|
|
{
|
|
m_parser = ne_xml_create();
|
|
}
|
|
|
|
XMLParser::~XMLParser()
|
|
{
|
|
ne_xml_destroy(m_parser);
|
|
}
|
|
|
|
XMLParser &XMLParser::pushHandler(const StartCB_t &start,
|
|
const DataCB_t &data,
|
|
const EndCB_t &end)
|
|
{
|
|
m_stack.push_back(Callbacks(start, data, end));
|
|
Callbacks &cb = m_stack.back();
|
|
ne_xml_push_handler(m_parser,
|
|
startCB, dataCB, endCB,
|
|
&cb);
|
|
return *this;
|
|
}
|
|
|
|
int XMLParser::startCB(void *userdata, int parent,
|
|
const char *nspace, const char *name,
|
|
const char **atts)
|
|
{
|
|
Callbacks *cb = static_cast<Callbacks *>(userdata);
|
|
try {
|
|
return cb->m_start(parent, nspace, name, atts);
|
|
} catch (...) {
|
|
Exception::handle();
|
|
SE_LOG_ERROR(NULL, NULL, "startCB %s %s failed", nspace, name);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
int XMLParser::dataCB(void *userdata, int state,
|
|
const char *cdata, size_t len)
|
|
{
|
|
Callbacks *cb = static_cast<Callbacks *>(userdata);
|
|
try {
|
|
return cb->m_data ?
|
|
cb->m_data(state, cdata, len) :
|
|
0;
|
|
} catch (...) {
|
|
Exception::handle();
|
|
SE_LOG_ERROR(NULL, NULL, "dataCB failed");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
int XMLParser::endCB(void *userdata, int state,
|
|
const char *nspace, const char *name)
|
|
{
|
|
Callbacks *cb = static_cast<Callbacks *>(userdata);
|
|
try {
|
|
return cb->m_end ?
|
|
cb->m_end(state, nspace, name) :
|
|
0;
|
|
} catch (...) {
|
|
Exception::handle();
|
|
SE_LOG_ERROR(NULL, NULL, "endCB %s %s failed", nspace, name);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
|
|
int XMLParser::accept(const std::string &nspaceExpected,
|
|
const std::string &nameExpected,
|
|
const char *nspace,
|
|
const char *name)
|
|
{
|
|
if (nspace && nspaceExpected == nspace &&
|
|
name && nameExpected == name) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int XMLParser::append(std::string &buffer,
|
|
const char *data,
|
|
size_t len)
|
|
{
|
|
buffer.append(data, len);
|
|
return 0;
|
|
}
|
|
|
|
int XMLParser::reset(std::string &buffer)
|
|
{
|
|
buffer.clear();
|
|
return 0;
|
|
}
|
|
|
|
void XMLParser::initReportParser(const ResponseEndCB_t &responseEnd)
|
|
{
|
|
pushHandler(boost::bind(Neon::XMLParser::accept, "DAV:", "multistatus", _2, _3));
|
|
pushHandler(boost::bind(Neon::XMLParser::accept, "DAV:", "response", _2, _3),
|
|
Neon::XMLParser::DataCB_t(),
|
|
boost::bind(&Neon::XMLParser::doResponseEnd,
|
|
this, responseEnd));
|
|
pushHandler(boost::bind(Neon::XMLParser::accept, "DAV:", "href", _2, _3),
|
|
boost::bind(Neon::XMLParser::append, boost::ref(m_href), _2, _3));
|
|
pushHandler(boost::bind(Neon::XMLParser::accept, "DAV:", "propstat", _2, _3));
|
|
pushHandler(boost::bind(Neon::XMLParser::accept, "DAV:", "status", _2, _3) /* check status? */);
|
|
pushHandler(boost::bind(Neon::XMLParser::accept, "DAV:", "prop", _2, _3));
|
|
pushHandler(boost::bind(Neon::XMLParser::accept, "DAV:", "getetag", _2, _3),
|
|
boost::bind(Neon::XMLParser::append, boost::ref(m_etag), _2, _3));
|
|
}
|
|
|
|
Request::Request(Session &session,
|
|
const std::string &method,
|
|
const std::string &path,
|
|
const std::string &body,
|
|
std::string &result) :
|
|
m_method(method),
|
|
m_session(session),
|
|
m_result(&result),
|
|
m_parser(NULL)
|
|
{
|
|
m_req = ne_request_create(session.getSession(), m_method.c_str(), path.c_str());
|
|
ne_set_request_body_buffer(m_req, body.c_str(), body.size());
|
|
}
|
|
|
|
Request::Request(Session &session,
|
|
const std::string &method,
|
|
const std::string &path,
|
|
const std::string &body,
|
|
XMLParser &parser) :
|
|
m_method(method),
|
|
m_session(session),
|
|
m_result(NULL),
|
|
m_parser(&parser)
|
|
{
|
|
m_req = ne_request_create(session.getSession(), m_method.c_str(), path.c_str());
|
|
ne_set_request_body_buffer(m_req, body.c_str(), body.size());
|
|
}
|
|
|
|
Request::~Request()
|
|
{
|
|
ne_request_destroy(m_req);
|
|
}
|
|
|
|
#ifdef NEON_COMPATIBILITY
|
|
/**
|
|
* wrapper needed to allow lazy resolution of the ne_accept_2xx() function when needed
|
|
* instead of when loaded
|
|
*/
|
|
static int ne_accept_2xx(void *userdata, ne_request *req, const ne_status *st)
|
|
{
|
|
return ::ne_accept_2xx(userdata, req, st);
|
|
}
|
|
#endif
|
|
|
|
bool Request::run(const std::set<int> *expectedCodes)
|
|
{
|
|
int error;
|
|
|
|
if (m_result) {
|
|
m_result->clear();
|
|
ne_add_response_body_reader(m_req, ne_accept_2xx,
|
|
addResultData, this);
|
|
error = ne_request_dispatch(m_req);
|
|
} else {
|
|
error = ne_xml_dispatch_request(m_req, m_parser->get());
|
|
}
|
|
|
|
return checkError(error, expectedCodes);
|
|
}
|
|
|
|
int Request::addResultData(void *userdata, const char *buf, size_t len)
|
|
{
|
|
Request *me = static_cast<Request *>(userdata);
|
|
me->m_result->append(buf, len);
|
|
return 0;
|
|
}
|
|
|
|
bool Request::checkError(int error, const std::set<int> *expectedCodes)
|
|
{
|
|
return m_session.checkError(error, getStatus()->code, getStatus(), getResponseHeader("Location"),
|
|
expectedCodes);
|
|
}
|
|
|
|
}
|
|
|
|
SE_END_CXX
|
|
|
|
#endif // ENABLE_DAV
|