| /* |
| * util.c : serf utility routines for ra_serf |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| */ |
| |
| |
| |
| #include <assert.h> |
| |
| #define APR_WANT_STRFUNC |
| #include <apr.h> |
| #include <apr_want.h> |
| |
| #include <serf.h> |
| #include <serf_bucket_types.h> |
| |
| #include "svn_hash.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_private_config.h" |
| #include "svn_string.h" |
| #include "svn_props.h" |
| #include "svn_dirent_uri.h" |
| |
| #include "../libsvn_ra/ra_loader.h" |
| #include "private/svn_dep_compat.h" |
| #include "private/svn_fspath.h" |
| #include "private/svn_auth_private.h" |
| #include "private/svn_cert.h" |
| |
| #include "ra_serf.h" |
| |
| 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) / (sizeof(serf_failure_map[0])); |
| ++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; |
| } |
| |
| |
| static apr_status_t |
| save_error(svn_ra_serf__session_t *session, |
| svn_error_t *err) |
| { |
| if (err || session->pending_error) |
| { |
| session->pending_error = svn_error_compose_create( |
| session->pending_error, |
| err); |
| return session->pending_error->apr_err; |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| |
| /* Construct the realmstring, e.g. https://svn.collab.net:443. */ |
| static const char * |
| construct_realm(svn_ra_serf__session_t *session, |
| apr_pool_t *pool) |
| { |
| const char *realm; |
| apr_port_t port; |
| |
| if (session->session_url.port_str) |
| { |
| port = session->session_url.port; |
| } |
| else |
| { |
| port = apr_uri_port_of_scheme(session->session_url.scheme); |
| } |
| |
| realm = apr_psprintf(pool, "%s://%s:%d", |
| session->session_url.scheme, |
| session->session_url.hostname, |
| port); |
| |
| return realm; |
| } |
| |
| /* 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) |
| { |
| const char *cn = svn_hash_gets(org, "CN"); |
| const char *org_unit = svn_hash_gets(org, "OU"); |
| const char *org_name = svn_hash_gets(org, "O"); |
| const char *locality = svn_hash_gets(org, "L"); |
| const char *state = svn_hash_gets(org, "ST"); |
| const char *country = svn_hash_gets(org, "C"); |
| const char *email = svn_hash_gets(org, "E"); |
| svn_stringbuf_t *buf = svn_stringbuf_create_empty(pool); |
| |
| if (cn) |
| { |
| svn_stringbuf_appendcstr(buf, cn); |
| svn_stringbuf_appendcstr(buf, ", "); |
| } |
| |
| if (org_unit) |
| { |
| svn_stringbuf_appendcstr(buf, org_unit); |
| svn_stringbuf_appendcstr(buf, ", "); |
| } |
| |
| if (org_name) |
| { |
| svn_stringbuf_appendcstr(buf, org_name); |
| svn_stringbuf_appendcstr(buf, ", "); |
| } |
| |
| if (locality) |
| { |
| svn_stringbuf_appendcstr(buf, locality); |
| svn_stringbuf_appendcstr(buf, ", "); |
| } |
| |
| if (state) |
| { |
| svn_stringbuf_appendcstr(buf, state); |
| svn_stringbuf_appendcstr(buf, ", "); |
| } |
| |
| if (country) |
| { |
| svn_stringbuf_appendcstr(buf, country); |
| svn_stringbuf_appendcstr(buf, ", "); |
| } |
| |
| /* Chop ', ' if any. */ |
| svn_stringbuf_chop(buf, 2); |
| |
| if (email) |
| { |
| svn_stringbuf_appendcstr(buf, "("); |
| svn_stringbuf_appendcstr(buf, email); |
| svn_stringbuf_appendcstr(buf, ")"); |
| } |
| |
| return buf->data; |
| } |
| |
| static void append_reason(svn_stringbuf_t *errmsg, const char *reason, int *reasons) |
| { |
| if (*reasons < 1) |
| svn_stringbuf_appendcstr(errmsg, _(": ")); |
| else |
| svn_stringbuf_appendcstr(errmsg, _(", ")); |
| svn_stringbuf_appendcstr(errmsg, reason); |
| (*reasons)++; |
| } |
| |
| /* 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 svn_error_t * |
| ssl_server_cert(void *baton, int failures, |
| const serf_ssl_certificate_t *cert, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_serf__connection_t *conn = baton; |
| 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; |
| apr_hash_t *issuer; |
| apr_hash_t *subject = NULL; |
| apr_hash_t *serf_cert = NULL; |
| void *creds; |
| |
| svn_failures = (ssl_convert_serf_failures(failures) |
| | conn->server_cert_failures); |
| |
| if (serf_ssl_cert_depth(cert) == 0) |
| { |
| /* If the depth is 0, the hostname must match the certificate. |
| |
| ### This should really be handled by serf, which should pass an error |
| for this case, but that has backwards compatibility issues. */ |
| apr_array_header_t *san; |
| svn_boolean_t found_matching_hostname = FALSE; |
| svn_string_t *actual_hostname = |
| svn_string_create(conn->session->session_url.hostname, scratch_pool); |
| |
| serf_cert = serf_ssl_cert_certificate(cert, scratch_pool); |
| |
| san = svn_hash_gets(serf_cert, "subjectAltName"); |
| /* Match server certificate CN with the hostname of the server iff |
| * we didn't find any subjectAltName fields and try to match them. |
| * Per RFC 2818 they are authoritative if present and CommonName |
| * should be ignored. NOTE: This isn't 100% correct since serf |
| * only loads the subjectAltName hash with dNSNames, technically |
| * we should ignore the CommonName if any subjectAltName entry |
| * exists even if it is one we don't support. */ |
| if (san && san->nelts > 0) |
| { |
| int i; |
| for (i = 0; i < san->nelts; i++) |
| { |
| const char *s = APR_ARRAY_IDX(san, i, const char*); |
| svn_string_t *cert_hostname = svn_string_create(s, scratch_pool); |
| |
| if (svn_cert__match_dns_identity(cert_hostname, actual_hostname)) |
| { |
| found_matching_hostname = TRUE; |
| break; |
| } |
| } |
| } |
| else |
| { |
| const char *hostname = NULL; |
| |
| subject = serf_ssl_cert_subject(cert, scratch_pool); |
| |
| if (subject) |
| hostname = svn_hash_gets(subject, "CN"); |
| |
| if (hostname) |
| { |
| svn_string_t *cert_hostname = svn_string_create(hostname, |
| scratch_pool); |
| |
| if (svn_cert__match_dns_identity(cert_hostname, actual_hostname)) |
| { |
| found_matching_hostname = TRUE; |
| } |
| } |
| } |
| |
| if (!found_matching_hostname) |
| svn_failures |= SVN_AUTH_SSL_CNMISMATCH; |
| } |
| |
| if (!svn_failures) |
| return SVN_NO_ERROR; |
| |
| /* Extract the info from the certificate */ |
| if (! subject) |
| subject = serf_ssl_cert_subject(cert, scratch_pool); |
| issuer = serf_ssl_cert_issuer(cert, scratch_pool); |
| if (! serf_cert) |
| serf_cert = serf_ssl_cert_certificate(cert, scratch_pool); |
| |
| cert_info.hostname = svn_hash_gets(subject, "CN"); |
| cert_info.fingerprint = svn_hash_gets(serf_cert, "sha1"); |
| if (! cert_info.fingerprint) |
| cert_info.fingerprint = apr_pstrdup(scratch_pool, "<unknown>"); |
| cert_info.valid_from = svn_hash_gets(serf_cert, "notBefore"); |
| if (! cert_info.valid_from) |
| cert_info.valid_from = apr_pstrdup(scratch_pool, "[invalid date]"); |
| cert_info.valid_until = svn_hash_gets(serf_cert, "notAfter"); |
| if (! cert_info.valid_until) |
| cert_info.valid_until = apr_pstrdup(scratch_pool, "[invalid date]"); |
| cert_info.issuer_dname = convert_organisation_to_str(issuer, scratch_pool); |
| cert_info.ascii_cert = serf_ssl_cert_export(cert, scratch_pool); |
| |
| /* Handle any non-server certs. */ |
| if (serf_ssl_cert_depth(cert) > 0) |
| { |
| svn_error_t *err; |
| |
| svn_auth_set_parameter(conn->session->auth_baton, |
| SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, |
| &cert_info); |
| |
| svn_auth_set_parameter(conn->session->auth_baton, |
| SVN_AUTH_PARAM_SSL_SERVER_FAILURES, |
| &svn_failures); |
| |
| realmstring = apr_psprintf(scratch_pool, "AUTHORITY:%s", |
| cert_info.fingerprint); |
| |
| err = svn_auth_first_credentials(&creds, &state, |
| SVN_AUTH_CRED_SSL_SERVER_AUTHORITY, |
| realmstring, |
| conn->session->auth_baton, |
| scratch_pool); |
| |
| svn_auth_set_parameter(conn->session->auth_baton, |
| SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, NULL); |
| |
| svn_auth_set_parameter(conn->session->auth_baton, |
| SVN_AUTH_PARAM_SSL_SERVER_FAILURES, NULL); |
| |
| if (err) |
| { |
| if (err->apr_err != SVN_ERR_AUTHN_NO_PROVIDER) |
| return svn_error_trace(err); |
| |
| /* No provider registered that handles server authorities */ |
| svn_error_clear(err); |
| creds = NULL; |
| } |
| |
| if (creds) |
| { |
| server_creds = creds; |
| SVN_ERR(svn_auth_save_credentials(state, scratch_pool)); |
| |
| svn_failures &= ~server_creds->accepted_failures; |
| } |
| |
| if (svn_failures) |
| conn->server_cert_failures |= svn_failures; |
| |
| return APR_SUCCESS; |
| } |
| |
| svn_auth_set_parameter(conn->session->auth_baton, |
| SVN_AUTH_PARAM_SSL_SERVER_FAILURES, |
| &svn_failures); |
| |
| svn_auth_set_parameter(conn->session->auth_baton, |
| SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, |
| &cert_info); |
| |
| realmstring = construct_realm(conn->session, conn->session->pool); |
| |
| SVN_ERR(svn_auth_first_credentials(&creds, &state, |
| SVN_AUTH_CRED_SSL_SERVER_TRUST, |
| realmstring, |
| conn->session->auth_baton, |
| scratch_pool)); |
| if (creds) |
| { |
| server_creds = creds; |
| svn_failures &= ~server_creds->accepted_failures; |
| SVN_ERR(svn_auth_save_credentials(state, scratch_pool)); |
| } |
| |
| while (svn_failures && creds) |
| { |
| SVN_ERR(svn_auth_next_credentials(&creds, state, scratch_pool)); |
| |
| if (creds) |
| { |
| server_creds = creds; |
| svn_failures &= ~server_creds->accepted_failures; |
| SVN_ERR(svn_auth_save_credentials(state, scratch_pool)); |
| } |
| } |
| |
| svn_auth_set_parameter(conn->session->auth_baton, |
| SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, NULL); |
| |
| /* Are there non accepted failures left? */ |
| if (svn_failures) |
| { |
| svn_stringbuf_t *errmsg; |
| int reasons = 0; |
| |
| errmsg = svn_stringbuf_create( |
| _("Server SSL certificate verification failed"), |
| scratch_pool); |
| |
| |
| if (svn_failures & SVN_AUTH_SSL_NOTYETVALID) |
| append_reason(errmsg, _("certificate is not yet valid"), &reasons); |
| |
| if (svn_failures & SVN_AUTH_SSL_EXPIRED) |
| append_reason(errmsg, _("certificate has expired"), &reasons); |
| |
| if (svn_failures & SVN_AUTH_SSL_CNMISMATCH) |
| append_reason(errmsg, |
| _("certificate issued for a different hostname"), |
| &reasons); |
| |
| if (svn_failures & SVN_AUTH_SSL_UNKNOWNCA) |
| append_reason(errmsg, _("issuer is not trusted"), &reasons); |
| |
| if (svn_failures & SVN_AUTH_SSL_OTHER) |
| append_reason(errmsg, _("and other reason(s)"), &reasons); |
| |
| return svn_error_create(SVN_ERR_RA_SERF_SSL_CERT_UNTRUSTED, NULL, |
| errmsg->data); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements serf_ssl_need_server_cert_t for ssl_server_cert */ |
| static apr_status_t |
| ssl_server_cert_cb(void *baton, int failures, |
| const serf_ssl_certificate_t *cert) |
| { |
| svn_ra_serf__connection_t *conn = baton; |
| svn_ra_serf__session_t *session = conn->session; |
| apr_pool_t *subpool; |
| svn_error_t *err; |
| |
| subpool = svn_pool_create(session->pool); |
| err = svn_error_trace(ssl_server_cert(baton, failures, cert, subpool)); |
| svn_pool_destroy(subpool); |
| |
| return save_error(session, err); |
| } |
| |
| static svn_error_t * |
| load_authorities(svn_ra_serf__connection_t *conn, const char *authorities, |
| apr_pool_t *pool) |
| { |
| apr_array_header_t *files = svn_cstring_split(authorities, ";", |
| TRUE /* chop_whitespace */, |
| pool); |
| int i; |
| |
| for (i = 0; i < files->nelts; ++i) |
| { |
| const char *file = APR_ARRAY_IDX(files, i, const char *); |
| 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_dirent_local_style(file, pool)); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| #if SERF_VERSION_AT_LEAST(1, 4, 0) && defined(SVN__SERF_TEST_HTTP2) |
| /* Implements serf_ssl_protocol_result_cb_t */ |
| static apr_status_t |
| conn_negotiate_protocol(void *data, |
| const char *protocol) |
| { |
| svn_ra_serf__connection_t *conn = data; |
| |
| if (!strcmp(protocol, "h2")) |
| { |
| serf_connection_set_framing_type( |
| conn->conn, |
| SERF_CONNECTION_FRAMING_TYPE_HTTP2); |
| |
| /* Disable generating content-length headers. */ |
| conn->session->http10 = FALSE; |
| conn->session->http20 = TRUE; |
| conn->session->using_chunked_requests = TRUE; |
| conn->session->detect_chunking = FALSE; |
| } |
| else |
| { |
| /* protocol should be "" or "http/1.1" */ |
| serf_connection_set_framing_type( |
| conn->conn, |
| SERF_CONNECTION_FRAMING_TYPE_HTTP1); |
| } |
| |
| return APR_SUCCESS; |
| } |
| #endif |
| |
| static svn_error_t * |
| conn_setup(apr_socket_t *sock, |
| serf_bucket_t **read_bkt, |
| serf_bucket_t **write_bkt, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| svn_ra_serf__connection_t *conn = baton; |
| |
| *read_bkt = serf_context_bucket_socket_create(conn->session->context, |
| sock, conn->bkt_alloc); |
| |
| if (conn->session->using_ssl) |
| { |
| /* input stream */ |
| *read_bkt = serf_bucket_ssl_decrypt_create(*read_bkt, conn->ssl_context, |
| conn->bkt_alloc); |
| if (!conn->ssl_context) |
| { |
| conn->ssl_context = serf_bucket_ssl_encrypt_context_get(*read_bkt); |
| |
| serf_ssl_set_hostname(conn->ssl_context, |
| conn->session->session_url.hostname); |
| |
| 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_cb, |
| 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_ERR(load_authorities(conn, conn->session->ssl_authorities, |
| conn->session->pool)); |
| } |
| #if SERF_VERSION_AT_LEAST(1, 4, 0) && defined(SVN__SERF_TEST_HTTP2) |
| if (APR_SUCCESS == |
| serf_ssl_negotiate_protocol(conn->ssl_context, "h2,http/1.1", |
| conn_negotiate_protocol, conn)) |
| { |
| serf_connection_set_framing_type( |
| conn->conn, |
| SERF_CONNECTION_FRAMING_TYPE_NONE); |
| } |
| #endif |
| } |
| |
| if (write_bkt) |
| { |
| /* output stream */ |
| *write_bkt = serf_bucket_ssl_encrypt_create(*write_bkt, |
| conn->ssl_context, |
| conn->bkt_alloc); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* svn_ra_serf__conn_setup is a callback for serf. This function |
| creates a read bucket and will wrap the write bucket if SSL |
| is needed. */ |
| apr_status_t |
| svn_ra_serf__conn_setup(apr_socket_t *sock, |
| serf_bucket_t **read_bkt, |
| serf_bucket_t **write_bkt, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| svn_ra_serf__connection_t *conn = baton; |
| svn_ra_serf__session_t *session = conn->session; |
| svn_error_t *err; |
| |
| err = svn_error_trace(conn_setup(sock, |
| read_bkt, |
| write_bkt, |
| baton, |
| pool)); |
| return save_error(session, err); |
| } |
| |
| |
| /* Our default serf response acceptor. */ |
| static serf_bucket_t * |
| accept_response(serf_request_t *request, |
| serf_bucket_t *stream, |
| void *acceptor_baton, |
| apr_pool_t *pool) |
| { |
| /* svn_ra_serf__handler_t *handler = acceptor_baton; */ |
| 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); |
| } |
| |
| |
| /* Custom response acceptor for HEAD requests. */ |
| static serf_bucket_t * |
| accept_head(serf_request_t *request, |
| serf_bucket_t *stream, |
| void *acceptor_baton, |
| apr_pool_t *pool) |
| { |
| /* svn_ra_serf__handler_t *handler = acceptor_baton; */ |
| serf_bucket_t *response; |
| |
| response = accept_response(request, stream, acceptor_baton, pool); |
| |
| /* We know we shouldn't get a response body. */ |
| serf_bucket_response_set_head(response); |
| |
| return response; |
| } |
| |
| static svn_error_t * |
| connection_closed(svn_ra_serf__connection_t *conn, |
| apr_status_t why, |
| apr_pool_t *pool) |
| { |
| if (why) |
| { |
| return svn_ra_serf__wrap_err(why, NULL); |
| } |
| |
| if (conn->session->using_ssl) |
| conn->ssl_context = NULL; |
| |
| return SVN_NO_ERROR; |
| } |
| |
| 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 *ra_conn = closed_baton; |
| svn_error_t *err; |
| |
| err = svn_error_trace(connection_closed(ra_conn, why, pool)); |
| |
| (void) save_error(ra_conn->session, err); |
| } |
| |
| |
| /* Implementation of svn_ra_serf__handle_client_cert */ |
| static svn_error_t * |
| handle_client_cert(void *data, |
| const char **cert_path, |
| apr_pool_t *pool) |
| { |
| svn_ra_serf__connection_t *conn = data; |
| svn_ra_serf__session_t *session = conn->session; |
| const char *realm; |
| void *creds; |
| |
| *cert_path = NULL; |
| |
| realm = construct_realm(session, session->pool); |
| |
| if (!conn->ssl_client_auth_state) |
| { |
| SVN_ERR(svn_auth_first_credentials(&creds, |
| &conn->ssl_client_auth_state, |
| SVN_AUTH_CRED_SSL_CLIENT_CERT, |
| realm, |
| session->auth_baton, |
| pool)); |
| } |
| else |
| { |
| SVN_ERR(svn_auth_next_credentials(&creds, |
| conn->ssl_client_auth_state, |
| session->pool)); |
| } |
| |
| if (creds) |
| { |
| svn_auth_cred_ssl_client_cert_t *client_creds; |
| client_creds = creds; |
| *cert_path = client_creds->cert_file; |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements serf_ssl_need_client_cert_t for handle_client_cert */ |
| 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; |
| svn_error_t *err; |
| |
| err = svn_error_trace(handle_client_cert(data, cert_path, session->pool)); |
| |
| return save_error(session, err); |
| } |
| |
| /* Implementation for svn_ra_serf__handle_client_cert_pw */ |
| static svn_error_t * |
| handle_client_cert_pw(void *data, |
| const char *cert_path, |
| const char **password, |
| apr_pool_t *pool) |
| { |
| svn_ra_serf__connection_t *conn = data; |
| svn_ra_serf__session_t *session = conn->session; |
| void *creds; |
| |
| *password = NULL; |
| |
| if (!conn->ssl_client_pw_auth_state) |
| { |
| SVN_ERR(svn_auth_first_credentials(&creds, |
| &conn->ssl_client_pw_auth_state, |
| SVN_AUTH_CRED_SSL_CLIENT_CERT_PW, |
| cert_path, |
| session->auth_baton, |
| pool)); |
| } |
| else |
| { |
| SVN_ERR(svn_auth_next_credentials(&creds, |
| conn->ssl_client_pw_auth_state, |
| pool)); |
| } |
| |
| if (creds) |
| { |
| /* At this stage we are unable to check whether the password |
| is correct; if it is incorrect serf will fail to establish |
| an SSL connection and will return a generic SSL error. */ |
| svn_auth_cred_ssl_client_cert_pw_t *pw_creds; |
| pw_creds = creds; |
| *password = pw_creds->password; |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| /* Implements serf_ssl_need_client_cert_pw_t for handle_client_cert_pw */ |
| 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; |
| |
| err = svn_error_trace(handle_client_cert_pw(data, |
| cert_path, |
| password, |
| session->pool)); |
| |
| return save_error(session, err); |
| } |
| |
| |
| /* |
| * Given a REQUEST on connection CONN, construct a request bucket for it, |
| * returning the bucket in *REQ_BKT. |
| * |
| * If HDRS_BKT is not-NULL, it will be set to a headers_bucket that |
| * corresponds to the new request. |
| * |
| * The request will be METHOD at URL. |
| * |
| * If BODY_BKT is not-NULL, it will be sent as the request body. |
| * |
| * If CONTENT_TYPE is not-NULL, it will be sent as the Content-Type header. |
| * |
| * If DAV_HEADERS is non-zero, it will add standard DAV capabilities headers |
| * to request. |
| * |
| * REQUEST_POOL should live for the duration of the request. Serf will |
| * construct this and provide it to the request_setup callback, so we |
| * should just use that one. |
| */ |
| static svn_error_t * |
| setup_serf_req(serf_request_t *request, |
| serf_bucket_t **req_bkt, |
| serf_bucket_t **hdrs_bkt, |
| svn_ra_serf__session_t *session, |
| const char *method, const char *url, |
| serf_bucket_t *body_bkt, const char *content_type, |
| const char *accept_encoding, |
| svn_boolean_t dav_headers, |
| apr_pool_t *request_pool, |
| apr_pool_t *scratch_pool) |
| { |
| serf_bucket_alloc_t *allocator = serf_request_get_alloc(request); |
| |
| svn_spillbuf_t *buf; |
| svn_boolean_t set_CL = session->http10 || !session->using_chunked_requests; |
| |
| if (set_CL && body_bkt != NULL) |
| { |
| /* Ugh. Use HTTP/1.0 to talk to the server because we don't know if |
| it speaks HTTP/1.1 (and thus, chunked requests), or because the |
| server actually responded as only supporting HTTP/1.0. |
| |
| We'll take the existing body_bkt, spool it into a spillbuf, and |
| then wrap a bucket around that spillbuf. The spillbuf will give |
| us the Content-Length value. */ |
| SVN_ERR(svn_ra_serf__copy_into_spillbuf(&buf, body_bkt, |
| request_pool, |
| scratch_pool)); |
| /* Destroy original bucket since it content is already copied |
| to spillbuf. */ |
| serf_bucket_destroy(body_bkt); |
| |
| body_bkt = svn_ra_serf__create_sb_bucket(buf, allocator, |
| request_pool, |
| scratch_pool); |
| } |
| |
| /* Create a request bucket. Note that this sucker is kind enough to |
| add a "Host" header for us. */ |
| *req_bkt = serf_request_bucket_request_create(request, method, url, |
| body_bkt, allocator); |
| |
| /* Set the Content-Length value. This will also trigger an HTTP/1.0 |
| request (rather than the default chunked request). */ |
| if (set_CL) |
| { |
| if (body_bkt == NULL) |
| serf_bucket_request_set_CL(*req_bkt, 0); |
| else |
| serf_bucket_request_set_CL(*req_bkt, svn_spillbuf__get_size(buf)); |
| } |
| |
| *hdrs_bkt = serf_bucket_request_get_headers(*req_bkt); |
| |
| /* We use serf_bucket_headers_setn() because the USERAGENT has a |
| lifetime longer than this bucket. Thus, there is no need to copy |
| the header values. */ |
| serf_bucket_headers_setn(*hdrs_bkt, "User-Agent", session->useragent); |
| |
| if (content_type) |
| { |
| serf_bucket_headers_setn(*hdrs_bkt, "Content-Type", content_type); |
| } |
| |
| if (session->http10) |
| { |
| serf_bucket_headers_setn(*hdrs_bkt, "Connection", "keep-alive"); |
| } |
| |
| if (accept_encoding) |
| { |
| serf_bucket_headers_setn(*hdrs_bkt, "Accept-Encoding", accept_encoding); |
| } |
| |
| /* These headers need to be sent with every request that might need |
| capability processing (e.g. during commit, reports, etc.), see |
| issue #3255 ("mod_dav_svn does not pass client capabilities to |
| start-commit hooks") for why. |
| |
| Some request types like GET/HEAD/PROPFIND are unaware of capability |
| handling; and in some cases the responses can even be cached by |
| proxies, so we don't have to send these hearders there. */ |
| if (dav_headers) |
| { |
| serf_bucket_headers_setn(*hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_DEPTH); |
| serf_bucket_headers_setn(*hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_MERGEINFO); |
| serf_bucket_headers_setn(*hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_LOG_REVPROPS); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_ra_serf__context_run(svn_ra_serf__session_t *sess, |
| apr_interval_time_t *waittime_left, |
| apr_pool_t *scratch_pool) |
| { |
| apr_status_t status; |
| svn_error_t *err; |
| assert(sess->pending_error == SVN_NO_ERROR); |
| |
| if (sess->cancel_func) |
| SVN_ERR(sess->cancel_func(sess->cancel_baton)); |
| |
| status = serf_context_run(sess->context, |
| SVN_RA_SERF__CONTEXT_RUN_DURATION, |
| scratch_pool); |
| |
| err = sess->pending_error; |
| sess->pending_error = SVN_NO_ERROR; |
| |
| /* If the context duration timeout is up, we'll subtract that |
| duration from the total time alloted for such things. If |
| there's no time left, we fail with a message indicating that |
| the connection timed out. */ |
| if (APR_STATUS_IS_TIMEUP(status)) |
| { |
| status = 0; |
| |
| if (sess->timeout) |
| { |
| if (*waittime_left > SVN_RA_SERF__CONTEXT_RUN_DURATION) |
| { |
| *waittime_left -= SVN_RA_SERF__CONTEXT_RUN_DURATION; |
| } |
| else |
| { |
| return |
| svn_error_compose_create( |
| err, |
| svn_error_create(SVN_ERR_RA_DAV_CONN_TIMEOUT, NULL, |
| _("Connection timed out"))); |
| } |
| } |
| } |
| else |
| { |
| *waittime_left = sess->timeout; |
| } |
| |
| SVN_ERR(err); |
| if (status) |
| { |
| /* ### This omits SVN_WARNING, and possibly relies on the fact that |
| ### MAX(SERF_ERROR_*) < SVN_ERR_BAD_CATEGORY_START? */ |
| if (status >= SVN_ERR_BAD_CATEGORY_START && status < SVN_ERR_LAST) |
| { |
| /* apr can't translate subversion errors to text */ |
| SVN_ERR_W(svn_error_create(status, NULL, NULL), |
| _("Error running context")); |
| } |
| |
| return svn_ra_serf__wrap_err(status, _("Error running context")); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_ra_serf__context_run_wait(svn_boolean_t *done, |
| svn_ra_serf__session_t *sess, |
| apr_pool_t *scratch_pool) |
| { |
| apr_pool_t *iterpool; |
| apr_interval_time_t waittime_left = sess->timeout; |
| |
| assert(sess->pending_error == SVN_NO_ERROR); |
| |
| iterpool = svn_pool_create(scratch_pool); |
| while (!*done) |
| { |
| int i; |
| |
| svn_pool_clear(iterpool); |
| |
| SVN_ERR(svn_ra_serf__context_run(sess, &waittime_left, iterpool)); |
| |
| /* Debugging purposes only! */ |
| for (i = 0; i < sess->num_conns; i++) |
| { |
| serf_debug__closed_conn(sess->conns[i]->bkt_alloc); |
| } |
| } |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Ensure that a handler is no longer scheduled on the connection. |
| |
| Eventually serf will have a reliable way to cancel existing requests, |
| but currently it doesn't even have a way to reliable identify a request |
| after rescheduling, for auth reasons. |
| |
| So the only thing we can do today is reset the connection, which |
| will cancel all outstanding requests and prepare the connection |
| for re-use. |
| */ |
| void |
| svn_ra_serf__unschedule_handler(svn_ra_serf__handler_t *handler) |
| { |
| serf_connection_reset(handler->conn->conn); |
| handler->scheduled = FALSE; |
| } |
| |
| svn_error_t * |
| svn_ra_serf__context_run_one(svn_ra_serf__handler_t *handler, |
| apr_pool_t *scratch_pool) |
| { |
| svn_error_t *err; |
| |
| /* Create a serf request based on HANDLER. */ |
| svn_ra_serf__request_create(handler); |
| |
| /* Wait until the response logic marks its DONE status. */ |
| err = svn_ra_serf__context_run_wait(&handler->done, handler->session, |
| scratch_pool); |
| |
| if (handler->scheduled) |
| { |
| /* We reset the connection (breaking pipelining, etc.), as |
| if we didn't the next data would still be handled by this handler, |
| which is done as far as our caller is concerned. */ |
| svn_ra_serf__unschedule_handler(handler); |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| |
| |
| |
| static apr_status_t |
| drain_bucket(serf_bucket_t *bucket) |
| { |
| /* Read whatever is in the bucket, and just drop it. */ |
| while (1) |
| { |
| apr_status_t status; |
| const char *data; |
| apr_size_t len; |
| |
| status = serf_bucket_read(bucket, SERF_READ_ALL_AVAIL, &data, &len); |
| if (status) |
| return status; |
| } |
| } |
| |
| |
| |
| |
| /* Implements svn_ra_serf__response_handler_t */ |
| svn_error_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; |
| |
| status = drain_bucket(response); |
| if (status) |
| return svn_ra_serf__wrap_err(status, NULL); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| apr_status_t |
| svn_ra_serf__response_discard_handler(serf_request_t *request, |
| serf_bucket_t *response, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| return drain_bucket(response); |
| } |
| |
| |
| /* Return the value of the RESPONSE's Location header if any, or NULL |
| otherwise. */ |
| static const char * |
| response_get_location(serf_bucket_t *response, |
| const char *base_url, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| serf_bucket_t *headers; |
| const char *location; |
| |
| headers = serf_bucket_response_get_headers(response); |
| location = serf_bucket_headers_get(headers, "Location"); |
| if (location == NULL) |
| return NULL; |
| |
| /* The RFCs say we should have received a full url in LOCATION, but |
| older apache versions and many custom web handlers just return a |
| relative path here... |
| |
| And we can't trust anything because it is network data. |
| */ |
| if (*location == '/') |
| { |
| apr_uri_t uri; |
| apr_status_t status; |
| |
| status = apr_uri_parse(scratch_pool, base_url, &uri); |
| |
| if (status != APR_SUCCESS) |
| return NULL; |
| |
| /* Replace the path path with what we got */ |
| uri.path = apr_pstrdup(scratch_pool, location); |
| |
| /* And make APR produce a proper full url for us */ |
| return apr_uri_unparse(result_pool, &uri, 0); |
| } |
| else if (!svn_path_is_url(location)) |
| { |
| return NULL; /* Any other formats we should support? */ |
| } |
| |
| return apr_pstrdup(result_pool, location); |
| } |
| |
| |
| /* Implements svn_ra_serf__response_handler_t */ |
| svn_error_t * |
| svn_ra_serf__expect_empty_body(serf_request_t *request, |
| serf_bucket_t *response, |
| void *baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_serf__handler_t *handler = baton; |
| serf_bucket_t *hdrs; |
| const char *val; |
| |
| /* This function is just like handle_multistatus_only() except for the |
| XML parsing callbacks. We want to look for the -readable element. */ |
| |
| /* We should see this just once, in order to initialize SERVER_ERROR. |
| At that point, the core error processing will take over. If we choose |
| not to parse an error, then we'll never return here (because we |
| change the response handler). */ |
| SVN_ERR_ASSERT(handler->server_error == NULL); |
| |
| hdrs = serf_bucket_response_get_headers(response); |
| val = serf_bucket_headers_get(hdrs, "Content-Type"); |
| if (val |
| && (handler->sline.code < 200 || handler->sline.code >= 300) |
| && strncasecmp(val, "text/xml", sizeof("text/xml") - 1) == 0) |
| { |
| svn_ra_serf__server_error_t *server_err; |
| |
| SVN_ERR(svn_ra_serf__setup_error_parsing(&server_err, handler, |
| FALSE, |
| handler->handler_pool, |
| handler->handler_pool)); |
| |
| handler->server_error = server_err; |
| } |
| else |
| { |
| /* The body was not text/xml, or we got a success code. |
| Toss anything that arrives. */ |
| handler->discard_body = TRUE; |
| } |
| |
| /* Returning SVN_NO_ERROR will return APR_SUCCESS to serf, which tells it |
| to call the response handler again. That will start up the XML parsing, |
| or it will be dropped on the floor (per the decision above). */ |
| return SVN_NO_ERROR; |
| } |
| |
| |
| apr_status_t |
| svn_ra_serf__credentials_callback(char **username, char **password, |
| serf_request_t *request, void *baton, |
| int code, const char *authn_type, |
| const char *realm, |
| apr_pool_t *pool) |
| { |
| svn_ra_serf__handler_t *handler = baton; |
| svn_ra_serf__session_t *session = handler->session; |
| void *creds; |
| svn_auth_cred_simple_t *simple_creds; |
| svn_error_t *err; |
| |
| if (code == 401) |
| { |
| /* Use svn_auth_first_credentials if this is the first time we ask for |
| credentials during this session OR if the last time we asked |
| session->auth_state wasn't set (eg. if the credentials provider was |
| cancelled by the user). */ |
| if (!session->auth_state) |
| { |
| err = svn_auth_first_credentials(&creds, |
| &session->auth_state, |
| SVN_AUTH_CRED_SIMPLE, |
| realm, |
| session->auth_baton, |
| session->pool); |
| } |
| else |
| { |
| err = svn_auth_next_credentials(&creds, |
| session->auth_state, |
| session->pool); |
| } |
| |
| if (err) |
| { |
| (void) save_error(session, err); |
| return err->apr_err; |
| } |
| |
| session->auth_attempts++; |
| |
| if (!creds || session->auth_attempts > 4) |
| { |
| /* No more credentials. */ |
| (void) save_error(session, |
| svn_error_create( |
| SVN_ERR_AUTHN_FAILED, NULL, |
| _("No more credentials or we tried too many " |
| "times.\nAuthentication failed"))); |
| return SVN_ERR_AUTHN_FAILED; |
| } |
| |
| simple_creds = creds; |
| *username = apr_pstrdup(pool, simple_creds->username); |
| *password = apr_pstrdup(pool, simple_creds->password); |
| } |
| else |
| { |
| *username = apr_pstrdup(pool, session->proxy_username); |
| *password = apr_pstrdup(pool, session->proxy_password); |
| |
| session->proxy_auth_attempts++; |
| |
| if (!session->proxy_username || session->proxy_auth_attempts > 4) |
| { |
| /* No more credentials. */ |
| (void) save_error(session, |
| svn_error_create( |
| SVN_ERR_AUTHN_FAILED, NULL, |
| _("Proxy authentication failed"))); |
| return SVN_ERR_AUTHN_FAILED; |
| } |
| } |
| |
| handler->conn->last_status_code = code; |
| |
| return APR_SUCCESS; |
| } |
| |
| /* Wait for HTTP response status and headers, and invoke HANDLER-> |
| response_handler() to carry out operation-specific processing. |
| Afterwards, check for connection close. |
| |
| SERF_STATUS allows returning errors to serf without creating a |
| subversion error object. |
| */ |
| static svn_error_t * |
| handle_response(serf_request_t *request, |
| serf_bucket_t *response, |
| svn_ra_serf__handler_t *handler, |
| apr_status_t *serf_status, |
| apr_pool_t *scratch_pool) |
| { |
| apr_status_t status; |
| svn_error_t *err; |
| |
| /* ### need to verify whether this already gets init'd on every |
| ### successful exit. for an error-exit, it will (properly) be |
| ### ignored by the caller. */ |
| *serf_status = APR_SUCCESS; |
| |
| if (!response) |
| { |
| /* Uh-oh. Our connection died. */ |
| handler->scheduled = FALSE; |
| |
| if (handler->response_error) |
| { |
| /* Give a handler chance to prevent request requeue. */ |
| SVN_ERR(handler->response_error(request, response, 0, |
| handler->response_error_baton)); |
| |
| svn_ra_serf__request_create(handler); |
| } |
| /* Response error callback is not configured. Requeue another request |
| for this handler only if we didn't started to process body. |
| Return error otherwise. */ |
| else if (!handler->reading_body) |
| { |
| svn_ra_serf__request_create(handler); |
| } |
| else |
| { |
| return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, |
| _("%s request on '%s' failed"), |
| handler->method, handler->path); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* If we're reading the body, then skip all this preparation. */ |
| if (handler->reading_body) |
| goto process_body; |
| |
| /* Copy the Status-Line info into HANDLER, if we don't yet have it. */ |
| if (handler->sline.version == 0) |
| { |
| serf_status_line sl; |
| |
| status = serf_bucket_response_status(response, &sl); |
| if (status != APR_SUCCESS) |
| { |
| /* The response line is not (yet) ready, or some other error. */ |
| *serf_status = status; |
| return SVN_NO_ERROR; /* Handled by serf */ |
| } |
| |
| /* If we got APR_SUCCESS, then we should have Status-Line info. */ |
| SVN_ERR_ASSERT(sl.version != 0); |
| |
| handler->sline = sl; |
| handler->sline.reason = apr_pstrdup(handler->handler_pool, sl.reason); |
| |
| /* HTTP/1.1? (or later) */ |
| if (sl.version != SERF_HTTP_10) |
| handler->session->http10 = FALSE; |
| |
| if (sl.version >= SERF_HTTP_VERSION(2, 0)) { |
| handler->session->http20 = TRUE; |
| } |
| } |
| |
| /* Keep reading from the network until we've read all the headers. */ |
| status = serf_bucket_response_wait_for_headers(response); |
| if (status) |
| { |
| /* The typical "error" will be APR_EAGAIN, meaning that more input |
| from the network is required to complete the reading of the |
| headers. */ |
| if (!APR_STATUS_IS_EOF(status)) |
| { |
| /* Either the headers are not (yet) complete, or there really |
| was an error. */ |
| *serf_status = status; |
| return SVN_NO_ERROR; |
| } |
| |
| /* wait_for_headers() will return EOF if there is no body in this |
| response, or if we completely read the body. The latter is not |
| true since we would have set READING_BODY to get the body read, |
| and we would not be back to this code block. |
| |
| It can also return EOF if we truly hit EOF while (say) processing |
| the headers. aka Badness. */ |
| |
| /* 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(handler->method, "HEAD") != 0 |
| && handler->sline.code != 204 |
| && handler->sline.code != 304) |
| { |
| err = svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, |
| svn_ra_serf__wrap_err(status, NULL), |
| _("Premature EOF seen from server" |
| " (http status=%d)"), |
| handler->sline.code); |
| |
| /* In case anything else arrives... discard it. */ |
| handler->discard_body = TRUE; |
| |
| return err; |
| } |
| } |
| |
| /* ... and set up the header fields in HANDLER. */ |
| handler->location = response_get_location(response, |
| handler->session->session_url_str, |
| handler->handler_pool, |
| scratch_pool); |
| |
| /* On the last request, we failed authentication. We succeeded this time, |
| so let's save away these credentials. */ |
| if (handler->conn->last_status_code == 401 && handler->sline.code < 400) |
| { |
| SVN_ERR(svn_auth_save_credentials(handler->session->auth_state, |
| handler->session->pool)); |
| handler->session->auth_attempts = 0; |
| handler->session->auth_state = NULL; |
| } |
| handler->conn->last_status_code = handler->sline.code; |
| |
| if (handler->sline.code >= 400) |
| { |
| /* 405 Method Not allowed. |
| 408 Request Timeout |
| 409 Conflict: can indicate a hook error. |
| 5xx (Internal) Server error. */ |
| serf_bucket_t *hdrs; |
| const char *val; |
| |
| 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) |
| { |
| svn_ra_serf__server_error_t *server_err; |
| |
| SVN_ERR(svn_ra_serf__setup_error_parsing(&server_err, handler, |
| FALSE, |
| handler->handler_pool, |
| handler->handler_pool)); |
| |
| handler->server_error = server_err; |
| } |
| else |
| { |
| handler->discard_body = TRUE; |
| } |
| } |
| else if (handler->sline.code <= 199) |
| { |
| handler->discard_body = TRUE; |
| } |
| |
| /* Stop processing the above, on every packet arrival. */ |
| handler->reading_body = TRUE; |
| |
| process_body: |
| |
| /* A client cert file password was obtained and worked (any HTTP |
| response means that the SSL connection was established.) */ |
| if (handler->conn->ssl_client_pw_auth_state) |
| { |
| SVN_ERR(svn_auth_save_credentials(handler->conn->ssl_client_pw_auth_state, |
| handler->session->pool)); |
| handler->conn->ssl_client_pw_auth_state = NULL; |
| } |
| if (handler->conn->ssl_client_auth_state) |
| { |
| /* The cert file provider doesn't have any code to save creds so |
| this is currently a no-op. */ |
| SVN_ERR(svn_auth_save_credentials(handler->conn->ssl_client_auth_state, |
| handler->session->pool)); |
| handler->conn->ssl_client_auth_state = NULL; |
| } |
| |
| /* We've been instructed to ignore the body. Drain whatever is present. */ |
| if (handler->discard_body) |
| { |
| *serf_status = drain_bucket(response); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* If we are supposed to parse the body as a server_error, then do |
| that now. */ |
| if (handler->server_error != NULL) |
| { |
| return svn_error_trace( |
| svn_ra_serf__handle_server_error(handler->server_error, |
| handler, |
| request, response, |
| serf_status, |
| scratch_pool)); |
| } |
| |
| /* Pass the body along to the registered response handler. */ |
| err = handler->response_handler(request, response, |
| handler->response_baton, |
| scratch_pool); |
| |
| if (err |
| && (!SERF_BUCKET_READ_ERROR(err->apr_err) |
| || APR_STATUS_IS_ECONNRESET(err->apr_err) |
| || APR_STATUS_IS_ECONNABORTED(err->apr_err))) |
| { |
| /* These errors are special cased in serf |
| ### We hope no handler returns these by accident. */ |
| *serf_status = err->apr_err; |
| svn_error_clear(err); |
| return SVN_NO_ERROR; |
| } |
| |
| return svn_error_trace(err); |
| } |
| |
| |
| /* Implements serf_response_handler_t for handle_response. Storing |
| errors in handler->session->pending_error if appropriate. */ |
| static apr_status_t |
| handle_response_cb(serf_request_t *request, |
| serf_bucket_t *response, |
| void *baton, |
| apr_pool_t *response_pool) |
| { |
| svn_ra_serf__handler_t *handler = baton; |
| svn_error_t *err; |
| apr_status_t inner_status; |
| apr_status_t outer_status; |
| apr_pool_t *scratch_pool = response_pool; /* Scratch pool needed? */ |
| |
| err = svn_error_trace(handle_response(request, response, |
| handler, &inner_status, |
| scratch_pool)); |
| |
| /* Select the right status value to return. */ |
| outer_status = save_error(handler->session, err); |
| if (!outer_status) |
| outer_status = inner_status; |
| |
| /* Make sure the DONE flag is set properly and requests are cleaned up. */ |
| if (APR_STATUS_IS_EOF(outer_status) || APR_STATUS_IS_EOF(inner_status)) |
| { |
| svn_ra_serf__session_t *sess = handler->session; |
| handler->done = TRUE; |
| handler->scheduled = FALSE; |
| outer_status = APR_EOF; |
| |
| /* We use a cached handler->session here to allow handler to free the |
| memory containing the handler */ |
| save_error(sess, |
| handler->done_delegate(request, handler->done_delegate_baton, |
| scratch_pool)); |
| } |
| else if (SERF_BUCKET_READ_ERROR(outer_status) |
| && handler->session->pending_error) |
| { |
| handler->discard_body = TRUE; /* Discard further data */ |
| handler->done = TRUE; /* Mark as done */ |
| /* handler->scheduled is still TRUE, as we still expect data. |
| If we would return an error outer-status the connection |
| would have to be restarted. With scheduled still TRUE |
| destroying the handler's pool will still reset the |
| connection, avoiding the possibility of returning |
| an error for this handler when a new request is |
| scheduled. */ |
| outer_status = APR_EAGAIN; /* Exit context loop */ |
| } |
| |
| return outer_status; |
| } |
| |
| /* Perform basic request setup, with special handling for HEAD requests, |
| and finer-grained callbacks invoked (if non-NULL) to produce the request |
| headers and body. */ |
| static svn_error_t * |
| setup_request(serf_request_t *request, |
| svn_ra_serf__handler_t *handler, |
| serf_bucket_t **req_bkt, |
| apr_pool_t *request_pool, |
| apr_pool_t *scratch_pool) |
| { |
| serf_bucket_t *body_bkt; |
| serf_bucket_t *headers_bkt; |
| const char *accept_encoding; |
| |
| if (handler->body_delegate) |
| { |
| serf_bucket_alloc_t *bkt_alloc = serf_request_get_alloc(request); |
| |
| SVN_ERR(handler->body_delegate(&body_bkt, handler->body_delegate_baton, |
| bkt_alloc, request_pool, scratch_pool)); |
| } |
| else |
| { |
| body_bkt = NULL; |
| } |
| |
| if (handler->custom_accept_encoding) |
| { |
| accept_encoding = NULL; |
| } |
| else if (handler->session->using_compression != svn_tristate_false) |
| { |
| /* Accept gzip compression if enabled. */ |
| accept_encoding = "gzip"; |
| } |
| else |
| { |
| accept_encoding = NULL; |
| } |
| |
| SVN_ERR(setup_serf_req(request, req_bkt, &headers_bkt, |
| handler->session, handler->method, handler->path, |
| body_bkt, handler->body_type, accept_encoding, |
| !handler->no_dav_headers, request_pool, |
| scratch_pool)); |
| |
| if (handler->header_delegate) |
| { |
| SVN_ERR(handler->header_delegate(headers_bkt, |
| handler->header_delegate_baton, |
| request_pool, scratch_pool)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Implements the serf_request_setup_t interface (which sets up both a |
| request and its response handler callback). Handles errors for |
| setup_request_cb */ |
| static apr_status_t |
| setup_request_cb(serf_request_t *request, |
| void *setup_baton, |
| serf_bucket_t **req_bkt, |
| serf_response_acceptor_t *acceptor, |
| void **acceptor_baton, |
| serf_response_handler_t *s_handler, |
| void **s_handler_baton, |
| apr_pool_t *request_pool) |
| { |
| svn_ra_serf__handler_t *handler = setup_baton; |
| apr_pool_t *scratch_pool; |
| svn_error_t *err; |
| |
| /* Construct a scratch_pool? serf gives us a pool that will live for |
| the duration of the request. But requests are retried in some cases */ |
| scratch_pool = svn_pool_create(request_pool); |
| |
| if (strcmp(handler->method, "HEAD") == 0) |
| *acceptor = accept_head; |
| else |
| *acceptor = accept_response; |
| *acceptor_baton = handler; |
| |
| *s_handler = handle_response_cb; |
| *s_handler_baton = handler; |
| |
| err = svn_error_trace(setup_request(request, handler, req_bkt, |
| request_pool, scratch_pool)); |
| |
| svn_pool_destroy(scratch_pool); |
| return save_error(handler->session, err); |
| } |
| |
| void |
| svn_ra_serf__request_create(svn_ra_serf__handler_t *handler) |
| { |
| SVN_ERR_ASSERT_NO_RETURN(handler->handler_pool != NULL |
| && !handler->scheduled); |
| |
| /* In case HANDLER is re-queued, reset the various transient fields. */ |
| handler->done = FALSE; |
| handler->server_error = NULL; |
| handler->sline.version = 0; |
| handler->location = NULL; |
| handler->reading_body = FALSE; |
| handler->discard_body = FALSE; |
| handler->scheduled = TRUE; |
| |
| /* Keeping track of the returned request object would be nice, but doesn't |
| work the way we would expect in ra_serf.. |
| |
| Serf sometimes creates a new request for us (and destroys the old one) |
| without telling, like when authentication failed (401/407 response. |
| |
| We 'just' trust serf to do the right thing and expect it to tell us |
| when the state of the request changes. |
| |
| ### I fixed a request leak in serf in r2258 on auth failures. |
| */ |
| (void) serf_connection_request_create(handler->conn->conn, |
| setup_request_cb, handler); |
| } |
| |
| |
| svn_error_t * |
| svn_ra_serf__discover_vcc(const char **vcc_url, |
| svn_ra_serf__session_t *session, |
| apr_pool_t *scratch_pool) |
| { |
| const char *path; |
| const char *relative_path; |
| const char *uuid; |
| |
| /* If we've already got the information our caller seeks, just return it. */ |
| if (session->vcc_url && session->repos_root_str) |
| { |
| *vcc_url = session->vcc_url; |
| return SVN_NO_ERROR; |
| } |
| |
| path = session->session_url.path; |
| *vcc_url = NULL; |
| uuid = NULL; |
| |
| do |
| { |
| apr_hash_t *props; |
| svn_error_t *err; |
| |
| err = svn_ra_serf__fetch_node_props(&props, session, |
| path, SVN_INVALID_REVNUM, |
| base_props, |
| scratch_pool, scratch_pool); |
| if (! err) |
| { |
| apr_hash_t *ns_props; |
| |
| ns_props = apr_hash_get(props, "DAV:", 4); |
| *vcc_url = svn_prop_get_value(ns_props, |
| "version-controlled-configuration"); |
| |
| ns_props = svn_hash_gets(props, SVN_DAV_PROP_NS_DAV); |
| relative_path = svn_prop_get_value(ns_props, |
| "baseline-relative-path"); |
| uuid = svn_prop_get_value(ns_props, "repository-uuid"); |
| break; |
| } |
| else |
| { |
| if ((err->apr_err != SVN_ERR_FS_NOT_FOUND) && |
| (err->apr_err != SVN_ERR_RA_DAV_FORBIDDEN)) |
| { |
| return svn_error_trace(err); /* found a _real_ error */ |
| } |
| else |
| { |
| /* This happens when the file is missing in HEAD. */ |
| svn_error_clear(err); |
| |
| /* Okay, strip off a component from PATH. */ |
| path = svn_urlpath__dirname(path, scratch_pool); |
| } |
| } |
| } |
| while ((path[0] != '\0') |
| && (! (path[0] == '/' && path[1] == '\0'))); |
| |
| if (!*vcc_url) |
| { |
| return svn_error_create(SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL, |
| _("The PROPFIND 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, scratch_pool); |
| |
| svn_path_remove_components(url_buf, |
| svn_path_component_count(relative_path)); |
| |
| /* Now recreate the root_url. */ |
| session->repos_root = session->session_url; |
| session->repos_root.path = |
| (char *)svn_fspath__canonicalize(url_buf->data, session->pool); |
| session->repos_root_str = |
| svn_urlpath__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); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_ra_serf__get_relative_path(const char **rel_path, |
| const char *orig_path, |
| svn_ra_serf__session_t *session, |
| apr_pool_t *pool) |
| { |
| const char *decoded_root, *decoded_orig; |
| |
| if (! session->repos_root.path) |
| { |
| const char *vcc_url; |
| |
| /* This should only happen if we haven't detected HTTP v2 |
| support from the server. */ |
| assert(! SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)); |
| |
| /* We don't actually care about the VCC_URL, but this API |
| promises to populate the session's root-url cache, and that's |
| what we really want. */ |
| SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, |
| pool)); |
| } |
| |
| decoded_root = svn_path_uri_decode(session->repos_root.path, pool); |
| decoded_orig = svn_path_uri_decode(orig_path, pool); |
| *rel_path = svn_urlpath__skip_ancestor(decoded_root, decoded_orig); |
| SVN_ERR_ASSERT(*rel_path != NULL); |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_ra_serf__report_resource(const char **report_target, |
| svn_ra_serf__session_t *session, |
| apr_pool_t *pool) |
| { |
| /* If we have HTTP v2 support, we want to report against the 'me' |
| resource. */ |
| if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)) |
| *report_target = apr_pstrdup(pool, session->me_resource); |
| |
| /* Otherwise, we'll use the default VCC. */ |
| else |
| SVN_ERR(svn_ra_serf__discover_vcc(report_target, session, pool)); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_ra_serf__error_on_status(serf_status_line sline, |
| const char *path, |
| const char *location) |
| { |
| switch(sline.code) |
| { |
| case 301: |
| case 302: |
| case 303: |
| case 307: |
| case 308: |
| return svn_error_createf(SVN_ERR_RA_DAV_RELOCATED, NULL, |
| (sline.code == 301) |
| ? _("Repository moved permanently to '%s'") |
| : _("Repository moved temporarily to '%s'"), |
| location); |
| case 403: |
| return svn_error_createf(SVN_ERR_RA_DAV_FORBIDDEN, NULL, |
| _("Access to '%s' forbidden"), path); |
| |
| case 404: |
| return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, |
| _("'%s' path not found"), path); |
| case 405: |
| return svn_error_createf(SVN_ERR_RA_DAV_METHOD_NOT_ALLOWED, NULL, |
| _("HTTP method is not allowed on '%s'"), |
| path); |
| case 409: |
| return svn_error_createf(SVN_ERR_FS_CONFLICT, NULL, |
| _("'%s' conflicts"), path); |
| case 412: |
| return svn_error_createf(SVN_ERR_RA_DAV_PRECONDITION_FAILED, NULL, |
| _("Precondition on '%s' failed"), path); |
| case 423: |
| return svn_error_createf(SVN_ERR_FS_NO_LOCK_TOKEN, NULL, |
| _("'%s': no lock token available"), path); |
| |
| case 411: |
| return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, |
| _("DAV request failed: 411 Content length required. The " |
| "server or an intermediate proxy does not accept " |
| "chunked encoding. Try setting 'http-chunked-requests' " |
| "to 'auto' or 'no' in your client configuration.")); |
| case 500: |
| return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, |
| _("Unexpected server error %d '%s' on '%s'"), |
| sline.code, sline.reason, path); |
| case 501: |
| return svn_error_createf(SVN_ERR_UNSUPPORTED_FEATURE, NULL, |
| _("The requested feature is not supported by " |
| "'%s'"), path); |
| } |
| |
| if (sline.code >= 300 || sline.code <= 199) |
| return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, |
| _("Unexpected HTTP status %d '%s' on '%s'"), |
| sline.code, sline.reason, path); |
| |
| return SVN_NO_ERROR; |
| } |
| |
| svn_error_t * |
| svn_ra_serf__unexpected_status(svn_ra_serf__handler_t *handler) |
| { |
| /* Is it a standard error status? */ |
| if (handler->sline.code != 405) |
| SVN_ERR(svn_ra_serf__error_on_status(handler->sline, |
| handler->path, |
| handler->location)); |
| |
| switch (handler->sline.code) |
| { |
| case 201: |
| return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, |
| _("Path '%s' unexpectedly created"), |
| handler->path); |
| case 204: |
| return svn_error_createf(SVN_ERR_FS_ALREADY_EXISTS, NULL, |
| _("Path '%s' already exists"), |
| handler->path); |
| |
| case 405: |
| return svn_error_createf(SVN_ERR_RA_DAV_METHOD_NOT_ALLOWED, NULL, |
| _("The HTTP method '%s' is not allowed" |
| " on '%s'"), |
| handler->method, handler->path); |
| default: |
| return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, |
| _("Unexpected HTTP status %d '%s' on '%s' " |
| "request to '%s'"), |
| handler->sline.code, handler->sline.reason, |
| handler->method, handler->path); |
| } |
| } |
| |
| svn_error_t * |
| svn_ra_serf__register_editor_shim_callbacks(svn_ra_session_t *ra_session, |
| svn_delta_shim_callbacks_t *callbacks) |
| { |
| svn_ra_serf__session_t *session = ra_session->priv; |
| |
| session->shim_callbacks = callbacks; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Shared/standard done_delegate handler */ |
| static svn_error_t * |
| response_done(serf_request_t *request, |
| void *handler_baton, |
| apr_pool_t *scratch_pool) |
| { |
| svn_ra_serf__handler_t *handler = handler_baton; |
| |
| assert(handler->done); |
| |
| if (handler->no_fail_on_http_failure_status) |
| return SVN_NO_ERROR; |
| |
| if (handler->server_error) |
| return svn_ra_serf__server_error_create(handler, scratch_pool); |
| |
| if (handler->sline.code >= 400 || handler->sline.code <= 199) |
| { |
| return svn_error_trace(svn_ra_serf__unexpected_status(handler)); |
| } |
| |
| if ((handler->sline.code >= 300 && handler->sline.code < 399) |
| && !handler->no_fail_on_http_redirect_status) |
| { |
| return svn_error_trace(svn_ra_serf__unexpected_status(handler)); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Pool cleanup handler for request handlers. |
| |
| If a serf context run stops for some outside error, like when the user |
| cancels a request via ^C in the context loop, the handler is still |
| registered in the serf context. With the pool cleanup there would be |
| handlers registered in no freed memory. |
| |
| This fallback kills the connection for this case, which will make serf |
| unregister any outstanding requests on it. */ |
| static apr_status_t |
| handler_cleanup(void *baton) |
| { |
| svn_ra_serf__handler_t *handler = baton; |
| if (handler->scheduled) |
| { |
| svn_ra_serf__unschedule_handler(handler); |
| } |
| |
| return APR_SUCCESS; |
| } |
| |
| svn_ra_serf__handler_t * |
| svn_ra_serf__create_handler(svn_ra_serf__session_t *session, |
| apr_pool_t *result_pool) |
| { |
| svn_ra_serf__handler_t *handler; |
| |
| handler = apr_pcalloc(result_pool, sizeof(*handler)); |
| handler->handler_pool = result_pool; |
| |
| apr_pool_cleanup_register(result_pool, handler, handler_cleanup, |
| apr_pool_cleanup_null); |
| |
| handler->session = session; |
| handler->conn = session->conns[0]; |
| |
| /* Setup the default done handler, to handle server errors */ |
| handler->done_delegate_baton = handler; |
| handler->done_delegate = response_done; |
| |
| return handler; |
| } |
| |
| svn_error_t * |
| svn_ra_serf__uri_parse(apr_uri_t *uri, |
| const char *url_str, |
| apr_pool_t *result_pool) |
| { |
| apr_status_t status; |
| |
| status = apr_uri_parse(result_pool, url_str, uri); |
| if (status) |
| { |
| /* Do not use returned error status in error message because currently |
| apr_uri_parse() returns APR_EGENERAL for all parsing errors. */ |
| return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, |
| _("Illegal URL '%s'"), |
| url_str); |
| } |
| |
| /* Depending the version of apr-util in use, for root paths uri.path |
| will be NULL or "", where serf requires "/". */ |
| if (uri->path == NULL || uri->path[0] == '\0') |
| { |
| uri->path = apr_pstrdup(result_pool, "/"); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| void |
| svn_ra_serf__setup_svndiff_accept_encoding(serf_bucket_t *headers, |
| svn_ra_serf__session_t *session) |
| { |
| if (session->using_compression == svn_tristate_false) |
| { |
| /* Don't advertise support for compressed svndiff formats if |
| compression is disabled. */ |
| serf_bucket_headers_setn( |
| headers, "Accept-Encoding", "svndiff"); |
| } |
| else if (session->using_compression == svn_tristate_unknown && |
| svn_ra_serf__is_low_latency_connection(session)) |
| { |
| /* With http-compression=auto, advertise that we prefer svndiff2 |
| to svndiff1 with a low latency connection (assuming the underlying |
| network has high bandwidth), as it is faster and in this case, we |
| don't care about worse compression ratio. */ |
| serf_bucket_headers_setn( |
| headers, "Accept-Encoding", |
| "gzip,svndiff2;q=0.9,svndiff1;q=0.8,svndiff;q=0.7"); |
| } |
| else |
| { |
| /* Otherwise, advertise that we prefer svndiff1 over svndiff2. |
| svndiff2 is not a reasonable substitute for svndiff1 with default |
| compression level, because, while it is faster, it also gives worse |
| compression ratio. While we can use svndiff2 in some cases (see |
| above), we can't do this generally. */ |
| serf_bucket_headers_setn( |
| headers, "Accept-Encoding", |
| "gzip,svndiff1;q=0.9,svndiff2;q=0.8,svndiff;q=0.7"); |
| } |
| } |
| |
| svn_boolean_t |
| svn_ra_serf__is_low_latency_connection(svn_ra_serf__session_t *session) |
| { |
| return session->conn_latency >= 0 && |
| session->conn_latency < apr_time_from_msec(5); |
| } |
| |
| apr_array_header_t * |
| svn_ra_serf__get_dirent_props(apr_uint32_t dirent_fields, |
| svn_ra_serf__session_t *session, |
| apr_pool_t *result_pool) |
| { |
| svn_ra_serf__dav_props_t *prop; |
| apr_array_header_t *props = apr_array_make |
| (result_pool, 7, sizeof(svn_ra_serf__dav_props_t)); |
| |
| if (session->supports_deadprop_count != svn_tristate_false |
| || ! (dirent_fields & SVN_DIRENT_HAS_PROPS)) |
| { |
| if (dirent_fields & SVN_DIRENT_KIND) |
| { |
| prop = apr_array_push(props); |
| prop->xmlns = "DAV:"; |
| prop->name = "resourcetype"; |
| } |
| |
| if (dirent_fields & SVN_DIRENT_SIZE) |
| { |
| prop = apr_array_push(props); |
| prop->xmlns = "DAV:"; |
| prop->name = "getcontentlength"; |
| } |
| |
| if (dirent_fields & SVN_DIRENT_HAS_PROPS) |
| { |
| prop = apr_array_push(props); |
| prop->xmlns = SVN_DAV_PROP_NS_DAV; |
| prop->name = "deadprop-count"; |
| } |
| |
| if (dirent_fields & SVN_DIRENT_CREATED_REV) |
| { |
| svn_ra_serf__dav_props_t *p = apr_array_push(props); |
| p->xmlns = "DAV:"; |
| p->name = SVN_DAV__VERSION_NAME; |
| } |
| |
| if (dirent_fields & SVN_DIRENT_TIME) |
| { |
| prop = apr_array_push(props); |
| prop->xmlns = "DAV:"; |
| prop->name = SVN_DAV__CREATIONDATE; |
| } |
| |
| if (dirent_fields & SVN_DIRENT_LAST_AUTHOR) |
| { |
| prop = apr_array_push(props); |
| prop->xmlns = "DAV:"; |
| prop->name = "creator-displayname"; |
| } |
| } |
| else |
| { |
| /* We found an old subversion server that can't handle |
| the deadprop-count property in the way we expect. |
| |
| The neon behavior is to retrieve all properties in this case */ |
| prop = apr_array_push(props); |
| prop->xmlns = "DAV:"; |
| prop->name = "allprop"; |
| } |
| |
| return props; |
| } |
| |