/*
* util.c : serf utility routines for ra_serf
*
* ====================================================================
* Copyright (c) 2006-2007 CollabNet. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://subversion.tigris.org/license-1.html.
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
*
* This software consists of voluntary contributions made by many
* individuals. For exact contribution history, see the revision
* history and logs, available at http://subversion.tigris.org/.
* ====================================================================
*/
#define APR_WANT_STRFUNC
#include <apr.h>
#include <apr_want.h>
#include <apr_fnmatch.h>
#include <serf.h>
#include <serf_bucket_types.h>
#include "svn_path.h"
#include "svn_private_config.h"
#include "svn_xml.h"
#include "private/svn_dep_compat.h"
#include "ra_serf.h"
/* Fix for older expat 1.95.x's that do not define
* XML_STATUS_OK/XML_STATUS_ERROR
*/
#ifndef XML_STATUS_OK
#define XML_STATUS_OK 1
#define XML_STATUS_ERROR 0
#endif
static const apr_uint32_t serf_failure_map[][2] =
{
{ SERF_SSL_CERT_NOTYETVALID, SVN_AUTH_SSL_NOTYETVALID },
{ SERF_SSL_CERT_EXPIRED, SVN_AUTH_SSL_EXPIRED },
{ SERF_SSL_CERT_SELF_SIGNED, SVN_AUTH_SSL_UNKNOWNCA },
{ SERF_SSL_CERT_UNKNOWNCA, SVN_AUTH_SSL_UNKNOWNCA }
};
/* Return a Subversion failure mask based on FAILURES, a serf SSL
failure mask. If anything in FAILURES is not directly mappable to
Subversion failures, set SVN_AUTH_SSL_OTHER in the returned mask. */
static apr_uint32_t
ssl_convert_serf_failures(int failures)
{
apr_uint32_t svn_failures = 0;
apr_size_t i;
for (i = 0; i < sizeof(serf_failure_map) / (2 * sizeof(apr_uint32_t)); ++i)
{
if (failures & serf_failure_map[i][0])
{
svn_failures |= serf_failure_map[i][1];
failures &= ~serf_failure_map[i][0];
}
}
/* Map any remaining failure bits to our OTHER bit. */
if (failures)
{
svn_failures |= SVN_AUTH_SSL_OTHER;
}
return svn_failures;
}
/* Convert a hash table containing the fields (as documented in X.509) of an
organisation to a string ORG, allocated in POOL. ORG is as returned by
serf_ssl_cert_issuer() and serf_ssl_cert_subject(). */
static char *
convert_organisation_to_str(apr_hash_t *org, apr_pool_t *pool)
{
return apr_psprintf(pool, "%s, %s, %s, %s, %s (%s)",
(char*)apr_hash_get(org, "OU", APR_HASH_KEY_STRING),
(char*)apr_hash_get(org, "O", APR_HASH_KEY_STRING),
(char*)apr_hash_get(org, "L", APR_HASH_KEY_STRING),
(char*)apr_hash_get(org, "ST", APR_HASH_KEY_STRING),
(char*)apr_hash_get(org, "C", APR_HASH_KEY_STRING),
(char*)apr_hash_get(org, "E", APR_HASH_KEY_STRING));
}
/* Callback that implements serf_ssl_need_server_cert_t. This function is
called on receiving a ssl certificate of a server when opening a https
connection. It allows Subversion to override the initial validation done
by serf.
Serf provides us the @a baton as provided in the call to
serf_ssl_server_cert_callback_set. The result of serf's initial validation
of the certificate @a CERT is returned as a bitmask in FAILURES. */
static apr_status_t
ssl_server_cert(void *baton, int failures,
const serf_ssl_certificate_t *cert)
{
svn_ra_serf__connection_t *conn = baton;
apr_pool_t *subpool;
svn_auth_ssl_server_cert_info_t cert_info;
svn_auth_cred_ssl_server_trust_t *server_creds = NULL;
svn_auth_iterstate_t *state;
const char *realmstring;
apr_uint32_t svn_failures;
svn_error_t *err;
apr_hash_t *issuer, *subject, *serf_cert;
void *creds;
/* Implicitly approve any non-server certs. */
if (serf_ssl_cert_depth(cert) > 0)
{
return APR_SUCCESS;
}
apr_pool_create(&subpool, conn->session->pool);
/* Extract the info from the certificate */
subject = serf_ssl_cert_subject(cert, subpool);
issuer = serf_ssl_cert_issuer(cert, subpool);
serf_cert = serf_ssl_cert_certificate(cert, subpool);
cert_info.hostname = apr_hash_get(subject, "CN", APR_HASH_KEY_STRING);
cert_info.fingerprint = apr_hash_get(serf_cert, "sha1", APR_HASH_KEY_STRING);
if (! cert_info.fingerprint)
cert_info.fingerprint = apr_pstrdup(subpool, "<unknown>");
cert_info.valid_from = apr_hash_get(serf_cert, "notBefore",
APR_HASH_KEY_STRING);
if (! cert_info.valid_from)
cert_info.valid_from = apr_pstrdup(subpool, "[invalid date]");
cert_info.valid_until = apr_hash_get(serf_cert, "notAfter",
APR_HASH_KEY_STRING);
if (! cert_info.valid_until)
cert_info.valid_until = apr_pstrdup(subpool, "[invalid date]");
cert_info.issuer_dname = convert_organisation_to_str(issuer, subpool);
cert_info.ascii_cert = serf_ssl_cert_export(cert, subpool);
svn_failures = ssl_convert_serf_failures(failures);
/* Match server certificate CN with the hostname of the server */
if (cert_info.hostname)
{
if (apr_fnmatch(cert_info.hostname, conn->hostinfo,
APR_FNM_PERIOD) == APR_FNM_NOMATCH)
{
svn_failures |= SVN_AUTH_SSL_CNMISMATCH;
}
}
svn_auth_set_parameter(conn->session->wc_callbacks->auth_baton,
SVN_AUTH_PARAM_SSL_SERVER_FAILURES,
&svn_failures);
svn_auth_set_parameter(conn->session->wc_callbacks->auth_baton,
SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO,
&cert_info);
/* Construct the realmstring, e.g. https://svn.collab.net:443 */
realmstring = apr_uri_unparse(subpool, &conn->session->repos_url,
APR_URI_UNP_OMITPATHINFO);
err = svn_auth_first_credentials(&creds, &state,
SVN_AUTH_CRED_SSL_SERVER_TRUST,
realmstring,
conn->session->wc_callbacks->auth_baton,
subpool);
if (err || ! creds)
{
svn_error_clear(err);
}
else
{
server_creds = creds;
err = svn_auth_save_credentials(state, subpool);
if (err)
{
/* It would be nice to show the error to the user somehow... */
svn_error_clear(err);
}
}
svn_auth_set_parameter(conn->session->wc_callbacks->auth_baton,
SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, NULL);
svn_pool_destroy(subpool);
return server_creds ? APR_SUCCESS : SVN_ERR_RA_SERF_SSL_CERT_UNTRUSTED;
}
static svn_error_t *
load_authorities(svn_ra_serf__connection_t *conn, const char *authorities,
apr_pool_t *pool)
{
char *files, *file, *last;
files = apr_pstrdup(pool, authorities);
while ((file = apr_strtok(files, ";", &last)) != NULL)
{
serf_ssl_certificate_t *ca_cert;
apr_status_t status = serf_ssl_load_cert_file(&ca_cert, file, pool);
if (status == APR_SUCCESS)
status = serf_ssl_trust_cert(conn->ssl_context, ca_cert);
if (status != APR_SUCCESS)
{
return svn_error_createf
(SVN_ERR_BAD_CONFIG_VALUE, NULL,
_("Invalid config: unable to load certificate file '%s'"),
svn_path_local_style(file, pool));
}
files = NULL;
}
return SVN_NO_ERROR;
}
serf_bucket_t *
svn_ra_serf__conn_setup(apr_socket_t *sock,
void *baton,
apr_pool_t *pool)
{
serf_bucket_t *bucket;
svn_ra_serf__connection_t *conn = baton;
bucket = serf_context_bucket_socket_create(conn->session->context,
sock, conn->bkt_alloc);
if (conn->using_ssl)
{
bucket = serf_bucket_ssl_decrypt_create(bucket, conn->ssl_context,
conn->bkt_alloc);
if (!conn->ssl_context)
{
conn->ssl_context = serf_bucket_ssl_decrypt_context_get(bucket);
serf_ssl_client_cert_provider_set(conn->ssl_context,
svn_ra_serf__handle_client_cert,
conn, conn->session->pool);
serf_ssl_client_cert_password_set(conn->ssl_context,
svn_ra_serf__handle_client_cert_pw,
conn, conn->session->pool);
serf_ssl_server_cert_callback_set(conn->ssl_context,
ssl_server_cert,
conn);
/* See if the user wants us to trust "default" openssl CAs. */
if (conn->session->trust_default_ca)
{
serf_ssl_use_default_certificates(conn->ssl_context);
}
/* Are there custom CAs to load? */
if (conn->session->ssl_authorities)
{
svn_error_t *err;
err = load_authorities(conn, conn->session->ssl_authorities,
conn->session->pool);
if (err)
{
/* TODO: we need a way to pass this error back to the
caller */
svn_error_clear(err);
}
}
}
}
return bucket;
}
serf_bucket_t*
svn_ra_serf__accept_response(serf_request_t *request,
serf_bucket_t *stream,
void *acceptor_baton,
apr_pool_t *pool)
{
serf_bucket_t *c;
serf_bucket_alloc_t *bkt_alloc;
bkt_alloc = serf_request_get_alloc(request);
c = serf_bucket_barrier_create(stream, bkt_alloc);
return serf_bucket_response_create(c, bkt_alloc);
}
static serf_bucket_t*
accept_head(serf_request_t *request,
serf_bucket_t *stream,
void *acceptor_baton,
apr_pool_t *pool)
{
serf_bucket_t *response;
response = svn_ra_serf__accept_response(request, stream, acceptor_baton,
pool);
/* We know we shouldn't get a response body. */
serf_bucket_response_set_head(response);
return response;
}
void
svn_ra_serf__conn_closed(serf_connection_t *conn,
void *closed_baton,
apr_status_t why,
apr_pool_t *pool)
{
svn_ra_serf__connection_t *our_conn = closed_baton;
if (why)
{
SVN_ERR_MALFUNCTION_NO_RETURN();
}
if (our_conn->using_ssl)
{
our_conn->ssl_context = NULL;
}
/* Restart the authentication phase on this new connection. */
if (our_conn->session->auth_protocol)
{
our_conn->session->auth_protocol->init_conn_func(our_conn->session,
our_conn,
our_conn->session->pool);
}
}
apr_status_t
svn_ra_serf__cleanup_serf_session(void *data)
{
/* svn_ra_serf__session_t *serf_sess = data; */
/* Nothing to do. */
return APR_SUCCESS;
}
apr_status_t svn_ra_serf__handle_client_cert(void *data,
const char **cert_path)
{
svn_ra_serf__connection_t *conn = data;
svn_ra_serf__session_t *session = conn->session;
const char *realm;
apr_port_t port;
svn_error_t *err;
void *creds;
*cert_path = NULL;
if (session->repos_url.port_str)
{
port = session->repos_url.port;
}
else
{
port = apr_uri_port_of_scheme(session->repos_url.scheme);
}
realm = apr_psprintf(session->pool, "%s://%s:%d",
session->repos_url.scheme,
session->repos_url.hostname,
port);
if (!conn->ssl_client_auth_state)
{
err = svn_auth_first_credentials(&creds,
&conn->ssl_client_auth_state,
SVN_AUTH_CRED_SSL_CLIENT_CERT,
realm,
session->wc_callbacks->auth_baton,
session->pool);
}
else
{
err = svn_auth_next_credentials(&creds,
conn->ssl_client_auth_state,
session->pool);
}
if (err)
{
session->pending_error = err;
return err->apr_err;
}
if (creds)
{
svn_auth_cred_ssl_client_cert_t *client_creds;
client_creds = creds;
*cert_path = client_creds->cert_file;
}
return APR_SUCCESS;
}
apr_status_t svn_ra_serf__handle_client_cert_pw(void *data,
const char *cert_path,
const char **password)
{
svn_ra_serf__connection_t *conn = data;
svn_ra_serf__session_t *session = conn->session;
svn_error_t *err;
void *creds;
*password = NULL;
if (!conn->ssl_client_pw_auth_state)
{
err = svn_auth_first_credentials(&creds,
&conn->ssl_client_pw_auth_state,
SVN_AUTH_CRED_SSL_CLIENT_CERT_PW,
cert_path,
session->wc_callbacks->auth_baton,
session->pool);
}
else
{
err = svn_auth_next_credentials(&creds,
conn->ssl_client_pw_auth_state,
session->pool);
}
if (err)
{
session->pending_error = err;
return err->apr_err;
}
if (creds)
{
svn_auth_cred_ssl_client_cert_pw_t *pw_creds;
pw_creds = creds;
*password = pw_creds->password;
}
return APR_SUCCESS;
}
void
svn_ra_serf__setup_serf_req(serf_request_t *request,
serf_bucket_t **req_bkt,
serf_bucket_t **ret_hdrs_bkt,
svn_ra_serf__connection_t *conn,
const char *method, const char *url,
serf_bucket_t *body_bkt, const char *content_type)
{
serf_bucket_t *hdrs_bkt;
*req_bkt = serf_bucket_request_create(method, url, body_bkt,
serf_request_get_alloc(request));
hdrs_bkt = serf_bucket_request_get_headers(*req_bkt);
serf_bucket_headers_setn(hdrs_bkt, "Host", conn->hostinfo);
serf_bucket_headers_setn(hdrs_bkt, "User-Agent", conn->useragent);
if (content_type)
{
serf_bucket_headers_setn(hdrs_bkt, "Content-Type", content_type);
}
/* These headers need to be sent with every request; see issue #3255
("mod_dav_svn does not pass client capabilities to start-commit
hooks") for why. */
serf_bucket_headers_set(hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_DEPTH);
serf_bucket_headers_set(hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_MERGEINFO);
serf_bucket_headers_set(hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_LOG_REVPROPS);
/* Setup server authorization headers */
if (conn->session->auth_protocol)
conn->session->auth_protocol->setup_request_func(conn, hdrs_bkt);
/* Setup proxy authorization headers */
if (conn->session->proxy_auth_protocol)
conn->session->proxy_auth_protocol->setup_request_func(conn, hdrs_bkt);
/* Set up SSL if we need to */
if (conn->using_ssl)
{
*req_bkt = serf_bucket_ssl_encrypt_create(*req_bkt, conn->ssl_context,
serf_request_get_alloc(request));
if (!conn->ssl_context)
{
conn->ssl_context = serf_bucket_ssl_encrypt_context_get(*req_bkt);
serf_ssl_client_cert_provider_set(conn->ssl_context,
svn_ra_serf__handle_client_cert,
conn, conn->session->pool);
serf_ssl_client_cert_password_set(conn->ssl_context,
svn_ra_serf__handle_client_cert_pw,
conn, conn->session->pool);
}
}
/* Set up Proxy settings */
if (conn->session->using_proxy)
{
char *root = apr_uri_unparse(conn->session->pool,
&conn->session->repos_url,
APR_URI_UNP_OMITPATHINFO);
serf_bucket_request_set_root(*req_bkt, root);
}
if (ret_hdrs_bkt)
{
*ret_hdrs_bkt = hdrs_bkt;
}
}
svn_error_t *
svn_ra_serf__context_run_wait(svn_boolean_t *done,
svn_ra_serf__session_t *sess,
apr_pool_t *pool)
{
apr_status_t status;
sess->pending_error = SVN_NO_ERROR;
while (!*done)
{
int i;
if (sess->wc_callbacks &&
sess->wc_callbacks->cancel_func)
SVN_ERR((sess->wc_callbacks->cancel_func)(sess->wc_callback_baton));
status = serf_context_run(sess->context, SERF_DURATION_FOREVER, pool);
if (APR_STATUS_IS_TIMEUP(status))
{
continue;
}
if (sess->pending_error)
{
return sess->pending_error;
}
if (status)
{
return svn_error_wrap_apr(status, "Error running context");
}
/* Debugging purposes only! */
serf_debug__closed_conn(sess->bkt_alloc);
for (i = 0; i < sess->num_conns; i++)
{
serf_debug__closed_conn(sess->conns[i]->bkt_alloc);
}
}
return SVN_NO_ERROR;
}
/*
* Expat callback invoked on a start element tag for an error response.
*/
static svn_error_t *
start_error(svn_ra_serf__xml_parser_t *parser,
void *userData,
svn_ra_serf__dav_props_t name,
const char **attrs)
{
svn_ra_serf__server_error_t *ctx = userData;
if (!ctx->in_error &&
strcmp(name.namespace, "DAV:") == 0 &&
strcmp(name.name, "error") == 0)
{
ctx->in_error = TRUE;
}
else if (ctx->in_error && strcmp(name.name, "human-readable") == 0)
{
const char *err_code;
err_code = svn_xml_get_attr_value("errcode", attrs);
if (err_code)
{
ctx->error->apr_err = apr_atoi64(err_code);
}
else
{
ctx->error->apr_err = APR_EGENERAL;
}
/* Start collecting cdata. */
svn_stringbuf_setempty(ctx->cdata);
ctx->collect_cdata = TRUE;
}
return SVN_NO_ERROR;
}
/*
* Expat callback invoked on an end element tag for a PROPFIND response.
*/
static svn_error_t *
end_error(svn_ra_serf__xml_parser_t *parser,
void *userData,
svn_ra_serf__dav_props_t name)
{
svn_ra_serf__server_error_t *ctx = userData;
if (ctx->in_error &&
strcmp(name.namespace, "DAV:") == 0 &&
strcmp(name.name, "error") == 0)
{
ctx->in_error = FALSE;
}
if (ctx->in_error && strcmp(name.name, "human-readable") == 0)
{
/* On the server dav_error_response_tag() will add a leading
and trailing newline if DEBUG_CR is defined in mod_dav.h,
so remove any such characters here. */
apr_size_t len;
const char *cd = ctx->cdata->data;
if (*cd == '\n')
++cd;
len = strlen(cd);
if (len > 0 && cd[len-1] == '\n')
--len;
ctx->error->message = apr_pstrmemdup(ctx->error->pool, cd, len);
ctx->collect_cdata = FALSE;
}
return SVN_NO_ERROR;
}
/*
* Expat callback invoked on CDATA elements in an error response.
*
* This callback can be called multiple times.
*/
static svn_error_t *
cdata_error(svn_ra_serf__xml_parser_t *parser,
void *userData,
const char *data,
apr_size_t len)
{
svn_ra_serf__server_error_t *ctx = userData;
if (ctx->collect_cdata)
{
svn_stringbuf_appendbytes(ctx->cdata, data, len);
}
return SVN_NO_ERROR;
}
apr_status_t
svn_ra_serf__handle_discard_body(serf_request_t *request,
serf_bucket_t *response,
void *baton,
apr_pool_t *pool)
{
apr_status_t status;
svn_ra_serf__server_error_t *server_err = baton;
if (server_err)
{
if (!server_err->init)
{
serf_bucket_t *hdrs;
const char *val;
server_err->init = TRUE;
hdrs = serf_bucket_response_get_headers(response);
val = serf_bucket_headers_get(hdrs, "Content-Type");
if (val && strncasecmp(val, "text/xml", sizeof("text/xml") - 1) == 0)
{
server_err->error = svn_error_create(APR_SUCCESS, NULL, NULL);
server_err->has_xml_response = TRUE;
server_err->cdata = svn_stringbuf_create("", pool);
server_err->collect_cdata = FALSE;
server_err->parser.pool = server_err->error->pool;
server_err->parser.user_data = server_err;
server_err->parser.start = start_error;
server_err->parser.end = end_error;
server_err->parser.cdata = cdata_error;
server_err->parser.done = &server_err->done;
server_err->parser.ignore_errors = TRUE;
}
else
{
server_err->error = SVN_NO_ERROR;
}
}
if (server_err->has_xml_response)
{
status = svn_ra_serf__handle_xml_parser(request, response,
&server_err->parser, pool);
if (server_err->done && server_err->error->apr_err == APR_SUCCESS)
{
svn_error_clear(server_err->error);
server_err->error = SVN_NO_ERROR;
}
return status;
}
}
/* Just loop through and discard the body. */
while (1)
{
const char *data;
apr_size_t len;
status = serf_bucket_read(response, SERF_READ_ALL_AVAIL, &data, &len);
if (status)
{
return status;
}
/* feed me */
}
}
apr_status_t
svn_ra_serf__handle_status_only(serf_request_t *request,
serf_bucket_t *response,
void *baton,
apr_pool_t *pool)
{
apr_status_t status;
svn_ra_serf__simple_request_context_t *ctx = baton;
status = svn_ra_serf__handle_discard_body(request, response,
&ctx->server_error, pool);
if (APR_STATUS_IS_EOF(status))
{
serf_status_line sl;
apr_status_t rv;
rv = serf_bucket_response_status(response, &sl);
ctx->status = sl.code;
ctx->reason = sl.reason;
ctx->done = TRUE;
}
return status;
}
/*
* Expat callback invoked on a start element tag for a 207 response.
*/
static svn_error_t *
start_207(svn_ra_serf__xml_parser_t *parser,
void *userData,
svn_ra_serf__dav_props_t name,
const char **attrs)
{
svn_ra_serf__server_error_t *ctx = userData;
if (!ctx->in_error &&
strcmp(name.namespace, "DAV:") == 0 &&
strcmp(name.name, "multistatus") == 0)
{
ctx->in_error = TRUE;
}
else if (ctx->in_error && strcmp(name.name, "responsedescription") == 0)
{
/* Start collecting cdata. */
svn_stringbuf_setempty(ctx->cdata);
ctx->collect_cdata = TRUE;
}
return SVN_NO_ERROR;
}
/*
* Expat callback invoked on an end element tag for a 207 response.
*/
static svn_error_t *
end_207(svn_ra_serf__xml_parser_t *parser,
void *userData,
svn_ra_serf__dav_props_t name)
{
svn_ra_serf__server_error_t *ctx = userData;
if (ctx->in_error &&
strcmp(name.namespace, "DAV:") == 0 &&
strcmp(name.name, "multistatus") == 0)
{
ctx->in_error = FALSE;
}
if (ctx->in_error && strcmp(name.name, "responsedescription") == 0)
{
ctx->collect_cdata = FALSE;
ctx->error->message = apr_pstrmemdup(ctx->error->pool, ctx->cdata->data,
ctx->cdata->len);
ctx->error->apr_err = SVN_ERR_RA_DAV_REQUEST_FAILED;
}
return SVN_NO_ERROR;
}
/*
* Expat callback invoked on CDATA elements in a 207 response.
*
* This callback can be called multiple times.
*/
static svn_error_t *
cdata_207(svn_ra_serf__xml_parser_t *parser,
void *userData,
const char *data,
apr_size_t len)
{
svn_ra_serf__server_error_t *ctx = userData;
if (ctx->collect_cdata)
{
svn_stringbuf_appendbytes(ctx->cdata, data, len);
}
return SVN_NO_ERROR;
}
apr_status_t
svn_ra_serf__handle_multistatus_only(serf_request_t *request,
serf_bucket_t *response,
void *baton,
apr_pool_t *pool)
{
apr_status_t status;
svn_ra_serf__simple_request_context_t *ctx = baton;
svn_ra_serf__server_error_t *server_err = &ctx->server_error;
/* If necessary, initialize our XML parser. */
if (server_err && !server_err->init)
{
serf_bucket_t *hdrs;
const char *val;
server_err->init = TRUE;
hdrs = serf_bucket_response_get_headers(response);
val = serf_bucket_headers_get(hdrs, "Content-Type");
if (val && strncasecmp(val, "text/xml", sizeof("text/xml") - 1) == 0)
{
server_err->error = svn_error_create(APR_SUCCESS, NULL, NULL);
server_err->has_xml_response = TRUE;
server_err->cdata = svn_stringbuf_create("", pool);
server_err->collect_cdata = FALSE;
server_err->parser.pool = server_err->error->pool;
server_err->parser.user_data = server_err;
server_err->parser.start = start_207;
server_err->parser.end = end_207;
server_err->parser.cdata = cdata_207;
server_err->parser.done = &ctx->done;
server_err->parser.ignore_errors = TRUE;
}
else
{
ctx->done = TRUE;
server_err->error = SVN_NO_ERROR;
}
}
/* If server_err->error still contains APR_SUCCESS, it means that we
have not successfully parsed the XML yet. */
if (server_err && server_err->error
&& server_err->error->apr_err == APR_SUCCESS)
{
status = svn_ra_serf__handle_xml_parser(request, response,
&server_err->parser, pool);
/* APR_EOF will be returned when parsing is complete. If we see
any other error, return it immediately. In practice the only
other error we expect to see is APR_EAGAIN, which indicates that
we could not parse the XML because the contents are not yet
available to be read. */
if (!APR_STATUS_IS_EOF(status))
{
return status;
}
else if (ctx->done && server_err->error->apr_err == APR_SUCCESS)
{
svn_error_clear(server_err->error);
server_err->error = SVN_NO_ERROR;
}
}
status = svn_ra_serf__handle_discard_body(request, response,
NULL, pool);
if (APR_STATUS_IS_EOF(status))
{
serf_status_line sl;
apr_status_t rv;
rv = serf_bucket_response_status(response, &sl);
ctx->status = sl.code;
ctx->reason = sl.reason;
}
return status;
}
static void
start_xml(void *userData, const char *raw_name, const char **attrs)
{
svn_ra_serf__xml_parser_t *parser = userData;
svn_ra_serf__dav_props_t name;
if (parser->error)
return;
if (!parser->state)
svn_ra_serf__xml_push_state(parser, 0);
svn_ra_serf__define_ns(&parser->state->ns_list, attrs, parser->state->pool);
name = svn_ra_serf__expand_ns(parser->state->ns_list, raw_name);
parser->error = parser->start(parser, parser->user_data, name, attrs);
}
static void
end_xml(void *userData, const char *raw_name)
{
svn_ra_serf__xml_parser_t *parser = userData;
svn_ra_serf__dav_props_t name;
if (parser->error)
return;
name = svn_ra_serf__expand_ns(parser->state->ns_list, raw_name);
parser->error = parser->end(parser, parser->user_data, name);
}
static void
cdata_xml(void *userData, const char *data, int len)
{
svn_ra_serf__xml_parser_t *parser = userData;
if (parser->error)
return;
if (!parser->state)
svn_ra_serf__xml_push_state(parser, 0);
parser->error = parser->cdata(parser, parser->user_data, data, len);
}
apr_status_t
svn_ra_serf__handle_xml_parser(serf_request_t *request,
serf_bucket_t *response,
void *baton,
apr_pool_t *pool)
{
const char *data;
apr_size_t len;
serf_status_line sl;
apr_status_t status;
int xml_status;
svn_ra_serf__xml_parser_t *ctx = baton;
serf_bucket_response_status(response, &sl);
if (ctx->status_code)
{
*ctx->status_code = sl.code;
}
/* Woo-hoo. Nothing here to see. */
if (sl.code == 404 && ctx->ignore_errors == FALSE)
{
/* If our caller won't know about the 404, abort() for now. */
SVN_ERR_ASSERT_NO_RETURN(ctx->status_code);
if (*ctx->done == FALSE)
{
*ctx->done = TRUE;
if (ctx->done_list)
{
ctx->done_item->data = ctx->user_data;
ctx->done_item->next = *ctx->done_list;
*ctx->done_list = ctx->done_item;
}
}
ctx->error = svn_ra_serf__handle_server_error(request, response, pool);
return svn_ra_serf__handle_discard_body(request, response, NULL, pool);
}
if (!ctx->xmlp)
{
ctx->xmlp = XML_ParserCreate(NULL);
XML_SetUserData(ctx->xmlp, ctx);
XML_SetElementHandler(ctx->xmlp, start_xml, end_xml);
if (ctx->cdata)
{
XML_SetCharacterDataHandler(ctx->xmlp, cdata_xml);
}
}
while (1)
{
status = serf_bucket_read(response, 8000, &data, &len);
if (SERF_BUCKET_READ_ERROR(status))
{
return status;
}
xml_status = XML_Parse(ctx->xmlp, data, len, 0);
if (xml_status == XML_STATUS_ERROR && ctx->ignore_errors == FALSE)
{
XML_ParserFree(ctx->xmlp);
SVN_ERR_ASSERT_NO_RETURN(ctx->status_code);
if (*ctx->done == FALSE)
{
*ctx->done = TRUE;
if (ctx->done_list)
{
ctx->done_item->data = ctx->user_data;
ctx->done_item->next = *ctx->done_list;
*ctx->done_list = ctx->done_item;
}
}
ctx->error = svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
"XML parsing failed: (%d %s)",
sl.code, sl.reason);
return ctx->error->apr_err;
}
if (ctx->error && ctx->ignore_errors == FALSE)
{
XML_ParserFree(ctx->xmlp);
return ctx->error->apr_err;
}
if (APR_STATUS_IS_EAGAIN(status))
{
return status;
}
if (APR_STATUS_IS_EOF(status))
{
xml_status = XML_Parse(ctx->xmlp, NULL, 0, 1);
XML_ParserFree(ctx->xmlp);
if (xml_status == XML_STATUS_ERROR && ctx->ignore_errors == FALSE)
{
svn_error_clear(ctx->error);
}
*ctx->done = TRUE;
if (ctx->done_list)
{
ctx->done_item->data = ctx->user_data;
ctx->done_item->next = *ctx->done_list;
*ctx->done_list = ctx->done_item;
}
return status;
}
/* feed me! */
}
/* not reached */
}
svn_error_t *
svn_ra_serf__handle_server_error(serf_request_t *request,
serf_bucket_t *response,
apr_pool_t *pool)
{
svn_ra_serf__server_error_t server_err = { 0 };
apr_status_t status;
status = svn_ra_serf__handle_discard_body(request, response,
&server_err, pool);
return server_err.error;
}
/* Implements the serf_response_handler_t interface. Wait for HTTP
response status and headers, and invoke CTX->response_handler() to
carry out operation-specific processing. Afterwards, check for
connection close.
If during the setup of the request we set a snapshot on the body buckets,
handle_response has to make sure these buckets get destroyed iff the
request doesn't have to be resent.
*/
static apr_status_t
handle_response(serf_request_t *request,
serf_bucket_t *response,
void *baton,
apr_pool_t *pool)
{
svn_ra_serf__handler_t *ctx = baton;
serf_status_line sl;
apr_status_t status;
if (!response)
{
/* Uh-oh. Our connection died. Requeue. */
if (ctx->response_error)
{
status = ctx->response_error(request, response, 0,
ctx->response_error_baton);
if (status)
{
goto cleanup;
}
}
svn_ra_serf__request_create(ctx);
return APR_SUCCESS;
}
status = serf_bucket_response_status(response, &sl);
if (SERF_BUCKET_READ_ERROR(status))
{
return status;
}
if (!sl.version && (APR_STATUS_IS_EOF(status) ||
APR_STATUS_IS_EAGAIN(status)))
{
goto cleanup;
}
status = serf_bucket_response_wait_for_headers(response);
if (status)
{
if (!APR_STATUS_IS_EOF(status))
{
goto cleanup;
}
/* Cases where a lack of a response body (via EOF) is okay:
* - A HEAD request
* - 204/304 response
*
* Otherwise, if we get an EOF here, something went really wrong: either
* the server closed on us early or we're reading too much. Either way,
* scream loudly.
*/
if (strcmp(ctx->method, "HEAD") != 0 && sl.code != 204 && sl.code != 304)
{
ctx->session->pending_error =
svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL,
_("Premature EOF seen from server"));
goto cleanup;
}
}
if (ctx->conn->last_status_code == 401 && sl.code < 400)
{
svn_auth_save_credentials(ctx->session->auth_state, ctx->session->pool);
ctx->session->auth_attempts = 0;
ctx->session->auth_state = NULL;
ctx->session->realm = NULL;
}
ctx->conn->last_status_code = sl.code;
if (sl.code == 401 || sl.code == 407)
{
/* 401 Authorization or 407 Proxy-Authentication required */
svn_error_t *err;
err = svn_ra_serf__handle_auth(sl.code, ctx->session, ctx->conn,
request, response, pool);
if (err)
{
ctx->session->pending_error = err;
svn_ra_serf__handle_discard_body(request, response, NULL, pool);
status = ctx->session->pending_error->apr_err;
goto cleanup;
}
else
{
status = svn_ra_serf__handle_discard_body(request, response, NULL,
pool);
/* At this time we might not have received the whole response from
the server. If that's the case, don't setup a new request now
but wait till we retry the request later. */
if (! APR_STATUS_IS_EAGAIN(status))
{
svn_ra_serf__priority_request_create(ctx);
return status;
}
}
}
else if (sl.code == 409 || sl.code >= 500)
{
/* 409 Conflict: can indicate a hook error.
5xx (Internal) Server error. */
ctx->session->pending_error =
svn_ra_serf__handle_server_error(request, response, pool);
if (!ctx->session->pending_error)
{
ctx->session->pending_error =
svn_error_create(APR_EGENERAL, NULL,
_("Unspecified error message"));
}
status = APR_EGENERAL;
goto cleanup;
}
else
{
status = ctx->response_handler(request, response, ctx->response_baton,
pool);
}
cleanup:
/* If a snapshot was set on the body bucket, it wasn't destroyed when the
request was sent, we have to destroy it now upon successful handling of
the response. */
if (ctx->body_snapshot_set && ctx->body_buckets)
{
serf_bucket_destroy(ctx->body_buckets);
ctx->body_buckets = NULL;
ctx->body_snapshot_set = FALSE;
}
return status;
}
/* Implements the serf_request_setup_t interface (which sets up both a
request and its response handler callback). If the CTX->delegate()
callback is non-NULL, invoke it to carry out the majority of the
serf_request_setup_t implementation. Otherwise, perform default
setup, with special handling for HEAD requests, and finer-grained
callbacks invoked (if non-NULL) to produce the request headers and
body. */
static apr_status_t
setup_request(serf_request_t *request,
void *setup_baton,
serf_bucket_t **req_bkt,
serf_response_acceptor_t *acceptor,
void **acceptor_baton,
serf_response_handler_t *handler,
void **handler_baton,
apr_pool_t *pool)
{
svn_ra_serf__handler_t *ctx = setup_baton;
serf_bucket_t *headers_bkt;
*acceptor = svn_ra_serf__accept_response;
*acceptor_baton = ctx->session;
if (ctx->delegate)
{
apr_status_t status;
status = ctx->delegate(request, ctx->delegate_baton, req_bkt,
acceptor, acceptor_baton, handler, handler_baton,
pool);
if (status)
{
return status;
}
ctx->response_handler = *handler;
ctx->response_baton = *handler_baton;
}
else
{
serf_bucket_t *body_bkt = ctx->body_buckets;
if (strcmp(ctx->method, "HEAD") == 0)
{
*acceptor = accept_head;
}
if (ctx->body_delegate)
{
body_bkt = ctx->body_buckets =
ctx->body_delegate(ctx->body_delegate_baton,
serf_request_get_alloc(request),
pool);
}
/* If this is a request that has to be retried, we might be able to reuse
the existing body buckets if a snapshot was set. */
else if (ctx->body_buckets)
{
/* Wrap the body bucket in a barrier bucket if a snapshot was set.
After the request is sent serf will destroy the request bucket
(req_bkt) including this barrier bucket, but this way our
body_buckets bucket will not be destroyed and we can reuse it
later.
This does put ownership of body_buckets in our own hands though,
so we have to make sure it gets destroyed when handling the
response. */
/* TODO: for now we assume restoring a snapshot on a bucket that
hasn't been read yet is a cheap operation. We need a way to find
out if we really need to restore a snapshot, or if we still are
in the initial state. */
apr_status_t status;
if (ctx->body_snapshot_set)
{
/* If restoring a snapshot doesn't work, we have to fall back
on current behavior (ie. retrying a request fails). */
status = serf_bucket_restore_snapshot(ctx->body_buckets);
}
status = serf_bucket_snapshot(ctx->body_buckets);
if (status == APR_SUCCESS)
{
ctx->body_snapshot_set = TRUE;
body_bkt = serf_bucket_barrier_create(ctx->body_buckets,
serf_request_get_alloc(request));
}
else
{
/* If the snapshot wasn't successful (maybe because the caller
used a bucket that doesn't support the snapshot feature),
fall back to non-snapshot behavior and hope that the request
is handled the first time. */
}
}
svn_ra_serf__setup_serf_req(request, req_bkt, &headers_bkt, ctx->conn,
ctx->method, ctx->path,
body_bkt, ctx->body_type);
if (ctx->header_delegate)
{
ctx->header_delegate(headers_bkt, ctx->header_delegate_baton, pool);
}
}
*handler = handle_response;
*handler_baton = ctx;
return APR_SUCCESS;
}
serf_request_t *
svn_ra_serf__request_create(svn_ra_serf__handler_t *handler)
{
return serf_connection_request_create(handler->conn->conn,
setup_request, handler);
}
serf_request_t *
svn_ra_serf__priority_request_create(svn_ra_serf__handler_t *handler)
{
return serf_connection_priority_request_create(handler->conn->conn,
setup_request, handler);
}
svn_error_t *
svn_ra_serf__discover_root(const char **vcc_url,
const char **rel_path,
svn_ra_serf__session_t *session,
svn_ra_serf__connection_t *conn,
const char *orig_path,
apr_pool_t *pool)
{
apr_hash_t *props;
const char *path, *relative_path, *present_path = "", *uuid;
/* If we're only interested in our VCC, just return it. */
if (session->vcc_url && !rel_path)
{
*vcc_url = session->vcc_url;
return SVN_NO_ERROR;
}
props = apr_hash_make(pool);
path = orig_path;
*vcc_url = NULL;
uuid = NULL;
do
{
svn_error_t *err = svn_ra_serf__retrieve_props(props, session, conn,
path, SVN_INVALID_REVNUM,
"0", base_props, pool);
if (! err)
{
*vcc_url =
svn_ra_serf__get_ver_prop(props, path,
SVN_INVALID_REVNUM,
"DAV:",
"version-controlled-configuration");
relative_path = svn_ra_serf__get_ver_prop(props, path,
SVN_INVALID_REVNUM,
SVN_DAV_PROP_NS_DAV,
"baseline-relative-path");
uuid = svn_ra_serf__get_ver_prop(props, path,
SVN_INVALID_REVNUM,
SVN_DAV_PROP_NS_DAV,
"repository-uuid");
break;
}
else
{
if (err->apr_err != SVN_ERR_FS_NOT_FOUND)
{
return err; /* found a _real_ error */
}
else
{
/* This happens when the file is missing in HEAD. */
svn_error_clear(err);
/* Okay, strip off. */
present_path = svn_path_join(svn_path_basename(path, pool),
present_path, pool);
path = svn_path_dirname(path, pool);
}
}
}
while (!svn_path_is_empty(path));
if (!*vcc_url)
{
return svn_error_create(SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL,
_("The OPTIONS response did not include the "
"requested version-controlled-configuration "
"value"));
}
/* Store our VCC in our cache. */
if (!session->vcc_url)
{
session->vcc_url = apr_pstrdup(session->pool, *vcc_url);
}
/* Update our cached repository root URL. */
if (!session->repos_root_str)
{
svn_stringbuf_t *url_buf;
url_buf = svn_stringbuf_create(path, pool);
svn_path_remove_components(url_buf,
svn_path_component_count(relative_path));
/* Now recreate the root_url. */
session->repos_root = session->repos_url;
session->repos_root.path = apr_pstrdup(session->pool, url_buf->data);
session->repos_root_str =
svn_path_canonicalize(apr_uri_unparse(session->pool,
&session->repos_root, 0),
session->pool);
}
/* Store the repository UUID in the cache. */
if (!session->uuid)
{
session->uuid = apr_pstrdup(session->pool, uuid);
}
if (rel_path)
{
if (present_path[0] != '\0')
{
/* The relative path is supposed to be URI decoded, so decode
present_path before joining both together. */
*rel_path = svn_path_join(relative_path,
svn_path_uri_decode(present_path, pool),
pool);
}
else
{
*rel_path = relative_path;
}
}
return SVN_NO_ERROR;
}
svn_error_t *
svn_ra_serf__error_on_status(int status_code, const char *path)
{
switch(status_code)
{
case 301:
case 302:
return svn_error_createf(SVN_ERR_RA_DAV_RELOCATED, NULL,
(status_code == 301)
? _("Repository moved permanently to '%s';"
" please relocate")
: _("Repository moved temporarily to '%s';"
" please relocate"), path);
case 404:
return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
_("'%s' path not found"), path);
}
return SVN_NO_ERROR;
}