diff options
author | Lorry <lorry@roadtrain.codethink.co.uk> | 2012-08-22 14:29:52 +0100 |
---|---|---|
committer | Lorry <lorry@roadtrain.codethink.co.uk> | 2012-08-22 14:29:52 +0100 |
commit | f1bdf13786f0752c0846cf36f0d91e4fc6747929 (patch) | |
tree | 4223b2035bf2240d681a53822808b3c7f687b905 /subversion/svnserve | |
download | subversion-tarball-f1bdf13786f0752c0846cf36f0d91e4fc6747929.tar.gz |
Tarball conversion
Diffstat (limited to 'subversion/svnserve')
-rw-r--r-- | subversion/svnserve/cyrus_auth.c | 381 | ||||
-rw-r--r-- | subversion/svnserve/log-escape.c | 143 | ||||
-rw-r--r-- | subversion/svnserve/main.c | 1054 | ||||
-rw-r--r-- | subversion/svnserve/serve.c | 3238 | ||||
-rw-r--r-- | subversion/svnserve/server.h | 187 | ||||
-rw-r--r-- | subversion/svnserve/svnserve.8 | 138 | ||||
-rw-r--r-- | subversion/svnserve/svnserve.conf.5 | 98 | ||||
-rw-r--r-- | subversion/svnserve/winservice.c | 490 | ||||
-rw-r--r-- | subversion/svnserve/winservice.h | 64 |
9 files changed, 5793 insertions, 0 deletions
diff --git a/subversion/svnserve/cyrus_auth.c b/subversion/svnserve/cyrus_auth.c new file mode 100644 index 0000000..8b4dda9 --- /dev/null +++ b/subversion/svnserve/cyrus_auth.c @@ -0,0 +1,381 @@ +/* + * sasl_auth.c : Functions for SASL-based authentication + * + * ==================================================================== + * 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 "svn_private_config.h" +#ifdef SVN_HAVE_SASL + +#define APR_WANT_STRFUNC +#include <apr_want.h> +#include <apr_general.h> +#include <apr_strings.h> + +#include "svn_types.h" +#include "svn_string.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_ra_svn.h" +#include "svn_base64.h" + +#include "private/svn_atomic.h" +#include "private/ra_svn_sasl.h" + +#include "server.h" + +/* SASL calls this function before doing anything with a username, which gives + us an opportunity to do some sanity-checking. If the username contains + an '@', SASL interprets the part following the '@' as the name of the + authentication realm, and worst of all, this realm overrides the one that + we pass to sasl_server_new(). If we didn't check this, a user that could + successfully authenticate in one realm would be able to authenticate + in any other realm, simply by appending '@realm' to his username. + + Note that the value returned in *OUT does not need to be + '\0'-terminated; we just need to set *OUT_LEN correctly. +*/ +static int canonicalize_username(sasl_conn_t *conn, + void *context, /* not used */ + const char *in, /* the username */ + unsigned inlen, /* its length */ + unsigned flags, /* not used */ + const char *user_realm, + char *out, /* the output buffer */ + unsigned out_max, unsigned *out_len) +{ + int realm_len = strlen(user_realm); + char *pos; + + *out_len = inlen; + + /* If the username contains an '@', the part after the '@' is the realm + that the user wants to authenticate in. */ + pos = memchr(in, '@', inlen); + if (pos) + { + /* The only valid realm is user_realm (i.e. the repository's realm). + If the user gave us another realm, complain. */ + if (strncmp(pos+1, user_realm, inlen-(pos-in+1)) != 0) + return SASL_BADPROT; + } + else + *out_len += realm_len + 1; + + /* First, check that the output buffer is large enough. */ + if (*out_len > out_max) + return SASL_BADPROT; + + /* Copy the username part. */ + strncpy(out, in, inlen); + + /* If necessary, copy the realm part. */ + if (!pos) + { + out[inlen] = '@'; + strncpy(&out[inlen+1], user_realm, realm_len); + } + + return SASL_OK; +} + +static sasl_callback_t callbacks[] = +{ + { SASL_CB_CANON_USER, canonicalize_username, NULL }, + { SASL_CB_LIST_END, NULL, NULL } +}; + +static svn_error_t *initialize(void *baton, apr_pool_t *pool) +{ + int result; + apr_status_t status; + + status = svn_ra_svn__sasl_common_init(pool); + if (status) + return svn_error_wrap_apr(status, + _("Could not initialize the SASL library")); + + /* The second parameter tells SASL to look for a configuration file + named subversion.conf. */ + result = sasl_server_init(callbacks, SVN_RA_SVN_SASL_NAME); + if (result != SASL_OK) + { + svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + sasl_errstring(result, NULL, NULL)); + return svn_error_quick_wrap(err, + _("Could not initialize the SASL library")); + } + return SVN_NO_ERROR; +} + +svn_error_t *cyrus_init(apr_pool_t *pool) +{ + SVN_ERR(svn_atomic__init_once(&svn_ra_svn__sasl_status, + initialize, NULL, pool)); + return SVN_NO_ERROR; +} + +/* Tell the client the authentication failed. This is only used during + the authentication exchange (i.e. inside try_auth()). */ +static svn_error_t * +fail_auth(svn_ra_svn_conn_t *conn, apr_pool_t *pool, sasl_conn_t *sasl_ctx) +{ + const char *msg = sasl_errdetail(sasl_ctx); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(c)", "failure", msg)); + return svn_ra_svn_flush(conn, pool); +} + +/* Like svn_ra_svn_write_cmd_failure, but also clears the given error + and sets it to SVN_NO_ERROR. */ +static svn_error_t * +write_failure(svn_ra_svn_conn_t *conn, apr_pool_t *pool, svn_error_t **err_p) +{ + svn_error_t *write_err = svn_ra_svn_write_cmd_failure(conn, pool, *err_p); + svn_error_clear(*err_p); + *err_p = SVN_NO_ERROR; + return write_err; +} + +/* Used if we run into a SASL error outside try_auth(). */ +static svn_error_t * +fail_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool, sasl_conn_t *sasl_ctx) +{ + svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + sasl_errdetail(sasl_ctx)); + SVN_ERR(write_failure(conn, pool, &err)); + return svn_ra_svn_flush(conn, pool); +} + +static svn_error_t *try_auth(svn_ra_svn_conn_t *conn, + sasl_conn_t *sasl_ctx, + apr_pool_t *pool, + server_baton_t *b, + svn_boolean_t *success) +{ + const char *out, *mech; + const svn_string_t *arg = NULL, *in; + unsigned int outlen; + int result; + svn_boolean_t use_base64; + + *success = FALSE; + + /* Read the client's chosen mech and the initial token. */ + SVN_ERR(svn_ra_svn_read_tuple(conn, pool, "w(?s)", &mech, &in)); + + if (strcmp(mech, "EXTERNAL") == 0 && !in) + in = svn_string_create(b->tunnel_user, pool); + else if (in) + in = svn_base64_decode_string(in, pool); + + /* For CRAM-MD5, we don't base64-encode stuff. */ + use_base64 = (strcmp(mech, "CRAM-MD5") != 0); + + result = sasl_server_start(sasl_ctx, mech, + in ? in->data : NULL, + in ? in->len : 0, &out, &outlen); + + if (result != SASL_OK && result != SASL_CONTINUE) + return fail_auth(conn, pool, sasl_ctx); + + while (result == SASL_CONTINUE) + { + svn_ra_svn_item_t *item; + + arg = svn_string_ncreate(out, outlen, pool); + /* Encode what we send to the client. */ + if (use_base64) + arg = svn_base64_encode_string2(arg, TRUE, pool); + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(s)", "step", arg)); + + /* Read and decode the client response. */ + SVN_ERR(svn_ra_svn_read_item(conn, pool, &item)); + if (item->kind != SVN_RA_SVN_STRING) + return SVN_NO_ERROR; + + in = item->u.string; + if (use_base64) + in = svn_base64_decode_string(in, pool); + result = sasl_server_step(sasl_ctx, in->data, in->len, &out, &outlen); + } + + if (result != SASL_OK) + return fail_auth(conn, pool, sasl_ctx); + + /* Send our last response, if necessary. */ + if (outlen) + arg = svn_base64_encode_string2(svn_string_ncreate(out, outlen, pool), TRUE, + pool); + else + arg = NULL; + + *success = TRUE; + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(?s)", "success", arg)); + + return SVN_NO_ERROR; +} + +static apr_status_t sasl_dispose_cb(void *data) +{ + sasl_conn_t *sasl_ctx = (sasl_conn_t*) data; + sasl_dispose(&sasl_ctx); + return APR_SUCCESS; +} + +svn_error_t *cyrus_auth_request(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, + server_baton_t *b, + enum access_type required, + svn_boolean_t needs_username) +{ + sasl_conn_t *sasl_ctx; + apr_pool_t *subpool; + apr_status_t apr_err; + const char *localaddrport = NULL, *remoteaddrport = NULL; + const char *mechlist, *val; + char hostname[APRMAXHOSTLEN + 1]; + sasl_security_properties_t secprops; + svn_boolean_t success, no_anonymous; + int mech_count, result = SASL_OK; + + SVN_ERR(svn_ra_svn__get_addresses(&localaddrport, &remoteaddrport, + conn, pool)); + apr_err = apr_gethostname(hostname, sizeof(hostname), pool); + if (apr_err) + { + svn_error_t *err = svn_error_wrap_apr(apr_err, _("Can't get hostname")); + SVN_ERR(write_failure(conn, pool, &err)); + return svn_ra_svn_flush(conn, pool); + } + + /* Create a SASL context. SASL_SUCCESS_DATA tells SASL that the protocol + supports sending data along with the final "success" message. */ + result = sasl_server_new(SVN_RA_SVN_SASL_NAME, + hostname, b->realm, + localaddrport, remoteaddrport, + NULL, SASL_SUCCESS_DATA, + &sasl_ctx); + if (result != SASL_OK) + { + svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + sasl_errstring(result, NULL, NULL)); + SVN_ERR(write_failure(conn, pool, &err)); + return svn_ra_svn_flush(conn, pool); + } + + /* Make sure the context is always destroyed. */ + apr_pool_cleanup_register(b->pool, sasl_ctx, sasl_dispose_cb, + apr_pool_cleanup_null); + + /* Initialize security properties. */ + svn_ra_svn__default_secprops(&secprops); + + /* Don't allow ANONYMOUS if a username is required. */ + no_anonymous = needs_username || get_access(b, UNAUTHENTICATED) < required; + if (no_anonymous) + secprops.security_flags |= SASL_SEC_NOANONYMOUS; + + svn_config_get(b->cfg, &val, + SVN_CONFIG_SECTION_SASL, + SVN_CONFIG_OPTION_MIN_SSF, + "0"); + SVN_ERR(svn_cstring_atoui(&secprops.min_ssf, val)); + + svn_config_get(b->cfg, &val, + SVN_CONFIG_SECTION_SASL, + SVN_CONFIG_OPTION_MAX_SSF, + "256"); + SVN_ERR(svn_cstring_atoui(&secprops.max_ssf, val)); + + /* Set security properties. */ + result = sasl_setprop(sasl_ctx, SASL_SEC_PROPS, &secprops); + if (result != SASL_OK) + return fail_cmd(conn, pool, sasl_ctx); + + /* SASL needs to know if we are externally authenticated. */ + if (b->tunnel_user) + result = sasl_setprop(sasl_ctx, SASL_AUTH_EXTERNAL, b->tunnel_user); + if (result != SASL_OK) + return fail_cmd(conn, pool, sasl_ctx); + + /* Get the list of mechanisms. */ + result = sasl_listmech(sasl_ctx, NULL, NULL, " ", NULL, + &mechlist, NULL, &mech_count); + + if (result != SASL_OK) + return fail_cmd(conn, pool, sasl_ctx); + + if (mech_count == 0) + { + svn_error_t *err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + _("Could not obtain the list" + " of SASL mechanisms")); + SVN_ERR(write_failure(conn, pool, &err)); + return svn_ra_svn_flush(conn, pool); + } + + /* Send the list of mechanisms and the realm to the client. */ + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "(w)c", + mechlist, b->realm)); + + /* The main authentication loop. */ + subpool = svn_pool_create(pool); + do + { + svn_pool_clear(subpool); + SVN_ERR(try_auth(conn, sasl_ctx, subpool, b, &success)); + } + while (!success); + svn_pool_destroy(subpool); + + SVN_ERR(svn_ra_svn__enable_sasl_encryption(conn, sasl_ctx, pool)); + + if (no_anonymous) + { + char *p; + const void *user; + + /* Get the authenticated username. */ + result = sasl_getprop(sasl_ctx, SASL_USERNAME, &user); + + if (result != SASL_OK) + return fail_cmd(conn, pool, sasl_ctx); + + if ((p = strchr(user, '@')) != NULL) + { + /* Drop the realm part. */ + b->user = apr_pstrndup(b->pool, user, p - (const char *)user); + } + else + { + svn_error_t *err; + err = svn_error_create(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + _("Couldn't obtain the authenticated" + " username")); + SVN_ERR(write_failure(conn, pool, &err)); + return svn_ra_svn_flush(conn, pool); + } + } + + return SVN_NO_ERROR; +} + +#endif /* SVN_HAVE_SASL */ diff --git a/subversion/svnserve/log-escape.c b/subversion/svnserve/log-escape.c new file mode 100644 index 0000000..79f5312 --- /dev/null +++ b/subversion/svnserve/log-escape.c @@ -0,0 +1,143 @@ +/* + * log-escape.c : Functions for escaping log items + * copied from Apache httpd + * + * ==================================================================== + * 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. + * + * ==================================================================== + * 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 <apr.h> +#define APR_WANT_STRFUNC +#include <apr_want.h> +#include "server.h" +#include "svn_ctype.h" + +/* copied from httpd-2.2.4/server/util.c */ +/* c2x takes an unsigned, and expects the caller has guaranteed that + * 0 <= what < 256... which usually means that you have to cast to + * unsigned char first, because (unsigned)(char)(x) first goes through + * signed extension to an int before the unsigned cast. + * + * The reason for this assumption is to assist gcc code generation -- + * the unsigned char -> unsigned extension is already done earlier in + * both uses of this code, so there's no need to waste time doing it + * again. + */ +static const char c2x_table[] = "0123456789abcdef"; + +/* copied from httpd-2.2.4/server/util.c */ +static APR_INLINE unsigned char *c2x(unsigned what, unsigned char prefix, + unsigned char *where) +{ +#if APR_CHARSET_EBCDIC + what = apr_xlate_conv_byte(ap_hdrs_to_ascii, (unsigned char)what); +#endif /*APR_CHARSET_EBCDIC*/ + *where++ = prefix; + *where++ = c2x_table[what >> 4]; + *where++ = c2x_table[what & 0xf]; + return where; +} + +/* copied from httpd-2.2.4/server/util.c */ +apr_size_t escape_errorlog_item(char *dest, const char *source, + apr_size_t buflen) +{ + unsigned char *d, *ep; + const unsigned char *s; + + if (!source || !buflen) { /* be safe */ + return 0; + } + + d = (unsigned char *)dest; + s = (const unsigned char *)source; + ep = d + buflen - 1; + + for (; d < ep && *s; ++s) { + + /* httpd-2.2.4/server/util.c has this: + if (TEST_CHAR(*s, T_ESCAPE_LOGITEM)) { + which does this same check with a fast lookup table. Well, + mostly the same; we don't escape quotes, as that does. + */ + if (*s && ( !svn_ctype_isprint(*s) + || *s == '\\' + || svn_ctype_iscntrl(*s))) { + *d++ = '\\'; + if (d >= ep) { + --d; + break; + } + + switch(*s) { + case '\b': + *d++ = 'b'; + break; + case '\n': + *d++ = 'n'; + break; + case '\r': + *d++ = 'r'; + break; + case '\t': + *d++ = 't'; + break; + case '\v': + *d++ = 'v'; + break; + case '\\': + *d++ = *s; + break; + case '"': /* no need for this in error log */ + d[-1] = *s; + break; + default: + if (d >= ep - 2) { + ep = --d; /* break the for loop as well */ + break; + } + c2x(*s, 'x', d); + d += 3; + } + } + else { + *d++ = *s; + } + } + *d = '\0'; + + return (d - (unsigned char *)dest); +} diff --git a/subversion/svnserve/main.c b/subversion/svnserve/main.c new file mode 100644 index 0000000..a3378bf --- /dev/null +++ b/subversion/svnserve/main.c @@ -0,0 +1,1054 @@ +/* + * main.c : Main control function for svnserve + * + * ==================================================================== + * 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. + * ==================================================================== + */ + + + +#define APR_WANT_STRFUNC +#include <apr_want.h> +#include <apr_general.h> +#include <apr_getopt.h> +#include <apr_network_io.h> +#include <apr_signal.h> +#include <apr_thread_proc.h> +#include <apr_portable.h> + +#include <locale.h> + +#include "svn_cmdline.h" +#include "svn_types.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_ra_svn.h" +#include "svn_utf.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_opt.h" +#include "svn_repos.h" +#include "svn_string.h" +#include "svn_cache_config.h" +#include "svn_version.h" +#include "svn_io.h" + +#include "svn_private_config.h" +#include "private/svn_dep_compat.h" +#include "winservice.h" + +#ifdef HAVE_UNISTD_H +#include <unistd.h> /* For getpid() */ +#endif + +#include "server.h" + +/* The strategy for handling incoming connections. Some of these may be + unavailable due to platform limitations. */ +enum connection_handling_mode { + connection_mode_fork, /* Create a process per connection */ + connection_mode_thread, /* Create a thread per connection */ + connection_mode_single /* One connection at a time in this process */ +}; + +/* The mode in which to run svnserve */ +enum run_mode { + run_mode_unspecified, + run_mode_inetd, + run_mode_daemon, + run_mode_tunnel, + run_mode_listen_once, + run_mode_service +}; + +#if APR_HAS_FORK +#if APR_HAS_THREADS + +#define CONNECTION_DEFAULT connection_mode_fork +#define CONNECTION_HAVE_THREAD_OPTION + +#else /* ! APR_HAS_THREADS */ + +#define CONNECTION_DEFAULT connection_mode_fork + +#endif /* ! APR_HAS_THREADS */ +#elif APR_HAS_THREADS /* and ! APR_HAS_FORK */ + +#define CONNECTION_DEFAULT connection_mode_thread + +#else /* ! APR_HAS_THREADS and ! APR_HAS_FORK */ + +#define CONNECTION_DEFAULT connection_mode_single + +#endif + + +#ifdef WIN32 +static apr_os_sock_t winservice_svnserve_accept_socket = INVALID_SOCKET; + +/* The SCM calls this function (on an arbitrary thread, not the main() + thread!) when it wants to stop the service. + + For now, our strategy is to close the listener socket, in order to + unblock main() and cause it to exit its accept loop. We cannot use + apr_socket_close, because that function deletes the apr_socket_t + structure, as well as closing the socket handle. If we called + apr_socket_close here, then main() will also call apr_socket_close, + resulting in a double-free. This way, we just close the kernel + socket handle, which causes the accept() function call to fail, + which causes main() to clean up the socket. So, memory gets freed + only once. + + This isn't pretty, but it's better than a lot of other options. + Currently, there is no "right" way to shut down svnserve. + + We store the OS handle rather than a pointer to the apr_socket_t + structure in order to eliminate any possibility of illegal memory + access. */ +void winservice_notify_stop(void) +{ + if (winservice_svnserve_accept_socket != INVALID_SOCKET) + closesocket(winservice_svnserve_accept_socket); +} +#endif /* _WIN32 */ + + +/* Option codes and descriptions for svnserve. + * + * The entire list must be terminated with an entry of nulls. + * + * APR requires that options without abbreviations + * have codes greater than 255. + */ +#define SVNSERVE_OPT_LISTEN_PORT 256 +#define SVNSERVE_OPT_LISTEN_HOST 257 +#define SVNSERVE_OPT_FOREGROUND 258 +#define SVNSERVE_OPT_TUNNEL_USER 259 +#define SVNSERVE_OPT_VERSION 260 +#define SVNSERVE_OPT_PID_FILE 261 +#define SVNSERVE_OPT_SERVICE 262 +#define SVNSERVE_OPT_CONFIG_FILE 263 +#define SVNSERVE_OPT_LOG_FILE 264 +#define SVNSERVE_OPT_CACHE_TXDELTAS 265 +#define SVNSERVE_OPT_CACHE_FULLTEXTS 266 + +static const apr_getopt_option_t svnserve__options[] = + { + {"daemon", 'd', 0, N_("daemon mode")}, + {"inetd", 'i', 0, N_("inetd mode")}, + {"tunnel", 't', 0, N_("tunnel mode")}, + {"listen-once", 'X', 0, N_("listen-once mode (useful for debugging)")}, +#ifdef WIN32 + {"service", SVNSERVE_OPT_SERVICE, 0, + N_("Windows service mode (Service Control Manager)")}, +#endif + {"root", 'r', 1, N_("root of directory to serve")}, + {"read-only", 'R', 0, + N_("force read only, overriding repository config file")}, + {"config-file", SVNSERVE_OPT_CONFIG_FILE, 1, + N_("read configuration from file ARG")}, + {"listen-port", SVNSERVE_OPT_LISTEN_PORT, 1, +#ifdef WIN32 + N_("listen port\n" + " " + "[mode: daemon, service, listen-once]")}, +#else + N_("listen port\n" + " " + "[mode: daemon, listen-once]")}, +#endif + {"listen-host", SVNSERVE_OPT_LISTEN_HOST, 1, +#ifdef WIN32 + N_("listen hostname or IP address\n" + " " + "[mode: daemon, service, listen-once]")}, +#else + N_("listen hostname or IP address\n" + " " + "[mode: daemon, listen-once]")}, +#endif + {"prefer-ipv6", '6', 0, + N_("prefer IPv6 when resolving the listen hostname\n" + " " + "[IPv4 is preferred by default. Using IPv4 and IPv6\n" + " " + "at the same time is not supported in daemon mode.\n" + " " + "Use inetd mode or tunnel mode if you need this.]")}, + {"compression", 'c', 1, + N_("compression level to use for network transmissions\n" + " " + "[0 .. no compression, 5 .. default, \n" + " " + " 9 .. maximum compression]")}, + {"memory-cache-size", 'M', 1, + N_("size of the extra in-memory cache in MB used to\n" + " " + "minimize redundant operations.\n" + " " + "Default is 128 for threaded and 16 for non-\n" + " " + "threaded mode.\n" + " " + "[used for FSFS repositories only]")}, + {"cache-txdeltas", SVNSERVE_OPT_CACHE_TXDELTAS, 1, + N_("enable or disable caching of deltas between older\n" + " " + "revisions.\n" + " " + "Default is no.\n" + " " + "[used for FSFS repositories only]")}, + {"cache-fulltexts", SVNSERVE_OPT_CACHE_FULLTEXTS, 1, + N_("enable or disable caching of file contents\n" + " " + "Default is yes.\n" + " " + "[used for FSFS repositories only]")}, +#ifdef CONNECTION_HAVE_THREAD_OPTION + /* ### Making the assumption here that WIN32 never has fork and so + * ### this option never exists when --service exists. */ + {"threads", 'T', 0, N_("use threads instead of fork " + "[mode: daemon]")}, +#endif + {"foreground", SVNSERVE_OPT_FOREGROUND, 0, + N_("run in foreground (useful for debugging)\n" + " " + "[mode: daemon]")}, + {"log-file", SVNSERVE_OPT_LOG_FILE, 1, + N_("svnserve log file")}, + {"pid-file", SVNSERVE_OPT_PID_FILE, 1, +#ifdef WIN32 + N_("write server process ID to file ARG\n" + " " + "[mode: daemon, listen-once, service]")}, +#else + N_("write server process ID to file ARG\n" + " " + "[mode: daemon, listen-once]")}, +#endif + {"tunnel-user", SVNSERVE_OPT_TUNNEL_USER, 1, + N_("tunnel username (default is current uid's name)\n" + " " + "[mode: tunnel]")}, + {"help", 'h', 0, N_("display this help")}, + {"version", SVNSERVE_OPT_VERSION, 0, + N_("show program version information")}, + {"quiet", 'q', 0, + N_("no progress (only errors) to stderr")}, + {0, 0, 0, 0} + }; + + +static void usage(const char *progname, apr_pool_t *pool) +{ + if (!progname) + progname = "svnserve"; + + svn_error_clear(svn_cmdline_fprintf(stderr, pool, + _("Type '%s --help' for usage.\n"), + progname)); + exit(1); +} + +static void help(apr_pool_t *pool) +{ + apr_size_t i; + +#ifdef WIN32 + svn_error_clear(svn_cmdline_fputs(_("usage: svnserve [-d | -i | -t | -X " + "| --service] [options]\n" + "\n" + "Valid options:\n"), + stdout, pool)); +#else + svn_error_clear(svn_cmdline_fputs(_("usage: svnserve [-d | -i | -t | -X] " + "[options]\n" + "\n" + "Valid options:\n"), + stdout, pool)); +#endif + for (i = 0; svnserve__options[i].name && svnserve__options[i].optch; i++) + { + const char *optstr; + svn_opt_format_option(&optstr, svnserve__options + i, TRUE, pool); + svn_error_clear(svn_cmdline_fprintf(stdout, pool, " %s\n", optstr)); + } + svn_error_clear(svn_cmdline_fprintf(stdout, pool, "\n")); + exit(0); +} + +static svn_error_t * version(svn_boolean_t quiet, apr_pool_t *pool) +{ + const char *fs_desc_start + = _("The following repository back-end (FS) modules are available:\n\n"); + + svn_stringbuf_t *version_footer; + + version_footer = svn_stringbuf_create(fs_desc_start, pool); + SVN_ERR(svn_fs_print_modules(version_footer, pool)); + +#ifdef SVN_HAVE_SASL + svn_stringbuf_appendcstr(version_footer, + _("\nCyrus SASL authentication is available.\n")); +#endif + + return svn_opt_print_help3(NULL, "svnserve", TRUE, quiet, version_footer->data, + NULL, NULL, NULL, NULL, NULL, pool); +} + + +#if APR_HAS_FORK +static void sigchld_handler(int signo) +{ + /* Nothing to do; we just need to interrupt the accept(). */ +} +#endif + +/* Redirect stdout to stderr. ARG is the pool. + * + * In tunnel or inetd mode, we don't want hook scripts corrupting the + * data stream by sending data to stdout, so we need to redirect + * stdout somewhere else. Sending it to stderr is acceptable; sending + * it to /dev/null is another option, but apr doesn't provide a way to + * do that without also detaching from the controlling terminal. + */ +static apr_status_t redirect_stdout(void *arg) +{ + apr_pool_t *pool = arg; + apr_file_t *out_file, *err_file; + apr_status_t apr_err; + + if ((apr_err = apr_file_open_stdout(&out_file, pool))) + return apr_err; + if ((apr_err = apr_file_open_stderr(&err_file, pool))) + return apr_err; + return apr_file_dup2(out_file, err_file, pool); +} + +/* "Arguments" passed from the main thread to the connection thread */ +struct serve_thread_t { + svn_ra_svn_conn_t *conn; + serve_params_t *params; + apr_pool_t *pool; +}; + +#if APR_HAS_THREADS +static void * APR_THREAD_FUNC serve_thread(apr_thread_t *tid, void *data) +{ + struct serve_thread_t *d = data; + + svn_error_clear(serve(d->conn, d->params, d->pool)); + svn_pool_destroy(d->pool); + + return NULL; +} +#endif + +/* Write the PID of the current process as a decimal number, followed by a + newline to the file FILENAME, using POOL for temporary allocations. */ +static svn_error_t *write_pid_file(const char *filename, apr_pool_t *pool) +{ + apr_file_t *file; + const char *contents = apr_psprintf(pool, "%" APR_PID_T_FMT "\n", + getpid()); + + SVN_ERR(svn_io_file_open(&file, filename, + APR_WRITE | APR_CREATE | APR_TRUNCATE, + APR_OS_DEFAULT, pool)); + SVN_ERR(svn_io_file_write_full(file, contents, strlen(contents), NULL, + pool)); + + SVN_ERR(svn_io_file_close(file, pool)); + + return SVN_NO_ERROR; +} + +/* Version compatibility check */ +static svn_error_t * +check_lib_versions(void) +{ + static const svn_version_checklist_t checklist[] = + { + { "svn_subr", svn_subr_version }, + { "svn_repos", svn_repos_version }, + { "svn_fs", svn_fs_version }, + { "svn_delta", svn_delta_version }, + { "svn_ra_svn", svn_ra_svn_version }, + { NULL, NULL } + }; + + SVN_VERSION_DEFINE(my_version); + return svn_ver_check_list(&my_version, checklist); +} + + +int main(int argc, const char *argv[]) +{ + enum run_mode run_mode = run_mode_unspecified; + svn_boolean_t foreground = FALSE; + apr_socket_t *sock, *usock; + apr_file_t *in_file, *out_file; + apr_sockaddr_t *sa; + apr_pool_t *pool; + apr_pool_t *connection_pool; + apr_allocator_t *allocator; + svn_error_t *err; + apr_getopt_t *os; + int opt; + serve_params_t params; + const char *arg; + apr_status_t status; + svn_ra_svn_conn_t *conn; + apr_proc_t proc; +#if APR_HAS_THREADS + apr_threadattr_t *tattr; + apr_thread_t *tid; + + struct serve_thread_t *thread_data; +#endif + enum connection_handling_mode handling_mode = CONNECTION_DEFAULT; + apr_uint16_t port = SVN_RA_SVN_PORT; + const char *host = NULL; + int family = APR_INET; + apr_int32_t sockaddr_info_flags = 0; + svn_boolean_t prefer_v6 = FALSE; + svn_boolean_t quiet = FALSE; + svn_boolean_t is_version = FALSE; + int mode_opt_count = 0; + const char *config_filename = NULL; + const char *pid_filename = NULL; + const char *log_filename = NULL; + svn_node_kind_t kind; + + /* Initialize the app. */ + if (svn_cmdline_init("svnserve", stderr) != EXIT_SUCCESS) + return EXIT_FAILURE; + + /* Create our top-level pool. */ + pool = svn_pool_create(NULL); + +#ifdef SVN_HAVE_SASL + SVN_INT_ERR(cyrus_init(pool)); +#endif + + /* Check library versions */ + err = check_lib_versions(); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + + /* Initialize the FS library. */ + err = svn_fs_initialize(pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + + err = svn_cmdline__getopt_init(&os, argc, argv, pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + + params.root = "/"; + params.tunnel = FALSE; + params.tunnel_user = NULL; + params.read_only = FALSE; + params.cfg = NULL; + params.pwdb = NULL; + params.authzdb = NULL; + params.compression_level = SVN_DELTA_COMPRESSION_LEVEL_DEFAULT; + params.log_file = NULL; + params.username_case = CASE_ASIS; + params.memory_cache_size = (apr_uint64_t)-1; + params.cache_fulltexts = TRUE; + params.cache_txdeltas = FALSE; + + while (1) + { + status = apr_getopt_long(os, svnserve__options, &opt, &arg); + if (APR_STATUS_IS_EOF(status)) + break; + if (status != APR_SUCCESS) + usage(argv[0], pool); + switch (opt) + { + case '6': + prefer_v6 = TRUE; + break; + + case 'h': + help(pool); + break; + + case 'q': + quiet = TRUE; + break; + + case SVNSERVE_OPT_VERSION: + is_version = TRUE; + break; + + case 'd': + if (run_mode != run_mode_daemon) + { + run_mode = run_mode_daemon; + mode_opt_count++; + } + break; + + case SVNSERVE_OPT_FOREGROUND: + foreground = TRUE; + break; + + case 'i': + if (run_mode != run_mode_inetd) + { + run_mode = run_mode_inetd; + mode_opt_count++; + } + break; + + case SVNSERVE_OPT_LISTEN_PORT: + { + apr_uint64_t val; + + err = svn_cstring_strtoui64(&val, arg, 0, APR_UINT16_MAX, 10); + if (err) + return svn_cmdline_handle_exit_error( + svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, err, + _("Invalid port '%s'"), arg), + pool, "svnserve: "); + port = (apr_uint16_t)val; + } + break; + + case SVNSERVE_OPT_LISTEN_HOST: + host = arg; + break; + + case 't': + if (run_mode != run_mode_tunnel) + { + run_mode = run_mode_tunnel; + mode_opt_count++; + } + break; + + case SVNSERVE_OPT_TUNNEL_USER: + params.tunnel_user = arg; + break; + + case 'X': + if (run_mode != run_mode_listen_once) + { + run_mode = run_mode_listen_once; + mode_opt_count++; + } + break; + + case 'r': + SVN_INT_ERR(svn_utf_cstring_to_utf8(¶ms.root, arg, pool)); + + err = svn_io_check_resolved_path(params.root, &kind, pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + if (kind != svn_node_dir) + { + svn_error_clear + (svn_cmdline_fprintf + (stderr, pool, + _("svnserve: Root path '%s' does not exist " + "or is not a directory.\n"), params.root)); + return EXIT_FAILURE; + } + + params.root = svn_dirent_internal_style(params.root, pool); + SVN_INT_ERR(svn_dirent_get_absolute(¶ms.root, params.root, pool)); + break; + + case 'R': + params.read_only = TRUE; + break; + + case 'T': + handling_mode = connection_mode_thread; + break; + + case 'c': + params.compression_level = atoi(arg); + if (params.compression_level < SVN_DELTA_COMPRESSION_LEVEL_NONE) + params.compression_level = SVN_DELTA_COMPRESSION_LEVEL_NONE; + if (params.compression_level > SVN_DELTA_COMPRESSION_LEVEL_MAX) + params.compression_level = SVN_DELTA_COMPRESSION_LEVEL_MAX; + break; + + case 'M': + params.memory_cache_size = 0x100000 * apr_strtoi64(arg, NULL, 0); + break; + + case SVNSERVE_OPT_CACHE_TXDELTAS: + params.cache_txdeltas + = svn_tristate__from_word(arg) == svn_tristate_true; + break; + + case SVNSERVE_OPT_CACHE_FULLTEXTS: + params.cache_fulltexts + = svn_tristate__from_word(arg) == svn_tristate_true; + break; + +#ifdef WIN32 + case SVNSERVE_OPT_SERVICE: + if (run_mode != run_mode_service) + { + run_mode = run_mode_service; + mode_opt_count++; + } + break; +#endif + + case SVNSERVE_OPT_CONFIG_FILE: + SVN_INT_ERR(svn_utf_cstring_to_utf8(&config_filename, arg, pool)); + config_filename = svn_dirent_internal_style(config_filename, pool); + SVN_INT_ERR(svn_dirent_get_absolute(&config_filename, config_filename, + pool)); + break; + + case SVNSERVE_OPT_PID_FILE: + SVN_INT_ERR(svn_utf_cstring_to_utf8(&pid_filename, arg, pool)); + pid_filename = svn_dirent_internal_style(pid_filename, pool); + SVN_INT_ERR(svn_dirent_get_absolute(&pid_filename, pid_filename, + pool)); + break; + + case SVNSERVE_OPT_LOG_FILE: + SVN_INT_ERR(svn_utf_cstring_to_utf8(&log_filename, arg, pool)); + log_filename = svn_dirent_internal_style(log_filename, pool); + SVN_INT_ERR(svn_dirent_get_absolute(&log_filename, log_filename, + pool)); + break; + + } + } + + if (is_version) + { + SVN_INT_ERR(version(quiet, pool)); + exit(0); + } + + if (os->ind != argc) + usage(argv[0], pool); + + if (mode_opt_count != 1) + { + svn_error_clear(svn_cmdline_fputs( +#ifdef WIN32 + _("You must specify exactly one of -d, -i, -t, " + "--service or -X.\n"), +#else + _("You must specify exactly one of -d, -i, -t or -X.\n"), +#endif + stderr, pool)); + usage(argv[0], pool); + } + + /* If a configuration file is specified, load it and any referenced + * password and authorization files. */ + if (config_filename) + SVN_INT_ERR(load_configs(¶ms.cfg, ¶ms.pwdb, ¶ms.authzdb, + ¶ms.username_case, config_filename, TRUE, + svn_dirent_dirname(config_filename, pool), + NULL, NULL, /* server baton, conn */ + pool)); + + if (log_filename) + SVN_INT_ERR(svn_io_file_open(¶ms.log_file, log_filename, + APR_WRITE | APR_CREATE | APR_APPEND, + APR_OS_DEFAULT, pool)); + + if (params.tunnel_user && run_mode != run_mode_tunnel) + { + svn_error_clear + (svn_cmdline_fprintf + (stderr, pool, + _("Option --tunnel-user is only valid in tunnel mode.\n"))); + exit(1); + } + + if (run_mode == run_mode_inetd || run_mode == run_mode_tunnel) + { + params.tunnel = (run_mode == run_mode_tunnel); + apr_pool_cleanup_register(pool, pool, apr_pool_cleanup_null, + redirect_stdout); + status = apr_file_open_stdin(&in_file, pool); + if (status) + { + err = svn_error_wrap_apr(status, _("Can't open stdin")); + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + } + + status = apr_file_open_stdout(&out_file, pool); + if (status) + { + err = svn_error_wrap_apr(status, _("Can't open stdout")); + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + } + + /* Use a subpool for the connection to ensure that if SASL is used + * the pool cleanup handlers that call sasl_dispose() (connection_pool) + * and sasl_done() (pool) are run in the right order. See issue #3664. */ + connection_pool = svn_pool_create(pool); + conn = svn_ra_svn_create_conn2(NULL, in_file, out_file, + params.compression_level, + connection_pool); + svn_error_clear(serve(conn, ¶ms, connection_pool)); + exit(0); + } + +#ifdef WIN32 + /* If svnserve needs to run as a Win32 service, then we need to + coordinate with the Service Control Manager (SCM) before + continuing. This function call registers the svnserve.exe + process with the SCM, waits for the "start" command from the SCM + (which will come very quickly), and confirms that those steps + succeeded. + + After this call succeeds, the service is free to run. At some + point in the future, the SCM will send a message to the service, + requesting that it stop. This is translated into a call to + winservice_notify_stop(). The service is then responsible for + cleanly terminating. + + We need to do this before actually starting the service logic + (opening files, sockets, etc.) because the SCM wants you to + connect *first*, then do your service-specific logic. If the + service process takes too long to connect to the SCM, then the + SCM will decide that the service is busted, and will give up on + it. + */ + if (run_mode == run_mode_service) + { + err = winservice_start(); + if (err) + { + svn_handle_error2(err, stderr, FALSE, "svnserve: "); + + /* This is the most common error. It means the user started + svnserve from a shell, and specified the --service + argument. svnserve cannot be started, as a service, in + this way. The --service argument is valid only valid if + svnserve is started by the SCM. */ + if (err->apr_err == + APR_FROM_OS_ERROR(ERROR_FAILED_SERVICE_CONTROLLER_CONNECT)) + { + svn_error_clear(svn_cmdline_fprintf(stderr, pool, + _("svnserve: The --service flag is only valid if the" + " process is started by the Service Control Manager.\n"))); + } + + svn_error_clear(err); + exit(1); + } + + /* The service is now in the "starting" state. Before the SCM will + consider the service "started", this thread must call the + winservice_running() function. */ + } +#endif /* WIN32 */ + + /* Make sure we have IPV6 support first before giving apr_sockaddr_info_get + APR_UNSPEC, because it may give us back an IPV6 address even if we can't + create IPV6 sockets. */ + +#if APR_HAVE_IPV6 +#ifdef MAX_SECS_TO_LINGER + /* ### old APR interface */ + status = apr_socket_create(&sock, APR_INET6, SOCK_STREAM, pool); +#else + status = apr_socket_create(&sock, APR_INET6, SOCK_STREAM, APR_PROTO_TCP, + pool); +#endif + if (status == 0) + { + apr_socket_close(sock); + family = APR_UNSPEC; + + if (prefer_v6) + { + if (host == NULL) + host = "::"; + sockaddr_info_flags = APR_IPV6_ADDR_OK; + } + else + { + if (host == NULL) + host = "0.0.0.0"; + sockaddr_info_flags = APR_IPV4_ADDR_OK; + } + } +#endif + + status = apr_sockaddr_info_get(&sa, host, family, port, + sockaddr_info_flags, pool); + if (status) + { + err = svn_error_wrap_apr(status, _("Can't get address info")); + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + } + + +#ifdef MAX_SECS_TO_LINGER + /* ### old APR interface */ + status = apr_socket_create(&sock, sa->family, SOCK_STREAM, pool); +#else + status = apr_socket_create(&sock, sa->family, SOCK_STREAM, APR_PROTO_TCP, + pool); +#endif + if (status) + { + err = svn_error_wrap_apr(status, _("Can't create server socket")); + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + } + + /* Prevents "socket in use" errors when server is killed and quickly + * restarted. */ + apr_socket_opt_set(sock, APR_SO_REUSEADDR, 1); + + status = apr_socket_bind(sock, sa); + if (status) + { + err = svn_error_wrap_apr(status, _("Can't bind server socket")); + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + } + + apr_socket_listen(sock, 7); + +#if APR_HAS_FORK + if (run_mode != run_mode_listen_once && !foreground) + apr_proc_detach(APR_PROC_DETACH_DAEMONIZE); + + apr_signal(SIGCHLD, sigchld_handler); +#endif + +#ifdef SIGPIPE + /* Disable SIGPIPE generation for the platforms that have it. */ + apr_signal(SIGPIPE, SIG_IGN); +#endif + +#ifdef SIGXFSZ + /* Disable SIGXFSZ generation for the platforms that have it, otherwise + * working with large files when compiled against an APR that doesn't have + * large file support will crash the program, which is uncool. */ + apr_signal(SIGXFSZ, SIG_IGN); +#endif + + if (pid_filename) + SVN_INT_ERR(write_pid_file(pid_filename, pool)); + +#ifdef WIN32 + status = apr_os_sock_get(&winservice_svnserve_accept_socket, sock); + if (status) + winservice_svnserve_accept_socket = INVALID_SOCKET; + + /* At this point, the service is "running". Notify the SCM. */ + if (run_mode == run_mode_service) + winservice_running(); +#endif + + /* Configure FS caches for maximum efficiency with svnserve. + * For pre-forked (i.e. multi-processed) mode of operation, + * keep the per-process caches smaller than the default. + * Also, apply the respective command line parameters, if given. */ + { + svn_cache_config_t settings = *svn_cache_config_get(); + + if (params.memory_cache_size != -1) + settings.cache_size = params.memory_cache_size; + + settings.single_threaded = TRUE; + if (handling_mode == connection_mode_thread) + { +#ifdef APR_HAS_THREADS + settings.single_threaded = FALSE; +#else + /* No requests will be processed at all + * (see "switch (handling_mode)" code further down). + * But if they were, some other synchronization code + * would need to take care of securing integrity of + * APR-based structures. That would include our caches. + */ +#endif + } + + svn_cache_config_set(&settings); + } + + while (1) + { +#ifdef WIN32 + if (winservice_is_stopping()) + return ERROR_SUCCESS; +#endif + + /* If we are using fulltext caches etc. we will allocate many large + chunks of memory of various sizes outside the cache for those + fulltexts. Make sure we use the memory wisely: use an allocator + that causes memory fragments to be given back to the OS early. */ + + if (apr_allocator_create(&allocator)) + return EXIT_FAILURE; + + apr_allocator_max_free_set(allocator, SVN_ALLOCATOR_RECOMMENDED_MAX_FREE); + + /* Non-standard pool handling. The main thread never blocks to join + the connection threads so it cannot clean up after each one. So + separate pools that can be cleared at thread exit are used. */ + + connection_pool = svn_pool_create_ex(NULL, allocator); + apr_allocator_owner_set(allocator, connection_pool); + + status = apr_socket_accept(&usock, sock, connection_pool); + if (handling_mode == connection_mode_fork) + { + /* Collect any zombie child processes. */ + while (apr_proc_wait_all_procs(&proc, NULL, NULL, APR_NOWAIT, + connection_pool) == APR_CHILD_DONE) + ; + } + if (APR_STATUS_IS_EINTR(status)) + { + svn_pool_destroy(connection_pool); + continue; + } + if (status) + { + err = svn_error_wrap_apr + (status, _("Can't accept client connection")); + return svn_cmdline_handle_exit_error(err, pool, "svnserve: "); + } + + /* Enable TCP keep-alives on the socket so we time out when + * the connection breaks due to network-layer problems. + * If the peer has dropped the connection due to a network partition + * or a crash, or if the peer no longer considers the connection + * valid because we are behind a NAT and our public IP has changed, + * it will respond to the keep-alive probe with a RST instead of an + * acknowledgment segment, which will cause svn to abort the session + * even while it is currently blocked waiting for data from the peer. */ + status = apr_socket_opt_set(usock, APR_SO_KEEPALIVE, 1); + if (status) + { + /* It's not a fatal error if we cannot enable keep-alives. */ + } + + conn = svn_ra_svn_create_conn2(usock, NULL, NULL, + params.compression_level, + connection_pool); + + if (run_mode == run_mode_listen_once) + { + err = serve(conn, ¶ms, connection_pool); + + if (err) + svn_handle_error2(err, stdout, FALSE, "svnserve: "); + svn_error_clear(err); + + apr_socket_close(usock); + apr_socket_close(sock); + exit(0); + } + + switch (handling_mode) + { + case connection_mode_fork: +#if APR_HAS_FORK + status = apr_proc_fork(&proc, connection_pool); + if (status == APR_INCHILD) + { + apr_socket_close(sock); + err = serve(conn, ¶ms, connection_pool); + log_error(err, params.log_file, + svn_ra_svn_conn_remote_host(conn), + NULL, NULL, /* user, repos */ + connection_pool); + svn_error_clear(err); + apr_socket_close(usock); + exit(0); + } + else if (status == APR_INPARENT) + { + apr_socket_close(usock); + } + else + { + err = svn_error_wrap_apr(status, "apr_proc_fork"); + log_error(err, params.log_file, + svn_ra_svn_conn_remote_host(conn), + NULL, NULL, /* user, repos */ + connection_pool); + svn_error_clear(err); + apr_socket_close(usock); + } + svn_pool_destroy(connection_pool); +#endif + break; + + case connection_mode_thread: + /* Create a detached thread for each connection. That's not a + particularly sophisticated strategy for a threaded server, it's + little different from forking one process per connection. */ +#if APR_HAS_THREADS + status = apr_threadattr_create(&tattr, connection_pool); + if (status) + { + err = svn_error_wrap_apr(status, _("Can't create threadattr")); + svn_handle_error2(err, stderr, FALSE, "svnserve: "); + svn_error_clear(err); + exit(1); + } + status = apr_threadattr_detach_set(tattr, 1); + if (status) + { + err = svn_error_wrap_apr(status, _("Can't set detached state")); + svn_handle_error2(err, stderr, FALSE, "svnserve: "); + svn_error_clear(err); + exit(1); + } + thread_data = apr_palloc(connection_pool, sizeof(*thread_data)); + thread_data->conn = conn; + thread_data->params = ¶ms; + thread_data->pool = connection_pool; + status = apr_thread_create(&tid, tattr, serve_thread, thread_data, + connection_pool); + if (status) + { + err = svn_error_wrap_apr(status, _("Can't create thread")); + svn_handle_error2(err, stderr, FALSE, "svnserve: "); + svn_error_clear(err); + exit(1); + } +#endif + break; + + case connection_mode_single: + /* Serve one connection at a time. */ + svn_error_clear(serve(conn, ¶ms, connection_pool)); + svn_pool_destroy(connection_pool); + } + } + + /* NOTREACHED */ +} diff --git a/subversion/svnserve/serve.c b/subversion/svnserve/serve.c new file mode 100644 index 0000000..8c4708d --- /dev/null +++ b/subversion/svnserve/serve.c @@ -0,0 +1,3238 @@ +/* + * serve.c : Functions for serving the Subversion protocol + * + * ==================================================================== + * 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 <limits.h> /* for UINT_MAX */ +#include <stdarg.h> + +#define APR_WANT_STRFUNC +#include <apr_want.h> +#include <apr_general.h> +#include <apr_lib.h> +#include <apr_strings.h> + +#include "svn_compat.h" +#include "svn_private_config.h" /* For SVN_PATH_LOCAL_SEPARATOR */ +#include "svn_types.h" +#include "svn_string.h" +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_ra.h" /* for SVN_RA_CAPABILITY_* */ +#include "svn_ra_svn.h" +#include "svn_repos.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_time.h" +#include "svn_config.h" +#include "svn_props.h" +#include "svn_mergeinfo.h" +#include "svn_user.h" + +#include "private/svn_log.h" +#include "private/svn_mergeinfo_private.h" +#include "private/svn_fspath.h" + +#ifdef HAVE_UNISTD_H +#include <unistd.h> /* For getpid() */ +#endif + +#include "server.h" + +typedef struct commit_callback_baton_t { + apr_pool_t *pool; + svn_revnum_t *new_rev; + const char **date; + const char **author; + const char **post_commit_err; +} commit_callback_baton_t; + +typedef struct report_driver_baton_t { + server_baton_t *sb; + const char *repos_url; /* Decoded repository URL. */ + void *report_baton; + svn_error_t *err; + /* so update() can distinguish checkout from update in logging */ + int entry_counter; + svn_boolean_t only_empty_entries; + /* for diff() logging */ + svn_revnum_t *from_rev; +} report_driver_baton_t; + +typedef struct log_baton_t { + const char *fs_path; + svn_ra_svn_conn_t *conn; + int stack_depth; +} log_baton_t; + +typedef struct file_revs_baton_t { + svn_ra_svn_conn_t *conn; + apr_pool_t *pool; /* Pool provided in the handler call. */ +} file_revs_baton_t; + +typedef struct fs_warning_baton_t { + server_baton_t *server; + svn_ra_svn_conn_t *conn; + apr_pool_t *pool; +} fs_warning_baton_t; + + +/* Write LEN bytes of ERRSTR to LOG_FILE with svn_io_file_write(). */ +static svn_error_t * +log_write(apr_file_t *log_file, const char *errstr, apr_size_t len, + apr_pool_t *pool) +{ + return svn_io_file_write(log_file, errstr, &len, pool); +} + +void +log_error(svn_error_t *err, apr_file_t *log_file, const char *remote_host, + const char *user, const char *repos, apr_pool_t *pool) +{ + const char *timestr, *continuation; + char errbuf[256]; + /* 8192 from MAX_STRING_LEN in from httpd-2.2.4/include/httpd.h */ + char errstr[8192]; + + if (err == SVN_NO_ERROR) + return; + + if (log_file == NULL) + return; + + timestr = svn_time_to_cstring(apr_time_now(), pool); + remote_host = (remote_host ? remote_host : "-"); + user = (user ? user : "-"); + repos = (repos ? repos : "-"); + + continuation = ""; + while (err != NULL) + { + const char *message = svn_err_best_message(err, errbuf, sizeof(errbuf)); + /* based on httpd-2.2.4/server/log.c:log_error_core */ + apr_size_t len = apr_snprintf(errstr, sizeof(errstr), + "%" APR_PID_T_FMT + " %s %s %s %s ERR%s %s %ld %d ", + getpid(), timestr, remote_host, user, + repos, continuation, + err->file ? err->file : "-", err->line, + err->apr_err); + + len += escape_errorlog_item(errstr + len, message, + sizeof(errstr) - len); + /* Truncate for the terminator (as apr_snprintf does) */ + if (len > sizeof(errstr) - sizeof(APR_EOL_STR)) { + len = sizeof(errstr) - sizeof(APR_EOL_STR); + } + strcpy(errstr + len, APR_EOL_STR); + len += strlen(APR_EOL_STR); + svn_error_clear(log_write(log_file, errstr, len, pool)); + + continuation = "-"; + err = err->child; + } +} + +/* Call log_error with log_file, remote_host, user, and repos + arguments from SERVER and CONN. */ +static void +log_server_error(svn_error_t *err, server_baton_t *server, + svn_ra_svn_conn_t *conn, apr_pool_t *pool) +{ + log_error(err, server->log_file, svn_ra_svn_conn_remote_host(conn), + server->user, server->repos_name, pool); +} + +/* svn_error_create() a new error, log_server_error() it, and + return it. */ +static svn_error_t * +error_create_and_log(apr_status_t apr_err, svn_error_t *child, + const char *message, server_baton_t *server, + svn_ra_svn_conn_t *conn, apr_pool_t *pool) +{ + svn_error_t *err = svn_error_create(apr_err, child, message); + log_server_error(err, server, conn, pool); + return err; +} + +/* Log a failure ERR, transmit ERR back to the client (as part of a + "failure" notification), consume ERR, and flush the connection. */ +static svn_error_t * +log_fail_and_flush(svn_error_t *err, server_baton_t *server, + svn_ra_svn_conn_t *conn, apr_pool_t *pool) +{ + svn_error_t *io_err; + + log_server_error(err, server, conn, pool); + io_err = svn_ra_svn_write_cmd_failure(conn, pool, err); + svn_error_clear(err); + SVN_ERR(io_err); + return svn_ra_svn_flush(conn, pool); +} + +/* Log a client command. */ +static svn_error_t *log_command(server_baton_t *b, + svn_ra_svn_conn_t *conn, + apr_pool_t *pool, + const char *fmt, ...) +{ + const char *remote_host, *timestr, *log, *line; + va_list ap; + apr_size_t nbytes; + + if (b->log_file == NULL) + return SVN_NO_ERROR; + + remote_host = svn_ra_svn_conn_remote_host(conn); + timestr = svn_time_to_cstring(apr_time_now(), pool); + + va_start(ap, fmt); + log = apr_pvsprintf(pool, fmt, ap); + va_end(ap); + + line = apr_psprintf(pool, "%" APR_PID_T_FMT + " %s %s %s %s %s" APR_EOL_STR, + getpid(), timestr, + (remote_host ? remote_host : "-"), + (b->user ? b->user : "-"), b->repos_name, log); + nbytes = strlen(line); + + return log_write(b->log_file, line, nbytes, pool); +} + +svn_error_t *load_configs(svn_config_t **cfg, + svn_config_t **pwdb, + svn_authz_t **authzdb, + enum username_case_type *username_case, + const char *filename, + svn_boolean_t must_exist, + const char *base, + server_baton_t *server, + svn_ra_svn_conn_t *conn, + apr_pool_t *pool) +{ + const char *pwdb_path, *authzdb_path; + svn_error_t *err; + + SVN_ERR(svn_config_read2(cfg, filename, must_exist, FALSE, pool)); + + svn_config_get(*cfg, &pwdb_path, SVN_CONFIG_SECTION_GENERAL, + SVN_CONFIG_OPTION_PASSWORD_DB, NULL); + + *pwdb = NULL; + if (pwdb_path) + { + pwdb_path = svn_dirent_canonicalize(pwdb_path, pool); + pwdb_path = svn_dirent_join(base, pwdb_path, pool); + + err = svn_config_read2(pwdb, pwdb_path, TRUE, FALSE, pool); + if (err) + { + if (server) + /* Called by listening server; log error no matter what it is. */ + log_server_error(err, server, conn, pool); + + /* Because it may be possible to read the pwdb file with some + access methods and not others, ignore errors reading the pwdb + file and just don't present password authentication as an + option. Also, some authentications (e.g. --tunnel) can + proceed without it anyway. + + ### Not entirely sure why SVN_ERR_BAD_FILENAME is checked + ### for here. That seems to have been introduced in r856914, + ### and only in r870942 was the APR_EACCES check introduced. */ + if (err->apr_err != SVN_ERR_BAD_FILENAME + && ! APR_STATUS_IS_EACCES(err->apr_err)) + { + if (server) + { + /* Called by listening server: Now that we've logged + * the error, clear it and return a nice, generic + * error to the user + * (http://subversion.tigris.org/issues/show_bug.cgi?id=2271). */ + svn_error_clear(err); + return svn_error_create(SVN_ERR_AUTHN_FAILED, NULL, NULL); + } + /* Called during startup; return the error, whereupon it + * will go to standard error for the admin to see. */ + return err; + } + else + /* Ignore SVN_ERR_BAD_FILENAME and APR_EACCES and proceed. */ + svn_error_clear(err); + } + } + + /* Read authz configuration. */ + svn_config_get(*cfg, &authzdb_path, SVN_CONFIG_SECTION_GENERAL, + SVN_CONFIG_OPTION_AUTHZ_DB, NULL); + if (authzdb_path) + { + const char *case_force_val; + + authzdb_path = svn_dirent_canonicalize(authzdb_path, pool); + authzdb_path = svn_dirent_join(base, authzdb_path, pool); + err = svn_repos_authz_read(authzdb, authzdb_path, TRUE, pool); + if (err) + { + if (server) + { + /* Called by listening server: Log the error, clear it, + * and return a nice, generic error to the user + * (http://subversion.tigris.org/issues/show_bug.cgi?id=2271). */ + log_server_error(err, server, conn, pool); + svn_error_clear(err); + return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, NULL); + } + else + /* Called during startup; return the error, whereupon it + * will go to standard error for the admin to see. */ + return err; + } + + /* Are we going to be case-normalizing usernames when we consult + * this authz file? */ + svn_config_get(*cfg, &case_force_val, SVN_CONFIG_SECTION_GENERAL, + SVN_CONFIG_OPTION_FORCE_USERNAME_CASE, NULL); + if (case_force_val) + { + if (strcmp(case_force_val, "upper") == 0) + *username_case = CASE_FORCE_UPPER; + else if (strcmp(case_force_val, "lower") == 0) + *username_case = CASE_FORCE_LOWER; + else + *username_case = CASE_ASIS; + } + } + else + { + *authzdb = NULL; + *username_case = CASE_ASIS; + } + + return SVN_NO_ERROR; +} + +/* Set *FS_PATH to the portion of URL that is the path within the + repository, if URL is inside REPOS_URL (if URL is not inside + REPOS_URL, then error, with the effect on *FS_PATH undefined). + + If the resultant fs path would be the empty string (i.e., URL and + REPOS_URL are the same), then set *FS_PATH to "/". + + Assume that REPOS_URL and URL are already URI-decoded. */ +static svn_error_t *get_fs_path(const char *repos_url, const char *url, + const char **fs_path) +{ + apr_size_t len; + + len = strlen(repos_url); + if (strncmp(url, repos_url, len) != 0) + return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, + "'%s' is not the same repository as '%s'", + url, repos_url); + *fs_path = url + len; + if (! **fs_path) + *fs_path = "/"; + + return SVN_NO_ERROR; +} + +/* --- AUTHENTICATION AND AUTHORIZATION FUNCTIONS --- */ + +/* Convert TEXT to upper case if TO_UPPERCASE is TRUE, else + converts it to lower case. */ +static void convert_case(char *text, svn_boolean_t to_uppercase) +{ + char *c = text; + while (*c) + { + *c = (char)(to_uppercase ? apr_toupper(*c) : apr_tolower(*c)); + ++c; + } +} + +/* Set *ALLOWED to TRUE if PATH is accessible in the REQUIRED mode to + the user described in BATON according to the authz rules in BATON. + Use POOL for temporary allocations only. If no authz rules are + present in BATON, grant access by default. */ +static svn_error_t *authz_check_access(svn_boolean_t *allowed, + const char *path, + svn_repos_authz_access_t required, + server_baton_t *b, + apr_pool_t *pool) +{ + /* If authz cannot be performed, grant access. This is NOT the same + as the default policy when authz is performed on a path with no + rules. In the latter case, the default is to deny access, and is + set by svn_repos_authz_check_access. */ + if (!b->authzdb) + { + *allowed = TRUE; + return SVN_NO_ERROR; + } + + /* If the authz request is for the empty path (ie. ""), replace it + with the root path. This happens because of stripping done at + various levels in svnserve that remove the leading / on an + absolute path. Passing such a malformed path to the authz + routines throws them into an infinite loop and makes them miss + ACLs. */ + if (path) + path = svn_fspath__canonicalize(path, pool); + + /* If we have a username, and we've not yet used it + any username + case normalization that might be requested to determine "the + username we used for authz purposes", do so now. */ + if (b->user && (! b->authz_user)) + { + char *authz_user = apr_pstrdup(b->pool, b->user); + if (b->username_case == CASE_FORCE_UPPER) + convert_case(authz_user, TRUE); + else if (b->username_case == CASE_FORCE_LOWER) + convert_case(authz_user, FALSE); + b->authz_user = authz_user; + } + + return svn_repos_authz_check_access(b->authzdb, b->authz_repos_name, + path, b->authz_user, required, + allowed, pool); +} + +/* Set *ALLOWED to TRUE if PATH is readable by the user described in + * BATON. Use POOL for temporary allocations only. ROOT is not used. + * Implements the svn_repos_authz_func_t interface. + */ +static svn_error_t *authz_check_access_cb(svn_boolean_t *allowed, + svn_fs_root_t *root, + const char *path, + void *baton, + apr_pool_t *pool) +{ + server_baton_t *sb = baton; + + return authz_check_access(allowed, path, svn_authz_read, sb, pool); +} + +/* If authz is enabled in the specified BATON, return a read authorization + function. Otherwise, return NULL. */ +static svn_repos_authz_func_t authz_check_access_cb_func(server_baton_t *baton) +{ + if (baton->authzdb) + return authz_check_access_cb; + return NULL; +} + +/* Set *ALLOWED to TRUE if the REQUIRED access to PATH is granted, + * according to the state in BATON. Use POOL for temporary + * allocations only. ROOT is not used. Implements the + * svn_repos_authz_callback_t interface. + */ +static svn_error_t *authz_commit_cb(svn_repos_authz_access_t required, + svn_boolean_t *allowed, + svn_fs_root_t *root, + const char *path, + void *baton, + apr_pool_t *pool) +{ + server_baton_t *sb = baton; + + return authz_check_access(allowed, path, required, sb, pool); +} + + +enum access_type get_access(server_baton_t *b, enum authn_type auth) +{ + const char *var = (auth == AUTHENTICATED) ? SVN_CONFIG_OPTION_AUTH_ACCESS : + SVN_CONFIG_OPTION_ANON_ACCESS; + const char *val, *def = (auth == AUTHENTICATED) ? "write" : "read"; + enum access_type result; + + svn_config_get(b->cfg, &val, SVN_CONFIG_SECTION_GENERAL, var, def); + result = (strcmp(val, "write") == 0 ? WRITE_ACCESS : + strcmp(val, "read") == 0 ? READ_ACCESS : NO_ACCESS); + return (result == WRITE_ACCESS && b->read_only) ? READ_ACCESS : result; +} + +static enum access_type current_access(server_baton_t *b) +{ + return get_access(b, (b->user) ? AUTHENTICATED : UNAUTHENTICATED); +} + +/* Send authentication mechs for ACCESS_TYPE to the client. If NEEDS_USERNAME + is true, don't send anonymous mech even if that would give the desired + access. */ +static svn_error_t *send_mechs(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + server_baton_t *b, enum access_type required, + svn_boolean_t needs_username) +{ + if (!needs_username && get_access(b, UNAUTHENTICATED) >= required) + SVN_ERR(svn_ra_svn_write_word(conn, pool, "ANONYMOUS")); + if (b->tunnel_user && get_access(b, AUTHENTICATED) >= required) + SVN_ERR(svn_ra_svn_write_word(conn, pool, "EXTERNAL")); + if (b->pwdb && get_access(b, AUTHENTICATED) >= required) + SVN_ERR(svn_ra_svn_write_word(conn, pool, "CRAM-MD5")); + return SVN_NO_ERROR; +} + +/* Context for cleanup handler. */ +struct cleanup_fs_access_baton +{ + svn_fs_t *fs; + apr_pool_t *pool; +}; + +/* Pool cleanup handler. Make sure fs's access_t points to NULL when + the command pool is destroyed. */ +static apr_status_t cleanup_fs_access(void *data) +{ + svn_error_t *serr; + struct cleanup_fs_access_baton *baton = data; + + serr = svn_fs_set_access(baton->fs, NULL); + if (serr) + { + apr_status_t apr_err = serr->apr_err; + svn_error_clear(serr); + return apr_err; + } + + return APR_SUCCESS; +} + + +/* Create an svn_fs_access_t in POOL for USER and associate it with + B's filesystem. Also, register a cleanup handler with POOL which + de-associates the svn_fs_access_t from B's filesystem. */ +static svn_error_t * +create_fs_access(server_baton_t *b, apr_pool_t *pool) +{ + svn_fs_access_t *fs_access; + struct cleanup_fs_access_baton *cleanup_baton; + + if (!b->user) + return SVN_NO_ERROR; + + SVN_ERR(svn_fs_create_access(&fs_access, b->user, pool)); + SVN_ERR(svn_fs_set_access(b->fs, fs_access)); + + cleanup_baton = apr_pcalloc(pool, sizeof(*cleanup_baton)); + cleanup_baton->pool = pool; + cleanup_baton->fs = b->fs; + apr_pool_cleanup_register(pool, cleanup_baton, cleanup_fs_access, + apr_pool_cleanup_null); + + return SVN_NO_ERROR; +} + +/* Authenticate, once the client has chosen a mechanism and possibly + * sent an initial mechanism token. On success, set *success to true + * and b->user to the authenticated username (or NULL for anonymous). + * On authentication failure, report failure to the client and set + * *success to FALSE. On communications failure, return an error. + * If NEEDS_USERNAME is TRUE, don't allow anonymous authentication. */ +static svn_error_t *auth(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + const char *mech, const char *mecharg, + server_baton_t *b, enum access_type required, + svn_boolean_t needs_username, + svn_boolean_t *success) +{ + const char *user; + *success = FALSE; + + if (get_access(b, AUTHENTICATED) >= required + && b->tunnel_user && strcmp(mech, "EXTERNAL") == 0) + { + if (*mecharg && strcmp(mecharg, b->tunnel_user) != 0) + return svn_ra_svn_write_tuple(conn, pool, "w(c)", "failure", + "Requested username does not match"); + b->user = b->tunnel_user; + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w()", "success")); + *success = TRUE; + return SVN_NO_ERROR; + } + + if (get_access(b, UNAUTHENTICATED) >= required + && strcmp(mech, "ANONYMOUS") == 0 && ! needs_username) + { + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w()", "success")); + *success = TRUE; + return SVN_NO_ERROR; + } + + if (get_access(b, AUTHENTICATED) >= required + && b->pwdb && strcmp(mech, "CRAM-MD5") == 0) + { + SVN_ERR(svn_ra_svn_cram_server(conn, pool, b->pwdb, &user, success)); + b->user = apr_pstrdup(b->pool, user); + return SVN_NO_ERROR; + } + + return svn_ra_svn_write_tuple(conn, pool, "w(c)", "failure", + "Must authenticate with listed mechanism"); +} + +/* Perform an authentication request using the built-in SASL implementation. */ +static svn_error_t * +internal_auth_request(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + server_baton_t *b, enum access_type required, + svn_boolean_t needs_username) +{ + svn_boolean_t success; + const char *mech, *mecharg; + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success")); + SVN_ERR(send_mechs(conn, pool, b, required, needs_username)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)c)", b->realm)); + do + { + SVN_ERR(svn_ra_svn_read_tuple(conn, pool, "w(?c)", &mech, &mecharg)); + if (!*mech) + break; + SVN_ERR(auth(conn, pool, mech, mecharg, b, required, needs_username, + &success)); + } + while (!success); + return SVN_NO_ERROR; +} + +/* Perform an authentication request in order to get an access level of + * REQUIRED or higher. Since the client may escape the authentication + * exchange, the caller should check current_access(b) to see if + * authentication succeeded. */ +static svn_error_t *auth_request(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + server_baton_t *b, enum access_type required, + svn_boolean_t needs_username) +{ +#ifdef SVN_HAVE_SASL + if (b->use_sasl) + return cyrus_auth_request(conn, pool, b, required, needs_username); +#endif + + return internal_auth_request(conn, pool, b, required, needs_username); +} + +/* Send a trivial auth notification on CONN which lists no mechanisms, + * indicating that authentication is unnecessary. Usually called in + * response to invocation of a svnserve command. + */ +static svn_error_t *trivial_auth_request(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, server_baton_t *b) +{ + return svn_ra_svn_write_cmd_response(conn, pool, "()c", ""); +} + +/* Ensure that the client has the REQUIRED access by checking the + * access directives (both blanket and per-directory) in BATON. If + * PATH is NULL, then only the blanket access configuration will + * impact the result. + * + * If NEEDS_USERNAME is TRUE, then a lookup is only successful if the + * user described in BATON is authenticated and, well, has a username + * assigned to him. + * + * Use POOL for temporary allocations only. + */ +static svn_boolean_t lookup_access(apr_pool_t *pool, + server_baton_t *baton, + svn_ra_svn_conn_t *conn, + svn_repos_authz_access_t required, + const char *path, + svn_boolean_t needs_username) +{ + enum access_type req = (required & svn_authz_write) ? + WRITE_ACCESS : READ_ACCESS; + svn_boolean_t authorized; + svn_error_t *err; + + /* Get authz's opinion on the access. */ + err = authz_check_access(&authorized, path, required, baton, pool); + + /* If an error made lookup fail, deny access. */ + if (err) + { + log_server_error(err, baton, conn, pool); + svn_error_clear(err); + return FALSE; + } + + /* If the required access is blanket-granted AND granted by authz + AND we already have a username if one is required, then the + lookup has succeeded. */ + if (current_access(baton) >= req + && authorized + && (! needs_username || baton->user)) + return TRUE; + + return FALSE; +} + +/* Check that the client has the REQUIRED access by consulting the + * authentication and authorization states stored in BATON. If the + * client does not have the required access credentials, attempt to + * authenticate the client to get that access, using CONN for + * communication. + * + * This function is supposed to be called to handle the authentication + * half of a standard svn protocol reply. If an error is returned, it + * probably means that the server can terminate the client connection + * with an apologetic error, as it implies an authentication failure. + * + * PATH and NEEDS_USERNAME are passed along to lookup_access, their + * behaviour is documented there. + */ +static svn_error_t *must_have_access(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, + server_baton_t *b, + svn_repos_authz_access_t required, + const char *path, + svn_boolean_t needs_username) +{ + enum access_type req = (required & svn_authz_write) ? + WRITE_ACCESS : READ_ACCESS; + + /* See whether the user already has the required access. If so, + nothing needs to be done. Create the FS access and send a + trivial auth request. */ + if (lookup_access(pool, b, conn, required, path, needs_username)) + { + SVN_ERR(create_fs_access(b, pool)); + return trivial_auth_request(conn, pool, b); + } + + /* If the required blanket access can be obtained by authenticating, + try that. Unfortunately, we can't tell until after + authentication whether authz will work or not. We force + requiring a username because we need one to be able to check + authz configuration again with a different user credentials than + the first time round. */ + if (b->user == NULL + && get_access(b, AUTHENTICATED) >= req + && (b->tunnel_user || b->pwdb || b->use_sasl)) + SVN_ERR(auth_request(conn, pool, b, req, TRUE)); + + /* Now that an authentication has been done get the new take of + authz on the request. */ + if (! lookup_access(pool, b, conn, required, path, needs_username)) + return svn_error_create(SVN_ERR_RA_SVN_CMD_ERR, + error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, + NULL, NULL, b, conn, pool), + NULL); + + /* Else, access is granted, and there is much rejoicing. */ + SVN_ERR(create_fs_access(b, pool)); + + return SVN_NO_ERROR; +} + +/* --- REPORTER COMMAND SET --- */ + +/* To allow for pipelining, reporter commands have no reponses. If we + * get an error, we ignore all subsequent reporter commands and return + * the error finish_report, to be handled by the calling command. + */ + +static svn_error_t *set_path(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + report_driver_baton_t *b = baton; + const char *path, *lock_token, *depth_word; + svn_revnum_t rev; + /* Default to infinity, for old clients that don't send depth. */ + svn_depth_t depth = svn_depth_infinity; + svn_boolean_t start_empty; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "crb?(?c)?w", + &path, &rev, &start_empty, &lock_token, + &depth_word)); + if (depth_word) + depth = svn_depth_from_word(depth_word); + path = svn_relpath_canonicalize(path, pool); + if (b->from_rev && strcmp(path, "") == 0) + *b->from_rev = rev; + if (!b->err) + b->err = svn_repos_set_path3(b->report_baton, path, rev, depth, + start_empty, lock_token, pool); + b->entry_counter++; + if (!start_empty) + b->only_empty_entries = FALSE; + return SVN_NO_ERROR; +} + +static svn_error_t *delete_path(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + report_driver_baton_t *b = baton; + const char *path; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &path)); + path = svn_relpath_canonicalize(path, pool); + if (!b->err) + b->err = svn_repos_delete_path(b->report_baton, path, pool); + return SVN_NO_ERROR; +} + +static svn_error_t *link_path(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + report_driver_baton_t *b = baton; + const char *path, *url, *lock_token, *fs_path, *depth_word; + svn_revnum_t rev; + svn_boolean_t start_empty; + /* Default to infinity, for old clients that don't send depth. */ + svn_depth_t depth = svn_depth_infinity; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "ccrb?(?c)?w", + &path, &url, &rev, &start_empty, + &lock_token, &depth_word)); + + /* ### WHAT?! The link path is an absolute URL?! Didn't see that + coming... -- cmpilato */ + path = svn_relpath_canonicalize(path, pool); + url = svn_uri_canonicalize(url, pool); + if (depth_word) + depth = svn_depth_from_word(depth_word); + if (!b->err) + b->err = get_fs_path(svn_path_uri_decode(b->repos_url, pool), + svn_path_uri_decode(url, pool), + &fs_path); + if (!b->err) + b->err = svn_repos_link_path3(b->report_baton, path, fs_path, rev, + depth, start_empty, lock_token, pool); + b->entry_counter++; + return SVN_NO_ERROR; +} + +static svn_error_t *finish_report(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + report_driver_baton_t *b = baton; + + /* No arguments to parse. */ + SVN_ERR(trivial_auth_request(conn, pool, b->sb)); + if (!b->err) + b->err = svn_repos_finish_report(b->report_baton, pool); + return SVN_NO_ERROR; +} + +static svn_error_t *abort_report(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + report_driver_baton_t *b = baton; + + /* No arguments to parse. */ + svn_error_clear(svn_repos_abort_report(b->report_baton, pool)); + return SVN_NO_ERROR; +} + +static const svn_ra_svn_cmd_entry_t report_commands[] = { + { "set-path", set_path }, + { "delete-path", delete_path }, + { "link-path", link_path }, + { "finish-report", finish_report, TRUE }, + { "abort-report", abort_report, TRUE }, + { NULL } +}; + +/* Accept a report from the client, drive the network editor with the + * result, and then write an empty command response. If there is a + * non-protocol failure, accept_report will abort the edit and return + * a command error to be reported by handle_commands(). + * + * If only_empty_entry is not NULL and the report contains only one + * item, and that item is empty, set *only_empty_entry to TRUE, else + * set it to FALSE. + * + * If from_rev is not NULL, set *from_rev to the revision number from + * the set-path on ""; if somehow set-path "" never happens, set + * *from_rev to SVN_INVALID_REVNUM. + */ +static svn_error_t *accept_report(svn_boolean_t *only_empty_entry, + svn_revnum_t *from_rev, + svn_ra_svn_conn_t *conn, apr_pool_t *pool, + server_baton_t *b, svn_revnum_t rev, + const char *target, const char *tgt_path, + svn_boolean_t text_deltas, + svn_depth_t depth, + svn_boolean_t send_copyfrom_args, + svn_boolean_t ignore_ancestry) +{ + const svn_delta_editor_t *editor; + void *edit_baton, *report_baton; + report_driver_baton_t rb; + svn_error_t *err; + + /* Make an svn_repos report baton. Tell it to drive the network editor + * when the report is complete. */ + svn_ra_svn_get_editor(&editor, &edit_baton, conn, pool, NULL, NULL); + SVN_CMD_ERR(svn_repos_begin_report2(&report_baton, rev, b->repos, + b->fs_path->data, target, tgt_path, + text_deltas, depth, ignore_ancestry, + send_copyfrom_args, + editor, edit_baton, + authz_check_access_cb_func(b), + b, pool)); + + rb.sb = b; + rb.repos_url = svn_path_uri_decode(b->repos_url, pool); + rb.report_baton = report_baton; + rb.err = NULL; + rb.entry_counter = 0; + rb.only_empty_entries = TRUE; + rb.from_rev = from_rev; + if (from_rev) + *from_rev = SVN_INVALID_REVNUM; + err = svn_ra_svn_handle_commands2(conn, pool, report_commands, &rb, TRUE); + if (err) + { + /* Network or protocol error while handling commands. */ + svn_error_clear(rb.err); + return err; + } + else if (rb.err) + { + /* Some failure during the reporting or editing operations. */ + SVN_CMD_ERR(rb.err); + } + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + if (only_empty_entry) + *only_empty_entry = rb.entry_counter == 1 && rb.only_empty_entries; + + return SVN_NO_ERROR; +} + +/* --- MAIN COMMAND SET --- */ + +/* Write out a list of property diffs. PROPDIFFS is an array of svn_prop_t + * values. */ +static svn_error_t *write_prop_diffs(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, + const apr_array_header_t *propdiffs) +{ + int i; + + for (i = 0; i < propdiffs->nelts; ++i) + { + const svn_prop_t *prop = &APR_ARRAY_IDX(propdiffs, i, svn_prop_t); + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "c(?s)", + prop->name, prop->value)); + } + + return SVN_NO_ERROR; +} + +/* Write out a lock to the client. */ +static svn_error_t *write_lock(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, + svn_lock_t *lock) +{ + const char *cdate, *edate; + + cdate = svn_time_to_cstring(lock->creation_date, pool); + edate = lock->expiration_date + ? svn_time_to_cstring(lock->expiration_date, pool) : NULL; + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "ccc(?c)c(?c)", lock->path, + lock->token, lock->owner, lock->comment, + cdate, edate)); + + return SVN_NO_ERROR; +} + +/* ### This really belongs in libsvn_repos. */ +/* Get the properties for a path, with hardcoded committed-info values. */ +static svn_error_t *get_props(apr_hash_t **props, svn_fs_root_t *root, + const char *path, apr_pool_t *pool) +{ + svn_string_t *str; + svn_revnum_t crev; + const char *cdate, *cauthor, *uuid; + + /* Get the properties. */ + SVN_ERR(svn_fs_node_proplist(props, root, path, pool)); + + /* Hardcode the values for the committed revision, date, and author. */ + SVN_ERR(svn_repos_get_committed_info(&crev, &cdate, &cauthor, root, + path, pool)); + str = svn_string_create(apr_psprintf(pool, "%ld", crev), + pool); + apr_hash_set(*props, SVN_PROP_ENTRY_COMMITTED_REV, APR_HASH_KEY_STRING, str); + str = (cdate) ? svn_string_create(cdate, pool) : NULL; + apr_hash_set(*props, SVN_PROP_ENTRY_COMMITTED_DATE, APR_HASH_KEY_STRING, + str); + str = (cauthor) ? svn_string_create(cauthor, pool) : NULL; + apr_hash_set(*props, SVN_PROP_ENTRY_LAST_AUTHOR, APR_HASH_KEY_STRING, str); + + /* Hardcode the values for the UUID. */ + SVN_ERR(svn_fs_get_uuid(svn_fs_root_fs(root), &uuid, pool)); + str = (uuid) ? svn_string_create(uuid, pool) : NULL; + apr_hash_set(*props, SVN_PROP_ENTRY_UUID, APR_HASH_KEY_STRING, str); + + return SVN_NO_ERROR; +} + +/* Set BATON->FS_PATH for the repository URL found in PARAMS. */ +static svn_error_t *reparent(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + const char *url; + const char *fs_path; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &url)); + url = svn_uri_canonicalize(url, pool); + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_CMD_ERR(get_fs_path(svn_path_uri_decode(b->repos_url, pool), + svn_path_uri_decode(url, pool), + &fs_path)); + SVN_ERR(log_command(b, conn, pool, "%s", svn_log__reparent(fs_path, pool))); + svn_stringbuf_set(b->fs_path, fs_path); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + return SVN_NO_ERROR; +} + +static svn_error_t *get_latest_rev(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + + SVN_ERR(log_command(b, conn, pool, "get-latest-rev")); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "r", rev)); + return SVN_NO_ERROR; +} + +static svn_error_t *get_dated_rev(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + apr_time_t tm; + const char *timestr; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", ×tr)); + SVN_ERR(log_command(b, conn, pool, "get-dated-rev %s", timestr)); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_CMD_ERR(svn_time_from_cstring(&tm, timestr, pool)); + SVN_CMD_ERR(svn_repos_dated_revision(&rev, b->repos, tm, pool)); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "r", rev)); + return SVN_NO_ERROR; +} + +/* Common logic for change_rev_prop() and change_rev_prop2(). */ +static svn_error_t *do_change_rev_prop(svn_ra_svn_conn_t *conn, + server_baton_t *b, + svn_revnum_t rev, + const char *name, + const svn_string_t *const *old_value_p, + const svn_string_t *value, + apr_pool_t *pool) +{ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, NULL, FALSE)); + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__change_rev_prop(rev, name, pool))); + SVN_CMD_ERR(svn_repos_fs_change_rev_prop4(b->repos, rev, b->user, + name, old_value_p, value, + TRUE, TRUE, + authz_check_access_cb_func(b), b, + pool)); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t *change_rev_prop2(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *name; + svn_string_t *value; + const svn_string_t *const *old_value_p; + svn_string_t *old_value; + svn_boolean_t dont_care; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rc(?s)(b?s)", + &rev, &name, &value, + &dont_care, &old_value)); + + /* Argument parsing. */ + if (dont_care) + old_value_p = NULL; + else + old_value_p = (const svn_string_t *const *)&old_value; + + /* Input validation. */ + if (dont_care && old_value) + { + svn_error_t *err; + err = svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, + "'previous-value' and 'dont-care' cannot both be " + "set in 'change-rev-prop2' request"); + return log_fail_and_flush(err, b, conn, pool); + } + + /* Do it. */ + SVN_ERR(do_change_rev_prop(conn, b, rev, name, old_value_p, value, pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t *change_rev_prop(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *name; + svn_string_t *value; + + /* Because the revprop value was at one time mandatory, the usual + optional element pattern "(?s)" isn't used. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rc?s", &rev, &name, &value)); + + SVN_ERR(do_change_rev_prop(conn, b, rev, name, NULL, value, pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t *rev_proplist(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + apr_hash_t *props; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "r", &rev)); + SVN_ERR(log_command(b, conn, pool, "%s", svn_log__rev_proplist(rev, pool))); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_CMD_ERR(svn_repos_fs_revision_proplist(&props, b->repos, rev, + authz_check_access_cb_func(b), b, + pool)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success")); + SVN_ERR(svn_ra_svn_write_proplist(conn, pool, props)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))")); + return SVN_NO_ERROR; +} + +static svn_error_t *rev_prop(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *name; + svn_string_t *value; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rc", &rev, &name)); + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__rev_prop(rev, name, pool))); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_CMD_ERR(svn_repos_fs_revision_prop(&value, b->repos, rev, name, + authz_check_access_cb_func(b), b, + pool)); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "(?s)", value)); + return SVN_NO_ERROR; +} + +static svn_error_t *commit_done(const svn_commit_info_t *commit_info, + void *baton, apr_pool_t *pool) +{ + commit_callback_baton_t *ccb = baton; + + *ccb->new_rev = commit_info->revision; + *ccb->date = commit_info->date + ? apr_pstrdup(ccb->pool, commit_info->date): NULL; + *ccb->author = commit_info->author + ? apr_pstrdup(ccb->pool, commit_info->author) : NULL; + *ccb->post_commit_err = commit_info->post_commit_err + ? apr_pstrdup(ccb->pool, commit_info->post_commit_err) : NULL; + return SVN_NO_ERROR; +} + +/* Add the LOCK_TOKENS (if any) to the filesystem access context, + * checking path authorizations using the state in SB as we go. + * LOCK_TOKENS is an array of svn_ra_svn_item_t structs. Return a + * client error if LOCK_TOKENS is not a list of lists. If a lock + * violates the authz configuration, return SVN_ERR_RA_NOT_AUTHORIZED + * to the client. Use POOL for temporary allocations only. + */ +static svn_error_t *add_lock_tokens(svn_ra_svn_conn_t *conn, + const apr_array_header_t *lock_tokens, + server_baton_t *sb, + apr_pool_t *pool) +{ + int i; + svn_fs_access_t *fs_access; + + SVN_ERR(svn_fs_get_access(&fs_access, sb->fs)); + + /* If there is no access context, nowhere to add the tokens. */ + if (! fs_access) + return SVN_NO_ERROR; + + for (i = 0; i < lock_tokens->nelts; ++i) + { + const char *path, *token, *full_path; + svn_ra_svn_item_t *path_item, *token_item; + svn_ra_svn_item_t *item = &APR_ARRAY_IDX(lock_tokens, i, + svn_ra_svn_item_t); + if (item->kind != SVN_RA_SVN_LIST) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + "Lock tokens aren't a list of lists"); + + path_item = &APR_ARRAY_IDX(item->u.list, 0, svn_ra_svn_item_t); + if (path_item->kind != SVN_RA_SVN_STRING) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + "Lock path isn't a string"); + + token_item = &APR_ARRAY_IDX(item->u.list, 1, svn_ra_svn_item_t); + if (token_item->kind != SVN_RA_SVN_STRING) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + "Lock token isn't a string"); + + path = path_item->u.string->data; + full_path = svn_fspath__join(sb->fs_path->data, + svn_relpath_canonicalize(path, pool), + pool); + + if (! lookup_access(pool, sb, conn, svn_authz_write, + full_path, TRUE)) + return error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, NULL, NULL, + sb, conn, pool); + + token = token_item->u.string->data; + SVN_ERR(svn_fs_access_add_lock_token2(fs_access, path, token)); + } + + return SVN_NO_ERROR; +} + +/* Unlock the paths with lock tokens in LOCK_TOKENS, ignoring any errors. + LOCK_TOKENS contains svn_ra_svn_item_t elements, assumed to be lists. */ +static svn_error_t *unlock_paths(const apr_array_header_t *lock_tokens, + server_baton_t *sb, + svn_ra_svn_conn_t *conn, + apr_pool_t *pool) +{ + int i; + apr_pool_t *iterpool; + + iterpool = svn_pool_create(pool); + + for (i = 0; i < lock_tokens->nelts; ++i) + { + svn_ra_svn_item_t *item, *path_item, *token_item; + const char *path, *token, *full_path; + svn_error_t *err; + svn_pool_clear(iterpool); + + item = &APR_ARRAY_IDX(lock_tokens, i, svn_ra_svn_item_t); + path_item = &APR_ARRAY_IDX(item->u.list, 0, svn_ra_svn_item_t); + token_item = &APR_ARRAY_IDX(item->u.list, 1, svn_ra_svn_item_t); + + path = path_item->u.string->data; + token = token_item->u.string->data; + + full_path = svn_fspath__join(sb->fs_path->data, + svn_relpath_canonicalize(path, iterpool), + iterpool); + + /* The lock may have become defunct after the commit, so ignore such + errors. */ + err = svn_repos_fs_unlock(sb->repos, full_path, token, + FALSE, iterpool); + log_server_error(err, sb, conn, iterpool); + svn_error_clear(err); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static svn_error_t *commit(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + const char *log_msg = NULL, + *date = NULL, + *author = NULL, + *post_commit_err = NULL; + apr_array_header_t *lock_tokens; + svn_boolean_t keep_locks; + apr_array_header_t *revprop_list = NULL; + apr_hash_t *revprop_table; + const svn_delta_editor_t *editor; + void *edit_baton; + svn_boolean_t aborted; + commit_callback_baton_t ccb; + svn_revnum_t new_rev; + + if (params->nelts == 1) + { + /* Clients before 1.2 don't send lock-tokens, keep-locks, + and rev-props fields. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &log_msg)); + lock_tokens = NULL; + keep_locks = TRUE; + revprop_list = NULL; + } + else + { + /* Clients before 1.5 don't send the rev-props field. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "clb?l", &log_msg, + &lock_tokens, &keep_locks, + &revprop_list)); + } + + /* The handling for locks is a little problematic, because the + protocol won't let us send several auth requests once one has + succeeded. So we request write access and a username before + adding tokens (if we have any), and subsequently fail if a lock + violates authz. */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, + NULL, + (lock_tokens && lock_tokens->nelts))); + + /* Authorize the lock tokens and give them to the FS if we got + any. */ + if (lock_tokens && lock_tokens->nelts) + SVN_CMD_ERR(add_lock_tokens(conn, lock_tokens, b, pool)); + + if (revprop_list) + SVN_ERR(svn_ra_svn_parse_proplist(revprop_list, pool, &revprop_table)); + else + { + revprop_table = apr_hash_make(pool); + apr_hash_set(revprop_table, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING, + svn_string_create(log_msg, pool)); + } + + /* Get author from the baton, making sure clients can't circumvent + the authentication via the revision props. */ + apr_hash_set(revprop_table, SVN_PROP_REVISION_AUTHOR, APR_HASH_KEY_STRING, + b->user ? svn_string_create(b->user, pool) : NULL); + + ccb.pool = pool; + ccb.new_rev = &new_rev; + ccb.date = &date; + ccb.author = &author; + ccb.post_commit_err = &post_commit_err; + /* ### Note that svn_repos_get_commit_editor5 actually wants a decoded URL. */ + SVN_CMD_ERR(svn_repos_get_commit_editor5 + (&editor, &edit_baton, b->repos, NULL, + svn_path_uri_decode(b->repos_url, pool), + b->fs_path->data, revprop_table, + commit_done, &ccb, + authz_commit_cb, baton, pool)); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + SVN_ERR(svn_ra_svn_drive_editor(conn, pool, editor, edit_baton, &aborted)); + if (!aborted) + { + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__commit(new_rev, pool))); + SVN_ERR(trivial_auth_request(conn, pool, b)); + + /* In tunnel mode, deltify before answering the client, because + answering may cause the client to terminate the connection + and thus kill the server. But otherwise, deltify after + answering the client, to avoid user-visible delay. */ + + if (b->tunnel) + SVN_ERR(svn_fs_deltify_revision(b->fs, new_rev, pool)); + + /* Unlock the paths. */ + if (! keep_locks && lock_tokens && lock_tokens->nelts) + SVN_ERR(unlock_paths(lock_tokens, b, conn, pool)); + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "r(?c)(?c)(?c)", + new_rev, date, author, post_commit_err)); + + if (! b->tunnel) + SVN_ERR(svn_fs_deltify_revision(b->fs, new_rev, pool)); + } + return SVN_NO_ERROR; +} + +static svn_error_t *get_file(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + const char *path, *full_path, *hex_digest; + svn_revnum_t rev; + svn_fs_root_t *root; + svn_stream_t *contents; + apr_hash_t *props = NULL; + svn_string_t write_str; + char buf[4096]; + apr_size_t len; + svn_boolean_t want_props, want_contents; + svn_checksum_t *checksum; + svn_error_t *err, *write_err; + + /* Parse arguments. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)bb", &path, &rev, + &want_props, &want_contents)); + + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + + /* Check authorizations */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_read, + full_path, FALSE)); + + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__get_file(full_path, rev, + want_contents, want_props, pool))); + + /* Fetch the properties and a stream for the contents. */ + SVN_CMD_ERR(svn_fs_revision_root(&root, b->fs, rev, pool)); + SVN_CMD_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, root, + full_path, TRUE, pool)); + hex_digest = svn_checksum_to_cstring_display(checksum, pool); + if (want_props) + SVN_CMD_ERR(get_props(&props, root, full_path, pool)); + if (want_contents) + SVN_CMD_ERR(svn_fs_file_contents(&contents, root, full_path, pool)); + + /* Send successful command response with revision and props. */ + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((?c)r(!", "success", + hex_digest, rev)); + SVN_ERR(svn_ra_svn_write_proplist(conn, pool, props)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))")); + + /* Now send the file's contents. */ + if (want_contents) + { + err = SVN_NO_ERROR; + while (1) + { + len = sizeof(buf); + err = svn_stream_read(contents, buf, &len); + if (err) + break; + if (len > 0) + { + write_str.data = buf; + write_str.len = len; + SVN_ERR(svn_ra_svn_write_string(conn, pool, &write_str)); + } + if (len < sizeof(buf)) + { + err = svn_stream_close(contents); + break; + } + } + write_err = svn_ra_svn_write_cstring(conn, pool, ""); + if (write_err) + { + svn_error_clear(err); + return write_err; + } + SVN_CMD_ERR(err); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + } + + return SVN_NO_ERROR; +} + +static svn_error_t *get_dir(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + const char *path, *full_path, *file_path, *cauthor, *cdate; + svn_revnum_t rev; + apr_hash_t *entries, *props = NULL, *file_props; + apr_hash_index_t *hi; + svn_fs_root_t *root; + apr_pool_t *subpool; + svn_boolean_t want_props, want_contents; + apr_uint64_t dirent_fields; + apr_array_header_t *dirent_fields_list = NULL; + svn_ra_svn_item_t *elt; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)bb?l", &path, &rev, + &want_props, &want_contents, + &dirent_fields_list)); + + if (! dirent_fields_list) + { + dirent_fields = SVN_DIRENT_ALL; + } + else + { + int i; + + dirent_fields = 0; + + for (i = 0; i < dirent_fields_list->nelts; ++i) + { + elt = &APR_ARRAY_IDX(dirent_fields_list, i, svn_ra_svn_item_t); + + if (elt->kind != SVN_RA_SVN_WORD) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + "Dirent field not a string"); + + if (strcmp(SVN_RA_SVN_DIRENT_KIND, elt->u.word) == 0) + dirent_fields |= SVN_DIRENT_KIND; + else if (strcmp(SVN_RA_SVN_DIRENT_SIZE, elt->u.word) == 0) + dirent_fields |= SVN_DIRENT_SIZE; + else if (strcmp(SVN_RA_SVN_DIRENT_HAS_PROPS, elt->u.word) == 0) + dirent_fields |= SVN_DIRENT_HAS_PROPS; + else if (strcmp(SVN_RA_SVN_DIRENT_CREATED_REV, elt->u.word) == 0) + dirent_fields |= SVN_DIRENT_CREATED_REV; + else if (strcmp(SVN_RA_SVN_DIRENT_TIME, elt->u.word) == 0) + dirent_fields |= SVN_DIRENT_TIME; + else if (strcmp(SVN_RA_SVN_DIRENT_LAST_AUTHOR, elt->u.word) == 0) + dirent_fields |= SVN_DIRENT_LAST_AUTHOR; + } + } + + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + + /* Check authorizations */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_read, + full_path, FALSE)); + + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__get_dir(full_path, rev, + want_contents, want_props, + dirent_fields, pool))); + + /* Fetch the root of the appropriate revision. */ + SVN_CMD_ERR(svn_fs_revision_root(&root, b->fs, rev, pool)); + + /* Fetch the directory properties if requested. */ + if (want_props) + SVN_CMD_ERR(get_props(&props, root, full_path, pool)); + + /* Fetch the directory entries if requested. */ + if (want_contents) + { + SVN_CMD_ERR(svn_fs_dir_entries(&entries, root, full_path, pool)); + + /* Transform the hash table's FS entries into dirents. This probably + * belongs in libsvn_repos. */ + subpool = svn_pool_create(pool); + for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) + { + const char *name = svn__apr_hash_index_key(hi); + svn_fs_dirent_t *fsent = svn__apr_hash_index_val(hi); + svn_dirent_t *entry; + + svn_pool_clear(subpool); + + file_path = svn_fspath__join(full_path, name, subpool); + + if (! lookup_access(subpool, b, conn, svn_authz_read, + file_path, FALSE)) + { + apr_hash_set(entries, name, APR_HASH_KEY_STRING, NULL); + continue; + } + + entry = apr_pcalloc(pool, sizeof(*entry)); + + if (dirent_fields & SVN_DIRENT_KIND) + { + /* kind */ + entry->kind = fsent->kind; + } + + if (dirent_fields & SVN_DIRENT_SIZE) + { + /* size */ + if (entry->kind == svn_node_dir) + entry->size = 0; + else + SVN_CMD_ERR(svn_fs_file_length(&entry->size, root, file_path, + subpool)); + } + + if (dirent_fields & SVN_DIRENT_HAS_PROPS) + { + /* has_props */ + SVN_CMD_ERR(svn_fs_node_proplist(&file_props, root, file_path, + subpool)); + entry->has_props = (apr_hash_count(file_props) > 0); + } + + if ((dirent_fields & SVN_DIRENT_LAST_AUTHOR) + || (dirent_fields & SVN_DIRENT_TIME) + || (dirent_fields & SVN_DIRENT_CREATED_REV)) + { + /* created_rev, last_author, time */ + SVN_CMD_ERR(svn_repos_get_committed_info(&entry->created_rev, + &cdate, + &cauthor, root, + file_path, + subpool)); + entry->last_author = apr_pstrdup(pool, cauthor); + if (cdate) + SVN_CMD_ERR(svn_time_from_cstring(&entry->time, cdate, + subpool)); + else + entry->time = (time_t) -1; + } + + /* Store the entry. */ + apr_hash_set(entries, name, APR_HASH_KEY_STRING, entry); + } + svn_pool_destroy(subpool); + } + + /* Write out response. */ + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(r(!", "success", rev)); + SVN_ERR(svn_ra_svn_write_proplist(conn, pool, props)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)(!")); + if (want_contents) + { + const char *missing_date = svn_time_to_cstring(0, pool); + for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi)) + { + const char *name = svn__apr_hash_index_key(hi); + svn_dirent_t *entry = svn__apr_hash_index_val(hi); + + /* The client does not properly handle a missing CDATE. For + interoperability purposes, we must fill in some junk. + + See libsvn_ra_svn/client.c:ra_svn_get_dir() */ + cdate = (entry->time == (time_t) -1) + ? missing_date + : svn_time_to_cstring(entry->time, pool); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "cwnbr(?c)(?c)", name, + svn_node_kind_to_word(entry->kind), + (apr_uint64_t) entry->size, + entry->has_props, entry->created_rev, + cdate, entry->last_author)); + } + } + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))")); + return SVN_NO_ERROR; +} + +static svn_error_t *update(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *target, *full_path, *depth_word; + svn_boolean_t recurse; + svn_boolean_t send_copyfrom_args; + apr_uint64_t send_copyfrom_param; + /* Default to unknown. Old clients won't send depth, but we'll + handle that by converting recurse if necessary. */ + svn_depth_t depth = svn_depth_unknown; + svn_boolean_t is_checkout; + + /* Parse the arguments. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?r)cb?wB", &rev, &target, + &recurse, &depth_word, &send_copyfrom_param)); + target = svn_relpath_canonicalize(target, pool); + + if (depth_word) + depth = svn_depth_from_word(depth_word); + else + depth = SVN_DEPTH_INFINITY_OR_FILES(recurse); + + send_copyfrom_args = (send_copyfrom_param == SVN_RA_SVN_UNSPECIFIED_NUMBER) ? + FALSE : (svn_boolean_t) send_copyfrom_param; + + full_path = svn_fspath__join(b->fs_path->data, target, pool); + /* Check authorization and authenticate the user if necessary. */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_read, full_path, FALSE)); + + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + + SVN_ERR(accept_report(&is_checkout, NULL, + conn, pool, b, rev, target, NULL, TRUE, + depth, send_copyfrom_args, FALSE)); + if (is_checkout) + { + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__checkout(full_path, rev, + depth, pool))); + } + else + { + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__update(full_path, rev, depth, + send_copyfrom_args, pool))); + } + + return SVN_NO_ERROR; +} + +static svn_error_t *switch_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *target, *depth_word; + const char *switch_url, *switch_path; + svn_boolean_t recurse; + /* Default to unknown. Old clients won't send depth, but we'll + handle that by converting recurse if necessary. */ + svn_depth_t depth = svn_depth_unknown; + + /* Parse the arguments. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?r)cbc?w", &rev, &target, + &recurse, &switch_url, &depth_word)); + target = svn_relpath_canonicalize(target, pool); + switch_url = svn_uri_canonicalize(switch_url, pool); + + if (depth_word) + depth = svn_depth_from_word(depth_word); + else + depth = SVN_DEPTH_INFINITY_OR_FILES(recurse); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + + SVN_CMD_ERR(get_fs_path(svn_path_uri_decode(b->repos_url, pool), + svn_path_uri_decode(switch_url, pool), + &switch_path)); + + { + const char *full_path = svn_fspath__join(b->fs_path->data, target, pool); + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__switch(full_path, switch_path, rev, + depth, pool))); + } + + return accept_report(NULL, NULL, + conn, pool, b, rev, target, switch_path, TRUE, + depth, + FALSE /* TODO(sussman): no copyfrom args for now */, + TRUE); +} + +static svn_error_t *status(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *target, *depth_word; + svn_boolean_t recurse; + /* Default to unknown. Old clients won't send depth, but we'll + handle that by converting recurse if necessary. */ + svn_depth_t depth = svn_depth_unknown; + + /* Parse the arguments. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "cb?(?r)?w", + &target, &recurse, &rev, &depth_word)); + target = svn_relpath_canonicalize(target, pool); + + if (depth_word) + depth = svn_depth_from_word(depth_word); + else + depth = SVN_DEPTH_INFINITY_OR_EMPTY(recurse); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + + { + const char *full_path = svn_fspath__join(b->fs_path->data, target, pool); + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__status(full_path, rev, depth, pool))); + } + + return accept_report(NULL, NULL, conn, pool, b, rev, target, NULL, FALSE, + depth, FALSE, FALSE); +} + +static svn_error_t *diff(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *target, *versus_url, *versus_path, *depth_word; + svn_boolean_t recurse, ignore_ancestry; + svn_boolean_t text_deltas; + /* Default to unknown. Old clients won't send depth, but we'll + handle that by converting recurse if necessary. */ + svn_depth_t depth = svn_depth_unknown; + + /* Parse the arguments. */ + if (params->nelts == 5) + { + /* Clients before 1.4 don't send the text_deltas boolean or depth. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?r)cbbc", &rev, &target, + &recurse, &ignore_ancestry, &versus_url)); + text_deltas = TRUE; + depth_word = NULL; + } + else + { + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?r)cbbcb?w", + &rev, &target, &recurse, + &ignore_ancestry, &versus_url, + &text_deltas, &depth_word)); + } + target = svn_relpath_canonicalize(target, pool); + versus_url = svn_uri_canonicalize(versus_url, pool); + + if (depth_word) + depth = svn_depth_from_word(depth_word); + else + depth = SVN_DEPTH_INFINITY_OR_FILES(recurse); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + SVN_CMD_ERR(get_fs_path(svn_path_uri_decode(b->repos_url, pool), + svn_path_uri_decode(versus_url, pool), + &versus_path)); + + { + const char *full_path = svn_fspath__join(b->fs_path->data, target, pool); + svn_revnum_t from_rev; + SVN_ERR(accept_report(NULL, &from_rev, + conn, pool, b, rev, target, versus_path, + text_deltas, depth, FALSE, ignore_ancestry)); + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__diff(full_path, from_rev, versus_path, + rev, depth, ignore_ancestry, + pool))); + } + return SVN_NO_ERROR; +} + +/* Regardless of whether a client's capabilities indicate an + understanding of this command (by way of SVN_RA_SVN_CAP_MERGEINFO), + we provide a response. + + ASSUMPTION: When performing a 'merge' with two URLs at different + revisions, the client will call this command more than once. */ +static svn_error_t *get_mergeinfo(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + apr_array_header_t *paths, *canonical_paths; + svn_mergeinfo_catalog_t mergeinfo; + int i; + apr_hash_index_t *hi; + const char *inherit_word; + svn_mergeinfo_inheritance_t inherit; + svn_boolean_t include_descendants; + apr_pool_t *iterpool; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "l(?r)wb", &paths, &rev, + &inherit_word, &include_descendants)); + inherit = svn_inheritance_from_word(inherit_word); + + /* Canonicalize the paths which mergeinfo has been requested for. */ + canonical_paths = apr_array_make(pool, paths->nelts, sizeof(const char *)); + for (i = 0; i < paths->nelts; i++) + { + svn_ra_svn_item_t *item = &APR_ARRAY_IDX(paths, i, svn_ra_svn_item_t); + const char *full_path; + + if (item->kind != SVN_RA_SVN_STRING) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + _("Path is not a string")); + full_path = svn_relpath_canonicalize(item->u.string->data, pool); + full_path = svn_fspath__join(b->fs_path->data, full_path, pool); + APR_ARRAY_PUSH(canonical_paths, const char *) = full_path; + } + + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__get_mergeinfo(canonical_paths, inherit, + include_descendants, + pool))); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_CMD_ERR(svn_repos_fs_get_mergeinfo(&mergeinfo, b->repos, + canonical_paths, rev, + inherit, + include_descendants, + authz_check_access_cb_func(b), b, + pool)); + SVN_ERR(svn_mergeinfo__remove_prefix_from_catalog(&mergeinfo, mergeinfo, + b->fs_path->data, pool)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success")); + iterpool = svn_pool_create(pool); + for (hi = apr_hash_first(pool, mergeinfo); hi; hi = apr_hash_next(hi)) + { + const char *key = svn__apr_hash_index_key(hi); + svn_mergeinfo_t value = svn__apr_hash_index_val(hi); + svn_string_t *mergeinfo_string; + + svn_pool_clear(iterpool); + + SVN_ERR(svn_mergeinfo_to_string(&mergeinfo_string, value, iterpool)); + SVN_ERR(svn_ra_svn_write_tuple(conn, iterpool, "cs", key, + mergeinfo_string)); + } + svn_pool_destroy(iterpool); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))")); + + return SVN_NO_ERROR; +} + +/* Send a log entry to the client. */ +static svn_error_t *log_receiver(void *baton, + svn_log_entry_t *log_entry, + apr_pool_t *pool) +{ + log_baton_t *b = baton; + svn_ra_svn_conn_t *conn = b->conn; + apr_hash_index_t *h; + svn_boolean_t invalid_revnum = FALSE; + char action[2]; + const char *author, *date, *message; + apr_uint64_t revprop_count; + + if (log_entry->revision == SVN_INVALID_REVNUM) + { + /* If the stack depth is zero, we've seen the last revision, so don't + send it, just return. */ + if (b->stack_depth == 0) + return SVN_NO_ERROR; + + /* Because the svn protocol won't let us send an invalid revnum, we have + to fudge here and send an additional flag. */ + log_entry->revision = 0; + invalid_revnum = TRUE; + b->stack_depth--; + } + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "(!")); + if (log_entry->changed_paths2) + { + for (h = apr_hash_first(pool, log_entry->changed_paths2); h; + h = apr_hash_next(h)) + { + const char *path = svn__apr_hash_index_key(h); + svn_log_changed_path2_t *change = svn__apr_hash_index_val(h); + + action[0] = change->action; + action[1] = '\0'; + SVN_ERR(svn_ra_svn_write_tuple( + conn, pool, "cw(?cr)(cbb)", + path, + action, + change->copyfrom_path, + change->copyfrom_rev, + svn_node_kind_to_word(change->node_kind), + /* text_modified and props_modified are never unknown */ + change->text_modified == svn_tristate_true, + change->props_modified == svn_tristate_true)); + } + } + svn_compat_log_revprops_out(&author, &date, &message, log_entry->revprops); + svn_compat_log_revprops_clear(log_entry->revprops); + if (log_entry->revprops) + revprop_count = apr_hash_count(log_entry->revprops); + else + revprop_count = 0; + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)r(?c)(?c)(?c)bbn(!", + log_entry->revision, + author, date, message, + log_entry->has_children, + invalid_revnum, revprop_count)); + SVN_ERR(svn_ra_svn_write_proplist(conn, pool, log_entry->revprops)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)b", + log_entry->subtractive_merge)); + + if (log_entry->has_children) + b->stack_depth++; + + return SVN_NO_ERROR; +} + +static svn_error_t *log_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + svn_error_t *err, *write_err; + server_baton_t *b = baton; + svn_revnum_t start_rev, end_rev; + const char *full_path; + svn_boolean_t changed_paths, strict_node, include_merged_revisions; + apr_array_header_t *paths, *full_paths, *revprop_items, *revprops; + char *revprop_word; + svn_ra_svn_item_t *elt; + int i; + apr_uint64_t limit, include_merged_revs_param; + log_baton_t lb; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "l(?r)(?r)bb?n?Bwl", &paths, + &start_rev, &end_rev, &changed_paths, + &strict_node, &limit, + &include_merged_revs_param, + &revprop_word, &revprop_items)); + + if (include_merged_revs_param == SVN_RA_SVN_UNSPECIFIED_NUMBER) + include_merged_revisions = FALSE; + else + include_merged_revisions = (svn_boolean_t) include_merged_revs_param; + + if (revprop_word == NULL) + /* pre-1.5 client */ + revprops = svn_compat_log_revprops_in(pool); + else if (strcmp(revprop_word, "all-revprops") == 0) + revprops = NULL; + else if (strcmp(revprop_word, "revprops") == 0) + { + SVN_ERR_ASSERT(revprop_items); + + revprops = apr_array_make(pool, revprop_items->nelts, + sizeof(char *)); + for (i = 0; i < revprop_items->nelts; i++) + { + elt = &APR_ARRAY_IDX(revprop_items, i, svn_ra_svn_item_t); + if (elt->kind != SVN_RA_SVN_STRING) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + _("Log revprop entry not a string")); + APR_ARRAY_PUSH(revprops, const char *) = elt->u.string->data; + } + } + else + return svn_error_createf(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + _("Unknown revprop word '%s' in log command"), + revprop_word); + + /* If we got an unspecified number then the user didn't send us anything, + so we assume no limit. If it's larger than INT_MAX then someone is + messing with us, since we know the svn client libraries will never send + us anything that big, so play it safe and default to no limit. */ + if (limit == SVN_RA_SVN_UNSPECIFIED_NUMBER || limit > INT_MAX) + limit = 0; + + full_paths = apr_array_make(pool, paths->nelts, sizeof(const char *)); + for (i = 0; i < paths->nelts; i++) + { + elt = &APR_ARRAY_IDX(paths, i, svn_ra_svn_item_t); + if (elt->kind != SVN_RA_SVN_STRING) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + _("Log path entry not a string")); + full_path = svn_relpath_canonicalize(elt->u.string->data, pool), + full_path = svn_fspath__join(b->fs_path->data, full_path, pool); + APR_ARRAY_PUSH(full_paths, const char *) = full_path; + } + SVN_ERR(trivial_auth_request(conn, pool, b)); + + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__log(full_paths, start_rev, end_rev, + limit, changed_paths, strict_node, + include_merged_revisions, revprops, + pool))); + + /* Get logs. (Can't report errors back to the client at this point.) */ + lb.fs_path = b->fs_path->data; + lb.conn = conn; + lb.stack_depth = 0; + err = svn_repos_get_logs4(b->repos, full_paths, start_rev, end_rev, + (int) limit, changed_paths, strict_node, + include_merged_revisions, revprops, + authz_check_access_cb_func(b), b, log_receiver, + &lb, pool); + + write_err = svn_ra_svn_write_word(conn, pool, "done"); + if (write_err) + { + svn_error_clear(err); + return write_err; + } + SVN_CMD_ERR(err); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + return SVN_NO_ERROR; +} + +static svn_error_t *check_path(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *path, *full_path; + svn_fs_root_t *root; + svn_node_kind_t kind; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)", &path, &rev)); + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + + /* Check authorizations */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_read, + full_path, FALSE)); + + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + + SVN_ERR(log_command(b, conn, pool, "check-path %s@%d", + svn_path_uri_encode(full_path, pool), rev)); + + SVN_CMD_ERR(svn_fs_revision_root(&root, b->fs, rev, pool)); + SVN_CMD_ERR(svn_fs_check_path(&kind, root, full_path, pool)); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "w", + svn_node_kind_to_word(kind))); + return SVN_NO_ERROR; +} + +static svn_error_t *stat_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_revnum_t rev; + const char *path, *full_path, *cdate; + svn_fs_root_t *root; + svn_dirent_t *dirent; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)", &path, &rev)); + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + + /* Check authorizations */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_read, + full_path, FALSE)); + + if (!SVN_IS_VALID_REVNUM(rev)) + SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool)); + + SVN_ERR(log_command(b, conn, pool, "stat %s@%d", + svn_path_uri_encode(full_path, pool), rev)); + + SVN_CMD_ERR(svn_fs_revision_root(&root, b->fs, rev, pool)); + SVN_CMD_ERR(svn_repos_stat(&dirent, root, full_path, pool)); + + /* Need to return the equivalent of "(?l)", since that's what the + client is reading. */ + + if (dirent == NULL) + { + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "()")); + return SVN_NO_ERROR; + } + + cdate = (dirent->time == (time_t) -1) ? NULL + : svn_time_to_cstring(dirent->time, pool); + + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "((wnbr(?c)(?c)))", + svn_node_kind_to_word(dirent->kind), + (apr_uint64_t) dirent->size, + dirent->has_props, dirent->created_rev, + cdate, dirent->last_author)); + + return SVN_NO_ERROR; +} + +static svn_error_t *get_locations(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + svn_error_t *err, *write_err; + server_baton_t *b = baton; + svn_revnum_t revision; + apr_array_header_t *location_revisions, *loc_revs_proto; + svn_ra_svn_item_t *elt; + int i; + const char *relative_path; + svn_revnum_t peg_revision; + apr_hash_t *fs_locations; + const char *abs_path; + + /* Parse the arguments. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "crl", &relative_path, + &peg_revision, + &loc_revs_proto)); + relative_path = svn_relpath_canonicalize(relative_path, pool); + + abs_path = svn_fspath__join(b->fs_path->data, relative_path, pool); + + location_revisions = apr_array_make(pool, loc_revs_proto->nelts, + sizeof(svn_revnum_t)); + for (i = 0; i < loc_revs_proto->nelts; i++) + { + elt = &APR_ARRAY_IDX(loc_revs_proto, i, svn_ra_svn_item_t); + if (elt->kind != SVN_RA_SVN_NUMBER) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + "Get-locations location revisions entry " + "not a revision number"); + revision = (svn_revnum_t)(elt->u.number); + APR_ARRAY_PUSH(location_revisions, svn_revnum_t) = revision; + } + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__get_locations(abs_path, peg_revision, + location_revisions, pool))); + + /* All the parameters are fine - let's perform the query against the + * repository. */ + + /* We store both err and write_err here, so the client will get + * the "done" even if there was an error in fetching the results. */ + + err = svn_repos_trace_node_locations(b->fs, &fs_locations, abs_path, + peg_revision, location_revisions, + authz_check_access_cb_func(b), b, pool); + + /* Now, write the results to the connection. */ + if (!err) + { + if (fs_locations) + { + apr_hash_index_t *iter; + + for (iter = apr_hash_first(pool, fs_locations); iter; + iter = apr_hash_next(iter)) + { + const svn_revnum_t *iter_key = svn__apr_hash_index_key(iter); + const char *iter_value = svn__apr_hash_index_val(iter); + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "rc", + *iter_key, iter_value)); + } + } + } + + write_err = svn_ra_svn_write_word(conn, pool, "done"); + if (write_err) + { + svn_error_clear(err); + return write_err; + } + SVN_CMD_ERR(err); + + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t *gls_receiver(svn_location_segment_t *segment, + void *baton, + apr_pool_t *pool) +{ + svn_ra_svn_conn_t *conn = baton; + return svn_ra_svn_write_tuple(conn, pool, "rr(?c)", + segment->range_start, + segment->range_end, + segment->path); +} + +static svn_error_t *get_location_segments(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, + apr_array_header_t *params, + void *baton) +{ + svn_error_t *err, *write_err; + server_baton_t *b = baton; + svn_revnum_t peg_revision, start_rev, end_rev; + const char *relative_path; + const char *abs_path; + + /* Parse the arguments. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)(?r)(?r)", + &relative_path, &peg_revision, + &start_rev, &end_rev)); + relative_path = svn_relpath_canonicalize(relative_path, pool); + + abs_path = svn_fspath__join(b->fs_path->data, relative_path, pool); + + if (SVN_IS_VALID_REVNUM(start_rev) + && SVN_IS_VALID_REVNUM(end_rev) + && (end_rev > start_rev)) + { + err = svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, + "Get-location-segments end revision must not be " + "younger than start revision"); + return log_fail_and_flush(err, b, conn, pool); + } + + if (SVN_IS_VALID_REVNUM(peg_revision) + && SVN_IS_VALID_REVNUM(start_rev) + && (start_rev > peg_revision)) + { + err = svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, + "Get-location-segments start revision must not " + "be younger than peg revision"); + return log_fail_and_flush(err, b, conn, pool); + } + + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_ERR(log_command(baton, conn, pool, "%s", + svn_log__get_location_segments(abs_path, peg_revision, + start_rev, end_rev, + pool))); + + /* All the parameters are fine - let's perform the query against the + * repository. */ + + /* We store both err and write_err here, so the client will get + * the "done" even if there was an error in fetching the results. */ + + err = svn_repos_node_location_segments(b->repos, abs_path, + peg_revision, start_rev, end_rev, + gls_receiver, (void *)conn, + authz_check_access_cb_func(b), b, + pool); + write_err = svn_ra_svn_write_word(conn, pool, "done"); + if (write_err) + { + svn_error_clear(err); + return write_err; + } + SVN_CMD_ERR(err); + + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +/* This implements svn_write_fn_t. Write LEN bytes starting at DATA to the + client as a string. */ +static svn_error_t *svndiff_handler(void *baton, const char *data, + apr_size_t *len) +{ + file_revs_baton_t *b = baton; + svn_string_t str; + + str.data = data; + str.len = *len; + return svn_ra_svn_write_string(b->conn, b->pool, &str); +} + +/* This implements svn_close_fn_t. Mark the end of the data by writing an + empty string to the client. */ +static svn_error_t *svndiff_close_handler(void *baton) +{ + file_revs_baton_t *b = baton; + + SVN_ERR(svn_ra_svn_write_cstring(b->conn, b->pool, "")); + return SVN_NO_ERROR; +} + +/* This implements the svn_repos_file_rev_handler_t interface. */ +static svn_error_t *file_rev_handler(void *baton, const char *path, + svn_revnum_t rev, apr_hash_t *rev_props, + svn_boolean_t merged_revision, + svn_txdelta_window_handler_t *d_handler, + void **d_baton, + apr_array_header_t *prop_diffs, + apr_pool_t *pool) +{ + file_revs_baton_t *frb = baton; + svn_stream_t *stream; + + SVN_ERR(svn_ra_svn_write_tuple(frb->conn, pool, "cr(!", + path, rev)); + SVN_ERR(svn_ra_svn_write_proplist(frb->conn, pool, rev_props)); + SVN_ERR(svn_ra_svn_write_tuple(frb->conn, pool, "!)(!")); + SVN_ERR(write_prop_diffs(frb->conn, pool, prop_diffs)); + SVN_ERR(svn_ra_svn_write_tuple(frb->conn, pool, "!)b", merged_revision)); + + /* Store the pool for the delta stream. */ + frb->pool = pool; + + /* Prepare for the delta or just write an empty string. */ + if (d_handler) + { + stream = svn_stream_create(baton, pool); + svn_stream_set_write(stream, svndiff_handler); + svn_stream_set_close(stream, svndiff_close_handler); + + /* If the connection does not support SVNDIFF1 or if we don't want to use + * compression, use the non-compressing "version 0" implementation */ + if ( svn_ra_svn_compression_level(frb->conn) > 0 + && svn_ra_svn_has_capability(frb->conn, SVN_RA_SVN_CAP_SVNDIFF1)) + svn_txdelta_to_svndiff3(d_handler, d_baton, stream, 1, + svn_ra_svn_compression_level(frb->conn), pool); + else + svn_txdelta_to_svndiff3(d_handler, d_baton, stream, 0, + svn_ra_svn_compression_level(frb->conn), pool); + } + else + SVN_ERR(svn_ra_svn_write_cstring(frb->conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t *get_file_revs(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_error_t *err, *write_err; + file_revs_baton_t frb; + svn_revnum_t start_rev, end_rev; + const char *path; + const char *full_path; + apr_uint64_t include_merged_revs_param; + svn_boolean_t include_merged_revisions; + + /* Parse arguments. */ + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)(?r)?B", + &path, &start_rev, &end_rev, + &include_merged_revs_param)); + path = svn_relpath_canonicalize(path, pool); + SVN_ERR(trivial_auth_request(conn, pool, b)); + full_path = svn_fspath__join(b->fs_path->data, path, pool); + + if (include_merged_revs_param == SVN_RA_SVN_UNSPECIFIED_NUMBER) + include_merged_revisions = FALSE; + else + include_merged_revisions = (svn_boolean_t) include_merged_revs_param; + + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__get_file_revs(full_path, start_rev, end_rev, + include_merged_revisions, + pool))); + + frb.conn = conn; + frb.pool = NULL; + + err = svn_repos_get_file_revs2(b->repos, full_path, start_rev, end_rev, + include_merged_revisions, + authz_check_access_cb_func(b), b, + file_rev_handler, &frb, pool); + write_err = svn_ra_svn_write_word(conn, pool, "done"); + if (write_err) + { + svn_error_clear(err); + return write_err; + } + SVN_CMD_ERR(err); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t *lock(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + const char *path; + const char *comment; + const char *full_path; + svn_boolean_t steal_lock; + svn_revnum_t current_rev; + svn_lock_t *l; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?c)b(?r)", &path, &comment, + &steal_lock, ¤t_rev)); + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + + SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, + full_path, TRUE)); + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__lock_one_path(full_path, steal_lock, pool))); + + SVN_CMD_ERR(svn_repos_fs_lock(&l, b->repos, full_path, NULL, comment, 0, + 0, /* No expiration time. */ + current_rev, steal_lock, pool)); + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(!", "success")); + SVN_ERR(write_lock(conn, pool, l)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)")); + + return SVN_NO_ERROR; +} + +static svn_error_t *lock_many(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + apr_array_header_t *path_revs; + const char *comment; + svn_boolean_t steal_lock; + int i; + apr_pool_t *subpool; + const char *path; + const char *full_path; + svn_revnum_t current_rev; + apr_array_header_t *log_paths; + svn_lock_t *l; + svn_error_t *err = SVN_NO_ERROR, *write_err; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?c)bl", &comment, &steal_lock, + &path_revs)); + + subpool = svn_pool_create(pool); + + /* Because we can only send a single auth reply per request, we send + a reply before parsing the lock commands. This means an authz + access denial will abort the processing of the locks and return + an error. */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, NULL, TRUE)); + + /* Loop through the lock requests. */ + log_paths = apr_array_make(pool, path_revs->nelts, sizeof(full_path)); + for (i = 0; i < path_revs->nelts; ++i) + { + svn_ra_svn_item_t *item = &APR_ARRAY_IDX(path_revs, i, + svn_ra_svn_item_t); + + svn_pool_clear(subpool); + + if (item->kind != SVN_RA_SVN_LIST) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + "Lock requests should be list of lists"); + + SVN_ERR(svn_ra_svn_parse_tuple(item->u.list, pool, "c(?r)", &path, + ¤t_rev)); + + /* Allocate the full_path out of pool so it will survive for use + * by operational logging, after this loop. */ + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, subpool), + pool); + APR_ARRAY_PUSH(log_paths, const char *) = full_path; + + if (! lookup_access(pool, b, conn, svn_authz_write, full_path, TRUE)) + { + err = error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, NULL, NULL, + b, conn, pool); + break; + } + + err = svn_repos_fs_lock(&l, b->repos, full_path, + NULL, comment, FALSE, + 0, /* No expiration time. */ + current_rev, + steal_lock, subpool); + + if (err) + { + if (SVN_ERR_IS_LOCK_ERROR(err)) + { + write_err = svn_ra_svn_write_cmd_failure(conn, pool, err); + svn_error_clear(err); + err = NULL; + SVN_ERR(write_err); + } + else + break; + } + else + { + SVN_ERR(svn_ra_svn_write_tuple(conn, subpool, "w!", "success")); + SVN_ERR(write_lock(conn, subpool, l)); + SVN_ERR(svn_ra_svn_write_tuple(conn, subpool, "!")); + } + } + + svn_pool_destroy(subpool); + + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__lock(log_paths, steal_lock, pool))); + + /* NOTE: err might contain a fatal locking error from the loop above. */ + write_err = svn_ra_svn_write_word(conn, pool, "done"); + if (!write_err) + SVN_CMD_ERR(err); + svn_error_clear(err); + SVN_ERR(write_err); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t *unlock(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + const char *path, *token, *full_path; + svn_boolean_t break_lock; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?c)b", &path, &token, + &break_lock)); + + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + + /* Username required unless break_lock was specified. */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, + full_path, ! break_lock)); + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__unlock_one_path(full_path, break_lock, pool))); + + SVN_CMD_ERR(svn_repos_fs_unlock(b->repos, full_path, token, break_lock, + pool)); + + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t *unlock_many(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + svn_boolean_t break_lock; + apr_array_header_t *unlock_tokens; + int i; + apr_pool_t *subpool; + const char *path; + const char *full_path; + apr_array_header_t *log_paths; + const char *token; + svn_error_t *err = SVN_NO_ERROR, *write_err; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "bl", &break_lock, + &unlock_tokens)); + + /* Username required unless break_lock was specified. */ + SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, NULL, ! break_lock)); + + subpool = svn_pool_create(pool); + + /* Loop through the unlock requests. */ + log_paths = apr_array_make(pool, unlock_tokens->nelts, sizeof(full_path)); + for (i = 0; i < unlock_tokens->nelts; i++) + { + svn_ra_svn_item_t *item = &APR_ARRAY_IDX(unlock_tokens, i, + svn_ra_svn_item_t); + + svn_pool_clear(subpool); + + if (item->kind != SVN_RA_SVN_LIST) + return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL, + "Unlock request should be a list of lists"); + + SVN_ERR(svn_ra_svn_parse_tuple(item->u.list, subpool, "c(?c)", &path, + &token)); + + /* Allocate the full_path out of pool so it will survive for use + * by operational logging, after this loop. */ + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, subpool), + pool); + APR_ARRAY_PUSH(log_paths, const char *) = full_path; + + if (! lookup_access(subpool, b, conn, svn_authz_write, full_path, + ! break_lock)) + return svn_error_create(SVN_ERR_RA_SVN_CMD_ERR, + error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, + NULL, NULL, + b, conn, pool), + NULL); + + err = svn_repos_fs_unlock(b->repos, full_path, token, break_lock, + subpool); + if (err) + { + if (SVN_ERR_IS_UNLOCK_ERROR(err)) + { + write_err = svn_ra_svn_write_cmd_failure(conn, pool, err); + svn_error_clear(err); + err = NULL; + SVN_ERR(write_err); + } + else + break; + } + else + SVN_ERR(svn_ra_svn_write_tuple(conn, subpool, "w(c)", "success", + path)); + } + + svn_pool_destroy(subpool); + + SVN_ERR(log_command(b, conn, pool, "%s", + svn_log__unlock(log_paths, break_lock, pool))); + + /* NOTE: err might contain a fatal unlocking error from the loop above. */ + write_err = svn_ra_svn_write_word(conn, pool, "done"); + if (! write_err) + SVN_CMD_ERR(err); + svn_error_clear(err); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t *get_lock(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + const char *path; + const char *full_path; + svn_lock_t *l; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &path)); + + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + + SVN_ERR(must_have_access(conn, pool, b, svn_authz_read, + full_path, FALSE)); + SVN_ERR(log_command(b, conn, pool, "get-lock %s", + svn_path_uri_encode(full_path, pool))); + + SVN_CMD_ERR(svn_fs_get_lock(&l, b->fs, full_path, pool)); + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success")); + if (l) + SVN_ERR(write_lock(conn, pool, l)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))")); + + return SVN_NO_ERROR; +} + +static svn_error_t *get_locks(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + server_baton_t *b = baton; + const char *path; + const char *full_path; + const char *depth_word; + svn_depth_t depth; + apr_hash_t *locks; + apr_hash_index_t *hi; + svn_error_t *err; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c?(?w)", &path, &depth_word)); + + depth = depth_word ? svn_depth_from_word(depth_word) : svn_depth_infinity; + if ((depth != svn_depth_empty) && + (depth != svn_depth_files) && + (depth != svn_depth_immediates) && + (depth != svn_depth_infinity)) + { + err = svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL, + "Invalid 'depth' specified in get-locks request"); + return log_fail_and_flush(err, b, conn, pool); + } + + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + + SVN_ERR(log_command(b, conn, pool, "get-locks %s", + svn_path_uri_encode(full_path, pool))); + SVN_CMD_ERR(svn_repos_fs_get_locks2(&locks, b->repos, full_path, depth, + authz_check_access_cb_func(b), b, pool)); + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success")); + for (hi = apr_hash_first(pool, locks); hi; hi = apr_hash_next(hi)) + { + svn_lock_t *l = svn__apr_hash_index_val(hi); + + SVN_ERR(write_lock(conn, pool, l)); + } + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))")); + + return SVN_NO_ERROR; +} + +static svn_error_t *replay_one_revision(svn_ra_svn_conn_t *conn, + server_baton_t *b, + svn_revnum_t rev, + svn_revnum_t low_water_mark, + svn_boolean_t send_deltas, + apr_pool_t *pool) +{ + const svn_delta_editor_t *editor; + void *edit_baton; + svn_fs_root_t *root; + svn_error_t *err; + + SVN_ERR(log_command(b, conn, pool, + svn_log__replay(b->fs_path->data, low_water_mark, + pool))); + + svn_ra_svn_get_editor(&editor, &edit_baton, conn, pool, NULL, NULL); + + err = svn_fs_revision_root(&root, b->fs, rev, pool); + + if (! err) + err = svn_repos_replay2(root, b->fs_path->data, low_water_mark, + send_deltas, editor, edit_baton, + authz_check_access_cb_func(b), b, pool); + + if (err) + svn_error_clear(editor->abort_edit(edit_baton, pool)); + SVN_CMD_ERR(err); + + return svn_ra_svn_write_cmd(conn, pool, "finish-replay", ""); +} + +static svn_error_t *replay(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + svn_revnum_t rev, low_water_mark; + svn_boolean_t send_deltas; + server_baton_t *b = baton; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rrb", &rev, &low_water_mark, + &send_deltas)); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + + SVN_ERR(replay_one_revision(conn, b, rev, low_water_mark, + send_deltas, pool)); + + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t *replay_range(svn_ra_svn_conn_t *conn, apr_pool_t *pool, + apr_array_header_t *params, void *baton) +{ + svn_revnum_t start_rev, end_rev, rev, low_water_mark; + svn_boolean_t send_deltas; + server_baton_t *b = baton; + apr_pool_t *iterpool; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rrrb", &start_rev, + &end_rev, &low_water_mark, + &send_deltas)); + + SVN_ERR(trivial_auth_request(conn, pool, b)); + + iterpool = svn_pool_create(pool); + for (rev = start_rev; rev <= end_rev; rev++) + { + apr_hash_t *props; + + svn_pool_clear(iterpool); + + SVN_CMD_ERR(svn_repos_fs_revision_proplist(&props, b->repos, rev, + authz_check_access_cb_func(b), + b, + iterpool)); + SVN_ERR(svn_ra_svn_write_tuple(conn, iterpool, "w(!", "revprops")); + SVN_ERR(svn_ra_svn_write_proplist(conn, iterpool, props)); + SVN_ERR(svn_ra_svn_write_tuple(conn, iterpool, "!)")); + + SVN_ERR(replay_one_revision(conn, b, rev, low_water_mark, + send_deltas, iterpool)); + + } + svn_pool_destroy(iterpool); + + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "")); + + return SVN_NO_ERROR; +} + +static svn_error_t * +get_deleted_rev(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, + apr_array_header_t *params, + void *baton) +{ + server_baton_t *b = baton; + const char *path, *full_path; + svn_revnum_t peg_revision; + svn_revnum_t end_revision; + svn_revnum_t revision_deleted; + + SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "crr", + &path, &peg_revision, &end_revision)); + full_path = svn_fspath__join(b->fs_path->data, + svn_relpath_canonicalize(path, pool), pool); + SVN_ERR(log_command(b, conn, pool, "get-deleted-rev")); + SVN_ERR(trivial_auth_request(conn, pool, b)); + SVN_ERR(svn_repos_deleted_rev(b->fs, full_path, peg_revision, end_revision, + &revision_deleted, pool)); + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "r", revision_deleted)); + return SVN_NO_ERROR; +} + +static const svn_ra_svn_cmd_entry_t main_commands[] = { + { "reparent", reparent }, + { "get-latest-rev", get_latest_rev }, + { "get-dated-rev", get_dated_rev }, + { "change-rev-prop", change_rev_prop }, + { "change-rev-prop2",change_rev_prop2 }, + { "rev-proplist", rev_proplist }, + { "rev-prop", rev_prop }, + { "commit", commit }, + { "get-file", get_file }, + { "get-dir", get_dir }, + { "update", update }, + { "switch", switch_cmd }, + { "status", status }, + { "diff", diff }, + { "get-mergeinfo", get_mergeinfo }, + { "log", log_cmd }, + { "check-path", check_path }, + { "stat", stat_cmd }, + { "get-locations", get_locations }, + { "get-location-segments", get_location_segments }, + { "get-file-revs", get_file_revs }, + { "lock", lock }, + { "lock-many", lock_many }, + { "unlock", unlock }, + { "unlock-many", unlock_many }, + { "get-lock", get_lock }, + { "get-locks", get_locks }, + { "replay", replay }, + { "replay-range", replay_range }, + { "get-deleted-rev", get_deleted_rev }, + { NULL } +}; + +/* Skip past the scheme part of a URL, including the tunnel specification + * if present. Return NULL if the scheme part is invalid for ra_svn. */ +static const char *skip_scheme_part(const char *url) +{ + if (strncmp(url, "svn", 3) != 0) + return NULL; + url += 3; + if (*url == '+') + url += strcspn(url, ":"); + if (strncmp(url, "://", 3) != 0) + return NULL; + return url + 3; +} + +/* Check that PATH is a valid repository path, meaning it doesn't contain any + '..' path segments. + NOTE: This is similar to svn_path_is_backpath_present, but that function + assumes the path separator is '/'. This function also checks for + segments delimited by the local path separator. */ +static svn_boolean_t +repos_path_valid(const char *path) +{ + const char *s = path; + + while (*s) + { + /* Scan for the end of the segment. */ + while (*path && *path != '/' && *path != SVN_PATH_LOCAL_SEPARATOR) + ++path; + + /* Check for '..'. */ +#ifdef WIN32 + /* On Windows, don't allow sequences of more than one character + consisting of just dots and spaces. Win32 functions treat + paths such as ".. " and "......." inconsistently. Make sure + no one can escape out of the root. */ + if (path - s >= 2 && strspn(s, ". ") == path - s) + return FALSE; +#else /* ! WIN32 */ + if (path - s == 2 && s[0] == '.' && s[1] == '.') + return FALSE; +#endif + + /* Skip all separators. */ + while (*path && (*path == '/' || *path == SVN_PATH_LOCAL_SEPARATOR)) + ++path; + s = path; + } + + return TRUE; +} + +/* Look for the repository given by URL, using ROOT as the virtual + * repository root. If we find one, fill in the repos, fs, cfg, + * repos_url, and fs_path fields of B. Set B->repos's client + * capabilities to CAPABILITIES, which must be at least as long-lived + * as POOL, and whose elements are SVN_RA_CAPABILITY_*. + */ +static svn_error_t *find_repos(const char *url, const char *root, + server_baton_t *b, + svn_ra_svn_conn_t *conn, + const apr_array_header_t *capabilities, + apr_pool_t *pool) +{ + const char *path, *full_path, *repos_root, *fs_path; + svn_stringbuf_t *url_buf; + + /* Skip past the scheme and authority part. */ + path = skip_scheme_part(url); + if (path == NULL) + return svn_error_createf(SVN_ERR_BAD_URL, NULL, + "Non-svn URL passed to svn server: '%s'", url); + + + path = strchr(path, '/'); + path = (path == NULL) ? "" : svn_relpath_canonicalize(path, pool); + path = svn_path_uri_decode(path, pool); + + /* Ensure that it isn't possible to escape the root by disallowing + '..' segments. */ + if (!repos_path_valid(path)) + return svn_error_create(SVN_ERR_BAD_FILENAME, NULL, + "Couldn't determine repository path"); + + /* Join the server-configured root with the client path. */ + full_path = svn_dirent_join(svn_dirent_canonicalize(root, pool), + path, pool); + + /* Search for a repository in the full path. */ + repos_root = svn_repos_find_root_path(full_path, pool); + if (!repos_root) + return svn_error_createf(SVN_ERR_RA_SVN_REPOS_NOT_FOUND, NULL, + "No repository found in '%s'", url); + + /* Open the repository and fill in b with the resulting information. */ + SVN_ERR(svn_repos_open2(&b->repos, repos_root, b->fs_config, pool)); + SVN_ERR(svn_repos_remember_client_capabilities(b->repos, capabilities)); + b->fs = svn_repos_fs(b->repos); + fs_path = full_path + strlen(repos_root); + b->fs_path = svn_stringbuf_create(*fs_path ? fs_path : "/", pool); + url_buf = svn_stringbuf_create(url, pool); + svn_path_remove_components(url_buf, + svn_path_component_count(b->fs_path->data)); + b->repos_url = url_buf->data; + b->authz_repos_name = svn_dirent_is_child(root, repos_root, pool); + if (b->authz_repos_name == NULL) + b->repos_name = svn_dirent_basename(repos_root, pool); + else + b->repos_name = b->authz_repos_name; + b->repos_name = svn_path_uri_encode(b->repos_name, pool); + + /* If the svnserve configuration files have not been loaded then + load them from the repository. */ + if (NULL == b->cfg) + SVN_ERR(load_configs(&b->cfg, &b->pwdb, &b->authzdb, &b->username_case, + svn_repos_svnserve_conf(b->repos, pool), FALSE, + svn_repos_conf_dir(b->repos, pool), + b, conn, + pool)); + +#ifdef SVN_HAVE_SASL + /* Should we use Cyrus SASL? */ + svn_config_get_bool(b->cfg, &b->use_sasl, SVN_CONFIG_SECTION_SASL, + SVN_CONFIG_OPTION_USE_SASL, FALSE); +#endif + + /* Use the repository UUID as the default realm. */ + SVN_ERR(svn_fs_get_uuid(b->fs, &b->realm, pool)); + svn_config_get(b->cfg, &b->realm, SVN_CONFIG_SECTION_GENERAL, + SVN_CONFIG_OPTION_REALM, b->realm); + + /* Make sure it's possible for the client to authenticate. Note + that this doesn't take into account any authz configuration read + above, because we can't know about access it grants until paths + are given by the client. */ + if (get_access(b, UNAUTHENTICATED) == NO_ACCESS + && (get_access(b, AUTHENTICATED) == NO_ACCESS + || (!b->tunnel_user && !b->pwdb && !b->use_sasl))) + return error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + "No access allowed to this repository", + b, conn, pool); + + return SVN_NO_ERROR; +} + +/* Compute the authentication name EXTERNAL should be able to get, if any. */ +static const char *get_tunnel_user(serve_params_t *params, apr_pool_t *pool) +{ + /* Only offer EXTERNAL for connections tunneled over a login agent. */ + if (!params->tunnel) + return NULL; + + /* If a tunnel user was provided on the command line, use that. */ + if (params->tunnel_user) + return params->tunnel_user; + + return svn_user_get_name(pool); +} + +static void +fs_warning_func(void *baton, svn_error_t *err) +{ + fs_warning_baton_t *b = baton; + log_server_error(err, b->server, b->conn, b->pool); + /* TODO: Keep log_pool in the server baton, cleared after every log? */ + svn_pool_clear(b->pool); +} + +svn_error_t *serve(svn_ra_svn_conn_t *conn, serve_params_t *params, + apr_pool_t *pool) +{ + svn_error_t *err, *io_err; + apr_uint64_t ver; + const char *uuid, *client_url, *ra_client_string, *client_string; + apr_array_header_t *caplist, *cap_words; + server_baton_t b; + fs_warning_baton_t warn_baton; + svn_stringbuf_t *cap_log = svn_stringbuf_create("", pool); + + b.tunnel = params->tunnel; + b.tunnel_user = get_tunnel_user(params, pool); + b.read_only = params->read_only; + b.user = NULL; + b.username_case = params->username_case; + b.authz_user = NULL; + b.cfg = params->cfg; + b.pwdb = params->pwdb; + b.authzdb = params->authzdb; + b.realm = NULL; + b.log_file = params->log_file; + b.pool = pool; + b.use_sasl = FALSE; + + /* construct FS configuration parameters */ + b.fs_config = apr_hash_make(pool); + apr_hash_set(b.fs_config, SVN_FS_CONFIG_FSFS_CACHE_DELTAS, + APR_HASH_KEY_STRING, params->cache_txdeltas ? "1" : "0"); + apr_hash_set(b.fs_config, SVN_FS_CONFIG_FSFS_CACHE_FULLTEXTS, + APR_HASH_KEY_STRING, params->cache_fulltexts ? "1" : "0"); + + /* Send greeting. We don't support version 1 any more, so we can + * send an empty mechlist. */ + if (params->compression_level > 0) + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "nn()(wwwwwwww)", + (apr_uint64_t) 2, (apr_uint64_t) 2, + SVN_RA_SVN_CAP_EDIT_PIPELINE, + SVN_RA_SVN_CAP_SVNDIFF1, + SVN_RA_SVN_CAP_ABSENT_ENTRIES, + SVN_RA_SVN_CAP_COMMIT_REVPROPS, + SVN_RA_SVN_CAP_DEPTH, + SVN_RA_SVN_CAP_LOG_REVPROPS, + SVN_RA_SVN_CAP_ATOMIC_REVPROPS, + SVN_RA_SVN_CAP_PARTIAL_REPLAY)); + else + SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "nn()(wwwwwww)", + (apr_uint64_t) 2, (apr_uint64_t) 2, + SVN_RA_SVN_CAP_EDIT_PIPELINE, + SVN_RA_SVN_CAP_ABSENT_ENTRIES, + SVN_RA_SVN_CAP_COMMIT_REVPROPS, + SVN_RA_SVN_CAP_DEPTH, + SVN_RA_SVN_CAP_LOG_REVPROPS, + SVN_RA_SVN_CAP_ATOMIC_REVPROPS, + SVN_RA_SVN_CAP_PARTIAL_REPLAY)); + + /* Read client response, which we assume to be in version 2 format: + * version, capability list, and client URL; then we do an auth + * request. */ + SVN_ERR(svn_ra_svn_read_tuple(conn, pool, "nlc?c(?c)", + &ver, &caplist, &client_url, + &ra_client_string, + &client_string)); + if (ver != 2) + return SVN_NO_ERROR; + + client_url = svn_uri_canonicalize(client_url, pool); + SVN_ERR(svn_ra_svn_set_capabilities(conn, caplist)); + + /* All released versions of Subversion support edit-pipeline, + * so we do not accept connections from clients that do not. */ + if (! svn_ra_svn_has_capability(conn, SVN_RA_SVN_CAP_EDIT_PIPELINE)) + return SVN_NO_ERROR; + + /* find_repos needs the capabilities as a list of words (eventually + they get handed to the start-commit hook). While we could add a + new interface to re-retrieve them from conn and convert the + result to a list, it's simpler to just convert caplist by hand + here, since we already have it and turning 'svn_ra_svn_item_t's + into 'const char *'s is pretty easy. + + We only record capabilities we care about. The client may report + more (because it doesn't know what the server cares about). */ + { + int i; + svn_ra_svn_item_t *item; + + cap_words = apr_array_make(pool, 1, sizeof(const char *)); + for (i = 0; i < caplist->nelts; i++) + { + item = &APR_ARRAY_IDX(caplist, i, svn_ra_svn_item_t); + /* ra_svn_set_capabilities() already type-checked for us */ + if (strcmp(item->u.word, SVN_RA_SVN_CAP_MERGEINFO) == 0) + { + APR_ARRAY_PUSH(cap_words, const char *) + = SVN_RA_CAPABILITY_MERGEINFO; + } + /* Save for operational log. */ + if (cap_log->len > 0) + svn_stringbuf_appendcstr(cap_log, " "); + svn_stringbuf_appendcstr(cap_log, item->u.word); + } + } + + err = find_repos(client_url, params->root, &b, conn, cap_words, pool); + if (!err) + { + SVN_ERR(auth_request(conn, pool, &b, READ_ACCESS, FALSE)); + if (current_access(&b) == NO_ACCESS) + err = error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, NULL, + "Not authorized for access", + &b, conn, pool); + } + if (err) + { + log_error(err, b.log_file, svn_ra_svn_conn_remote_host(conn), + b.user, NULL, pool); + io_err = svn_ra_svn_write_cmd_failure(conn, pool, err); + svn_error_clear(err); + SVN_ERR(io_err); + return svn_ra_svn_flush(conn, pool); + } + + /* Log the open. */ + if (ra_client_string == NULL || ra_client_string[0] == '\0') + ra_client_string = "-"; + else + ra_client_string = svn_path_uri_encode(ra_client_string, pool); + if (client_string == NULL || client_string[0] == '\0') + client_string = "-"; + else + client_string = svn_path_uri_encode(client_string, pool); + SVN_ERR(log_command(&b, conn, pool, + "open %" APR_UINT64_T_FMT " cap=(%s) %s %s %s", + ver, cap_log->data, + svn_path_uri_encode(b.fs_path->data, pool), + ra_client_string, client_string)); + + warn_baton.server = &b; + warn_baton.conn = conn; + warn_baton.pool = svn_pool_create(pool); + svn_fs_set_warning_func(b.fs, fs_warning_func, &warn_baton); + + SVN_ERR(svn_fs_get_uuid(b.fs, &uuid, pool)); + + /* We can't claim mergeinfo capability until we know whether the + repository supports mergeinfo (i.e., is not a 1.4 repository), + but we don't get the repository url from the client until after + we've already sent the initial list of server capabilities. So + we list repository capabilities here, in our first response after + the client has sent the url. */ + { + svn_boolean_t supports_mergeinfo; + SVN_ERR(svn_repos_has_capability(b.repos, &supports_mergeinfo, + SVN_REPOS_CAPABILITY_MERGEINFO, pool)); + + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(cc(!", + "success", uuid, b.repos_url)); + if (supports_mergeinfo) + SVN_ERR(svn_ra_svn_write_word(conn, pool, SVN_RA_SVN_CAP_MERGEINFO)); + SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))")); + } + + return svn_ra_svn_handle_commands2(conn, pool, main_commands, &b, FALSE); +} diff --git a/subversion/svnserve/server.h b/subversion/svnserve/server.h new file mode 100644 index 0000000..41ab09c --- /dev/null +++ b/subversion/svnserve/server.h @@ -0,0 +1,187 @@ +/* + * svn_server.h : declarations for the svn server + * + * ==================================================================== + * 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. + * ==================================================================== + */ + + + +#ifndef SERVER_H +#define SERVER_H + +#include <apr_network_io.h> + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#include "svn_config.h" +#include "svn_repos.h" +#include "svn_ra_svn.h" + +enum username_case_type { CASE_FORCE_UPPER, CASE_FORCE_LOWER, CASE_ASIS }; + +typedef struct server_baton_t { + svn_repos_t *repos; + const char *repos_name; /* URI-encoded name of repository (not for authz) */ + svn_fs_t *fs; /* For convenience; same as svn_repos_fs(repos) */ + svn_config_t *cfg; /* Parsed repository svnserve.conf */ + svn_config_t *pwdb; /* Parsed password database */ + svn_authz_t *authzdb; /* Parsed authz rules */ + const char *authz_repos_name; /* The name of the repository for authz */ + const char *realm; /* Authentication realm */ + const char *repos_url; /* URL to base of repository */ + svn_stringbuf_t *fs_path;/* Decoded base in-repos path (w/ leading slash) */ + apr_hash_t *fs_config; /* Additional FS configuration parameters */ + const char *user; /* Authenticated username of the user */ + enum username_case_type username_case; /* Case-normalize the username? */ + const char *authz_user; /* Username for authz ('user' + 'username_case') */ + svn_boolean_t tunnel; /* Tunneled through login agent */ + const char *tunnel_user; /* Allow EXTERNAL to authenticate as this */ + svn_boolean_t read_only; /* Disallow write access (global flag) */ + svn_boolean_t use_sasl; /* Use Cyrus SASL for authentication; + always false if SVN_HAVE_SASL not defined */ + apr_file_t *log_file; /* Log filehandle. */ + apr_pool_t *pool; +} server_baton_t; + +enum authn_type { UNAUTHENTICATED, AUTHENTICATED }; +enum access_type { NO_ACCESS, READ_ACCESS, WRITE_ACCESS }; + +enum access_type get_access(server_baton_t *b, enum authn_type auth); + +typedef struct serve_params_t { + /* The virtual root of the repositories to serve. The client URL + path is interpreted relative to this root and is not allowed to + escape it. */ + const char *root; + + /* True if the connection is tunneled over an ssh-like transport, + such that the client may use EXTERNAL to authenticate as the + current uid's username. */ + svn_boolean_t tunnel; + + /* If tunnel is true, overrides the current uid's username as the + identity EXTERNAL authenticates as. */ + const char *tunnel_user; + + /* True if the read-only flag was specified on the command-line, + which forces all connections to be read-only. */ + svn_boolean_t read_only; + + /* A parsed repository svnserve configuration file, ala + svnserve.conf. If this is NULL, then no configuration file was + specified on the command line. If this is non-NULL, then + per-repository svnserve.conf are not read. */ + svn_config_t *cfg; + + /* A parsed repository password database. If this is NULL, then + either no svnserve configuration file was specified on the + command line, or it was specified and it did not refer to a + password database. */ + svn_config_t *pwdb; + + /* A parsed repository authorization database. If this is NULL, + then either no svnserve configuration file was specified on the + command line, or it was specified and it did not refer to a + authorization database. */ + svn_authz_t *authzdb; + + /* A filehandle open for writing logs to; possibly NULL. */ + apr_file_t *log_file; + + /* Username case normalization style. */ + enum username_case_type username_case; + + /* Enable text delta caching for all FSFS repositories. */ + svn_boolean_t cache_txdeltas; + + /* Enable full-text caching for all FSFS repositories. */ + svn_boolean_t cache_fulltexts; + + /* Size of the in-memory cache (used by FSFS only). */ + apr_uint64_t memory_cache_size; + + /* Data compression level to reduce for network traffic. If this + is 0, no compression should be applied and the protocol may + fall back to svndiff "version 0" bypassing zlib entirely. + Defaults to SVN_DELTA_COMPRESSION_LEVEL_DEFAULT. */ + int compression_level; + +} serve_params_t; + +/* Serve the connection CONN according to the parameters PARAMS. */ +svn_error_t *serve(svn_ra_svn_conn_t *conn, serve_params_t *params, + apr_pool_t *pool); + +/* Load a svnserve configuration file located at FILENAME into CFG, + and if such as found, then: + + - set *PWDB to any referenced password database, + - set *AUTHZDB to any referenced authorization database, and + - set *USERNAME_CASE to the enumerated value of the + 'force-username-case' configuration value (or its default). + + If MUST_EXIST is true and FILENAME does not exist, then return an + error. BASE may be specified as the base path to any referenced + password and authorization files found in FILENAME. + + If SERVER is not NULL, log the real errors with SERVER and CONN but + return generic errors to the client. CONN must not be NULL if SERVER + is not NULL. */ +svn_error_t *load_configs(svn_config_t **cfg, + svn_config_t **pwdb, + svn_authz_t **authzdb, + enum username_case_type *username_case, + const char *filename, + svn_boolean_t must_exist, + const char *base, + server_baton_t *server, + svn_ra_svn_conn_t *conn, + apr_pool_t *pool); + +/* Initialize the Cyrus SASL library. POOL is used for allocations. */ +svn_error_t *cyrus_init(apr_pool_t *pool); + +/* Authenticate using Cyrus SASL. */ +svn_error_t *cyrus_auth_request(svn_ra_svn_conn_t *conn, + apr_pool_t *pool, + server_baton_t *b, + enum access_type required, + svn_boolean_t needs_username); + +/* Escape SOURCE into DEST where SOURCE is null-terminated and DEST is + size BUFLEN DEST will be null-terminated. Returns number of bytes + written, including terminating null byte. */ +apr_size_t escape_errorlog_item(char *dest, const char *source, + apr_size_t buflen); + +/* Log ERR to LOG_FILE if LOG_FILE is not NULL. Include REMOTE_HOST, + USER, and REPOS in the log if they are not NULL. Allocate temporary + char buffers in POOL (which caller can then clear or dispose of). */ +void +log_error(svn_error_t *err, apr_file_t *log_file, const char *remote_host, + const char *user, const char *repos, apr_pool_t *pool); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SERVER_H */ diff --git a/subversion/svnserve/svnserve.8 b/subversion/svnserve/svnserve.8 new file mode 100644 index 0000000..66cb983 --- /dev/null +++ b/subversion/svnserve/svnserve.8 @@ -0,0 +1,138 @@ +.\" +.\" +.\" 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. +.\" +.\" +.\" You can view this file with: +.\" nroff -man [filename] +.\" +.TH svnserve 8 +.SH NAME +svnserve \- Server for the 'svn' repository access method +.SH SYNOPSIS +.TP +\fBsvnserve\fP [\fIoptions\fP] +.SH DESCRIPTION +\fBsvnserve\fP allows access to Subversion repositories using the svn +network protocol. It can both run as a standalone server process, or +it can run out of inetd. You must choose a mode of operation when you +start \fBsvnserve\fP. The following options are recognized: +.PP +.TP 5 +\fB\-d\fP, \fB\-\-daemon\fP +Causes \fBsvnserve\fP to run in daemon mode. \fBsvnserve\fP +backgrounds itself and accepts and serves TCP/IP connections on the +svn port (3690, by default). +.PP +.TP 5 +\fB\-\-listen\-port\fP=\fIport\fP +Causes \fBsvnserve\fP to listen on \fIport\fP when run in daemon mode. +.PP +.TP 5 +\fB\-\-listen\-host\fP=\fIhost\fP +Causes \fBsvnserve\fP to listen on the interface specified by \fIhost\fP, +which may be either a hostname or an IP address. +.PP +.TP 5 +\fB\-\-foreground\fP +When used together with \fB\-d\fP, this option causes \fBsvnserve\fP +to stay in the foreground. This option is mainly useful for +debugging. +.PP +.TP 5 +\fB\-i\fP, \fB\-\-inetd\fP +Causes \fBsvnserve\fP to use the stdin/stdout file descriptors, as is +appropriate for a daemon running out of inetd. +.PP +.TP 5 +\fB\-h\fP, \fB\-\-help\fP +Displays a usage summary and exits. +.PP +.TP 5 +\fB\-\-version\fP +Print \fBsvnserve\fP's version and the repository filesystem +back-end(s) a particular \fBsvnserve\fP supports. +.PP +.TP 5 +\fB\-r\fP \fIroot\fP, \fB\-\-root\fP=\fIroot\fP +Sets the virtual root for repositories served by \fBsvnserve\fP. The +pathname in URLs provided by the client will be interpreted relative +to this root, and will not be allowed to escape this root. +.PP +.TP 5 +\fB\-R\fP \fB\-\-read\-only\fP +Force all write operations through this \fBsvnserve\fP instance to be +forbidden, overriding all other access policy configuration. Do not +use this option to set general repository access policy - that is what +the \fBconf/svnserve.conf\fP repository configuration file is for. +This option should be used only to restrict access via a certain +method of invoking \fBsvnserve\fP - for example, to allow write access +via SSH, but not via a \fBsvnserve\fP daemon, or to create a +restricted SSH key which is only capable of read access. +.PP +.TP 5 +\fB\-t\fP, \fB\-\-tunnel\fP +Causes \fBsvnserve\fP to run in tunnel mode, which is just like the +inetd mode of operation (serve one connection over stdin/stdout) +except that the connection is considered to be pre-authenticated with +the username of the current uid. This flag is selected by the client +when running over a tunnel agent. +.PP +.TP 5 +\fB\-\-tunnel\-user\fP=\fIusername\fP +When combined with \fB\-\-tunnel\fP, overrides the pre-authenticated +username with the supplied \fIusername\fP. This is useful in +combination with the ssh authorized_key file's "command" directive to +allow a single system account to be used by multiple committers, each +having a distinct ssh identity. +.PP +.TP 5 +\fB\-T\fP, \fB\-\-threads\fP +When running in daemon mode, causes \fBsvnserve\fP to spawn a thread +instead of a process for each connection. The \fBsvnserve\fP process +still backgrounds itself at startup time. +.PP +.TP 5 +\fB\-\-config\-file\fP=\fIfilename\fP +When specified, \fBsvnserve\fP reads \fIfilename\fP once at program +startup and caches the \fBsvnserve\fP configuration and any passwords +and authorization configuration referenced from \fIfilename\fP. +\fBsvnserve\fP will not read any per-repository +\fBconf/svnserve.conf\fP files when this option is used. See the +\fBsvnserve.conf\fP(5) man page for details of the file format for +this option. +.PP +.TP 5 +\fB\-\-pid\-file\fP=\fIfilename\fP +When specified, \fBsvnserve\fP will write its process ID to +\fIfilename\fP. +.PP +.TP 5 +\fB\-X\fP, \fB\-\-listen\-once\fP +Causes \fBsvnserve\fP to accept one connection on the svn port, serve +it, and exit. This option is mainly useful for debugging. +.PP +Unless the \fB\-\-config\-file\fP option was specified on the command +line, once the client has selected a repository by transmitting its +URL, \fBsvnserve\fP reads a file named \fBconf/svnserve.conf\fP in the +repository directory to determine repository-specific settings such as +what authentication database to use and what authorization policies to +apply. See the \fBsvnserve.conf\fP(5) man page for details of that +file format. +.SH SEE ALSO +.BR svnserve.conf (5) diff --git a/subversion/svnserve/svnserve.conf.5 b/subversion/svnserve/svnserve.conf.5 new file mode 100644 index 0000000..fefccce --- /dev/null +++ b/subversion/svnserve/svnserve.conf.5 @@ -0,0 +1,98 @@ +.\" +.\" +.\" 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. +.\" +.\" +.\" You can view this file with: +.\" nroff -man [filename] +.\" +.TH svnserve.conf 5 +.SH NAME +svnserve.conf \- Repository configuration file for svnserve +.SH SYNOPSIS +.TP +\fIrepository-path\fP\fB/conf/svnserve.conf\fP +.SH DESCRIPTION +\fBsvnserve.conf\fP controls the behavior of the \fBsvnserve\fP daemon +on a per-repository basis. It is located in the \fBconf\fP +subdirectory of the repository. +.PP +The overall structure of the file is the same as the structure of +Subversion user configuration files. At the top level are sections, +which are specified by words in square brackets; inside each section +are variable definitions of the form "variable = value". Lines +beginning with '#' are ignored. \fBsvnserve.conf\fP currently uses +only one section named "general", and supports the following +variables: +.PP +.TP 5 +\fBanon-access\fP = \fBnone\fP|\fBread\fP|\fBwrite\fP +Determines the access level for unauthenticated users. \fBwrite\fP +access allows all repository operations. \fBread\fP access allows all +operations except committing and changing revision properties. +\fBnone\fP access allows no access. The default level is \fBread\fP. +.PP +.TP 5 +\fBauth-access\fP = \fBnone\fP|\fBread\fP|\fBwrite\fP +Determines the access level for authenticated users, using the same +access levels as above. The default level is \fBwrite\fP. +.PP +.TP 5 +\fBpassword-db\fP = \fIfilename\fP +Sets the location of the password database. \fIfilename\fP may be +relative to the repository conf directory. There is no default value. +The password database has the same overall format as this file. It +uses only one section "users"; each variable within the section is a +username, and each value is a password. +.PP +.TP 5 +\fBauthz-db\fP = \fIfilename\fP +The authz-db option controls the location of the authorization +rules for path-based access control. \fIfilename\fP may be +relative to the repository conf directory. There is no default value. +If you don't specify an authz-db, no path-based access control is done. +.PP +.TP 5 +\fBrealm\fP = \fIrealm\-name\fP +Sets the authentication realm of the repository. If two repositories +have the same password database, they should have the same realm, and +vice versa; this association allows clients to use a single cached +password for several repositories. The default realm value is the +repository's uuid. +.SH EXAMPLE +The following example \fBsvnserve.conf\fP allows read access for +authenticated users, no access for anonymous users, points to a passwd +database in the same directory, and defines a realm name. +.PP +.nf + [general] + anon-access = none + auth-access = read + password-db = passwd + realm = My First Repository +.fi +.PP +The file "passwd" would look like: +.PP +.nf + [users] + joeuser = joepassword + jayrandom = randomjay +.fi +.SH SEE ALSO +.BR svnserve (8) diff --git a/subversion/svnserve/winservice.c b/subversion/svnserve/winservice.c new file mode 100644 index 0000000..dcb399e --- /dev/null +++ b/subversion/svnserve/winservice.c @@ -0,0 +1,490 @@ +/* + * winservice.c : Implementation of Windows Service support + * + * ==================================================================== + * 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. + * ==================================================================== + */ + + + +#define APR_WANT_STRFUNC +#include <apr_want.h> +#include <apr_errno.h> + +#include "svn_error.h" + +#include "svn_private_config.h" +#include "winservice.h" + +/* +Design Notes +------------ + +The code in this file allows svnserve to run as a Windows service. +Windows Services are only supported on operating systems derived +from Windows NT, which is basically all modern versions of Windows +(2000, XP, Server, Vista, etc.) and excludes the Windows 9x line. + +Windows Services are processes that are started and controlled by +the Service Control Manager. When the SCM wants to start a service, +it creates the process, then waits for the process to connect to +the SCM, so that the SCM and service process can communicate. +This is done using the StartServiceCtrlDispatcher function. + +In order to minimize changes to the svnserve startup logic, this +implementation differs slightly from most service implementations. +In most services, main() immediately calls StartServiceCtrlDispatcher, +which does not return control to main() until the SCM sends the +"stop" request to the service, and the service stops. + + +Installing the Service +---------------------- + +Installation is beyond the scope of source code comments. There +is a separate document that describes how to install and uninstall +the service. Basically, you create a Windows Service, give it a +binary path that points to svnserve.exe, and make sure that you +specify --service on the command line. + + +Starting the Service +-------------------- + +First, the SCM decides that it wants to start a service. It creates +the process for the service, passing it the command-line that is +stored in the service configuration (stored in the registry). + +Next, main() runs. The command-line should contain the --service +argument, which is the hint that svnserve is running under the SCM, +not as a standalone process. main() calls winservice_start(). + +winservice_start() creates an event object (winservice_start_event), +and creates and starts a separate thread, the "dispatcher" thread. +winservice_start() then waits for either winservice_start_event +to fire (meaning: "the dispatcher thread successfully connected +to the SCM, and now the service is starting") or for the dispatcher +thread to exit (meaning: "failed to connect to SCM"). + +If the dispatcher thread quit, then winservice_start returns an error. +If the start event fired, then winservice_start returns a success code +(SVN_NO_ERROR). At this point, the service is now in the "starting" +state, from the perspective of the SCM. winservice_start also registers +an atexit handler, which handles cleaning up some of the service logic, +as explained below in "Stopping the Service". + +Next, control returns to main(), which performs the usual startup +logic for svnserve. Mostly, it creates the listener socket. If +main() was able to start the service, then it calls the function +winservice_running(). + +winservice_running() informs the SCM that the service has finished +starting, and is now in the "running" state. main() then does its +work, accepting client sockets and processing SVN requests. + +Stopping the Service +-------------------- + +At some point, the SCM will decide to stop the service, either because +an administrator chose to stop the service, or the system is shutting +down. To do this, the SCM calls winservice_handler() with the +SERVICE_CONTROL_STOP control code. When this happens, +winservice_handler() will inform the SCM that the service is now +in the "stopping" state, and will call winservice_notify_stop(). + +winservice_notify_stop() is responsible for cleanly shutting down the +svnserve logic (waiting for client requests to finish, stopping database +access, etc.). Right now, all it does is close the listener socket, +which causes the apr_socket_accept() call in main() to fail. main() +then calls exit(), which processes all atexit() handlers, which +results in winservice_stop() being called. + +winservice_stop() notifies the SCM that the service is now stopped, +and then waits for the dispatcher thread to exit. Because all services +in the process have now stopped, the call to StartServiceCtrlDispatcher +(in the dispatcher thread) finally returns, and winservice_stop() returns, +and the process finally exits. +*/ + + +#ifdef WIN32 + +#include <assert.h> +#include <winsvc.h> + +/* This is just a placeholder, and doesn't actually constrain the + service name. You have to provide *some* service name to the SCM + API, but for services that are marked SERVICE_WIN32_OWN_PROCESS (as + is the case for svnserve), the service name is ignored. It *is* + relevant for service binaries that run more than one service in a + single process. */ +#define WINSERVICE_SERVICE_NAME "svnserve" + + +/* Win32 handle to the dispatcher thread. */ +static HANDLE winservice_dispatcher_thread = NULL; + +/* Win32 event handle, used to notify winservice_start() that we have + successfully connected to the SCM. */ +static HANDLE winservice_start_event = NULL; + +/* RPC handle that allows us to notify the SCM of changes in our + service status. */ +static SERVICE_STATUS_HANDLE winservice_status_handle = NULL; + +/* Our current idea of the service status (stopped, running, controls + accepted, exit code, etc.) */ +static SERVICE_STATUS winservice_status; + + +#ifdef SVN_DEBUG +static void dbg_print(const char* text) +{ + OutputDebugStringA(text); +} +#else +/* Make sure dbg_print compiles to nothing in release builds. */ +#define dbg_print(text) +#endif + + +static void winservice_atexit(void); + +/* Notifies the Service Control Manager of the current state of the + service. */ +static void +winservice_update_state(void) +{ + if (winservice_status_handle != NULL) + { + if (!SetServiceStatus(winservice_status_handle, &winservice_status)) + { + dbg_print("SetServiceStatus - FAILED\r\n"); + } + } +} + + +/* This function cleans up state associated with the service support. + If the dispatcher thread handle is non-NULL, then this function + will wait for the dispatcher thread to exit. */ +static void +winservice_cleanup(void) +{ + if (winservice_start_event != NULL) + { + CloseHandle(winservice_start_event); + winservice_start_event = NULL; + } + + if (winservice_dispatcher_thread != NULL) + { + dbg_print("winservice_cleanup:" + " waiting for dispatcher thread to exit\r\n"); + WaitForSingleObject(winservice_dispatcher_thread, INFINITE); + CloseHandle(winservice_dispatcher_thread); + winservice_dispatcher_thread = NULL; + } +} + + +/* The SCM invokes this function to cause state changes in the + service. */ +static void WINAPI +winservice_handler(DWORD control) +{ + switch (control) + { + case SERVICE_CONTROL_INTERROGATE: + /* The SCM just wants to check our state. We are required to + call SetServiceStatus, but we don't need to make any state + changes. */ + dbg_print("SERVICE_CONTROL_INTERROGATE\r\n"); + winservice_update_state(); + break; + + case SERVICE_CONTROL_STOP: + dbg_print("SERVICE_CONTROL_STOP\r\n"); + winservice_status.dwCurrentState = SERVICE_STOP_PENDING; + winservice_update_state(); + winservice_notify_stop(); + break; + } +} + + +/* This is the "service main" routine (in the Win32 terminology). + + Normally, this function (thread) implements the "main" loop of a + service. However, in order to minimize changes to the svnserve + main() function, this function is running in a different thread, + and main() is blocked in winservice_start(), waiting for + winservice_start_event. So this function (thread) only needs to + signal that event to "start" the service. + + If this function succeeds, it signals winservice_start_event, which + wakes up the winservice_start() frame that is blocked. */ +static void WINAPI +winservice_service_main(DWORD argc, LPTSTR *argv) +{ + DWORD error; + + assert(winservice_start_event != NULL); + + winservice_status_handle = + RegisterServiceCtrlHandler(WINSERVICE_SERVICE_NAME, winservice_handler); + if (winservice_status_handle == NULL) + { + /* Ok, that's not fair. We received a request to start a service, + and now we cannot bind to the SCM in order to update status? + Bring down the app. */ + error = GetLastError(); + dbg_print("RegisterServiceCtrlHandler FAILED\r\n"); + /* Put the error code somewhere where winservice_start can find it. */ + winservice_status.dwWin32ExitCode = error; + SetEvent(winservice_start_event); + return; + } + + winservice_status.dwCurrentState = SERVICE_START_PENDING; + winservice_status.dwWin32ExitCode = ERROR_SUCCESS; + winservice_update_state(); + + dbg_print("winservice_service_main: service is starting\r\n"); + SetEvent(winservice_start_event); +} + + +static const SERVICE_TABLE_ENTRY winservice_service_table[] = + { + { WINSERVICE_SERVICE_NAME, winservice_service_main }, + { NULL, NULL } + }; + + +/* This is the thread routine for the "dispatcher" thread. The + purpose of this thread is to connect this process with the Service + Control Manager, which allows this process to receive control + requests from the SCM, and allows this process to update the SCM + with status information. + + The StartServiceCtrlDispatcher connects this process to the SCM. + If it succeeds, then it will not return until all of the services + running in this process have stopped. (In our case, there is only + one service per process.) */ +static DWORD WINAPI +winservice_dispatcher_thread_routine(PVOID arg) +{ + dbg_print("winservice_dispatcher_thread_routine: starting\r\n"); + + if (!StartServiceCtrlDispatcher(winservice_service_table)) + { + /* This is a common error. Usually, it means the user has + invoked the service with the --service flag directly. This + is incorrect. The only time the --service flag is passed is + when the process is being started by the SCM. */ + DWORD error = GetLastError(); + + dbg_print("dispatcher: FAILED to connect to SCM\r\n"); + return error; + } + + dbg_print("dispatcher: SCM is done using this process -- exiting\r\n"); + return ERROR_SUCCESS; +} + + +/* If svnserve needs to run as a Win32 service, then we need to + coordinate with the Service Control Manager (SCM) before + continuing. This function call registers the svnserve.exe process + with the SCM, waits for the "start" command from the SCM (which + will come very quickly), and confirms that those steps succeeded. + + After this call succeeds, the service should perform whatever work + it needs to start the service, and then the service should call + winservice_running() (if no errors occurred) or winservice_stop() + (if something failed during startup). */ +svn_error_t * +winservice_start(void) +{ + HANDLE handles[2]; + DWORD thread_id; + DWORD error_code; + apr_status_t apr_status; + DWORD wait_status; + + dbg_print("winservice_start: starting svnserve as a service...\r\n"); + + ZeroMemory(&winservice_status, sizeof(winservice_status)); + winservice_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + winservice_status.dwControlsAccepted = SERVICE_ACCEPT_STOP; + winservice_status.dwCurrentState = SERVICE_STOPPED; + + /* Create the event that will wake up this thread when the SCM + creates the ServiceMain thread. */ + winservice_start_event = CreateEvent(NULL, FALSE, FALSE, NULL); + if (winservice_start_event == NULL) + { + apr_status = apr_get_os_error(); + return svn_error_wrap_apr(apr_status, + _("Failed to create winservice_start_event")); + } + + winservice_dispatcher_thread = + (HANDLE)CreateThread(NULL, 0, winservice_dispatcher_thread_routine, + NULL, 0, &thread_id); + if (winservice_dispatcher_thread == NULL) + { + apr_status = apr_get_os_error(); + winservice_cleanup(); + return svn_error_wrap_apr(apr_status, + _("The service failed to start")); + } + + /* Next, we wait for the "start" event to fire (meaning the service + logic has successfully started), or for the dispatch thread to + exit (meaning the service logic could not start). */ + + handles[0] = winservice_start_event; + handles[1] = winservice_dispatcher_thread; + wait_status = WaitForMultipleObjects(2, handles, FALSE, INFINITE); + switch (wait_status) + { + case WAIT_OBJECT_0: + dbg_print("winservice_start: service is now starting\r\n"); + + /* We no longer need the start event. */ + CloseHandle(winservice_start_event); + winservice_start_event = NULL; + + /* Register our cleanup logic. */ + atexit(winservice_atexit); + return SVN_NO_ERROR; + + case WAIT_OBJECT_0+1: + /* The dispatcher thread exited without starting the service. + This happens when the dispatcher fails to connect to the SCM. */ + dbg_print("winservice_start: dispatcher thread has failed\r\n"); + + if (GetExitCodeThread(winservice_dispatcher_thread, &error_code)) + { + dbg_print("winservice_start: dispatcher thread failed\r\n"); + + if (error_code == ERROR_SUCCESS) + error_code = ERROR_INTERNAL_ERROR; + + } + else + { + error_code = ERROR_INTERNAL_ERROR; + } + + CloseHandle(winservice_dispatcher_thread); + winservice_dispatcher_thread = NULL; + + winservice_cleanup(); + + return svn_error_wrap_apr + (APR_FROM_OS_ERROR(error_code), + _("Failed to connect to Service Control Manager")); + + default: + /* This should never happen! This indicates that our handles are + broken, or some other highly unusual error. There is nothing + rational that we can do to recover. */ + apr_status = apr_get_os_error(); + dbg_print("winservice_start: WaitForMultipleObjects failed!\r\n"); + + winservice_cleanup(); + return svn_error_wrap_apr + (apr_status, _("The service failed to start; an internal error" + " occurred while starting the service")); + } +} + + +/* main() calls this function in order to inform the SCM that the + service has successfully started. This is required; otherwise, the + SCM will believe that the service is stuck in the "starting" state, + and management tools will also believe that the service is stuck. */ +void +winservice_running(void) +{ + winservice_status.dwCurrentState = SERVICE_RUNNING; + winservice_update_state(); + dbg_print("winservice_notify_running: service is now running\r\n"); +} + + +/* main() calls this function in order to notify the SCM that the + service has stopped. This function also handles cleaning up the + dispatcher thread (the one that we created above in + winservice_start. */ +static void +winservice_stop(DWORD exit_code) +{ + dbg_print("winservice_stop - notifying SCM that service has stopped\r\n"); + winservice_status.dwCurrentState = SERVICE_STOPPED; + winservice_status.dwWin32ExitCode = exit_code; + winservice_update_state(); + + if (winservice_dispatcher_thread != NULL) + { + dbg_print("waiting for dispatcher thread to exit...\r\n"); + WaitForSingleObject(winservice_dispatcher_thread, INFINITE); + dbg_print("dispatcher thread has exited.\r\n"); + + CloseHandle(winservice_dispatcher_thread); + winservice_dispatcher_thread = NULL; + } + else + { + /* There was no dispatcher thread. So we never started in + the first place. */ + exit_code = winservice_status.dwWin32ExitCode; + dbg_print("dispatcher thread was not running\r\n"); + } + + if (winservice_start_event != NULL) + { + CloseHandle(winservice_start_event); + winservice_start_event = NULL; + } + + dbg_print("winservice_stop - service has stopped\r\n"); +} + + +/* This function is installed as an atexit-handler. This is done so + that we don't need to alter every exit() call in main(). */ +static void +winservice_atexit(void) +{ + dbg_print("winservice_atexit - stopping\r\n"); + winservice_stop(ERROR_SUCCESS); +} + + +svn_boolean_t +winservice_is_stopping(void) +{ + return (winservice_status.dwCurrentState == SERVICE_STOP_PENDING); +} + +#endif /* WIN32 */ diff --git a/subversion/svnserve/winservice.h b/subversion/svnserve/winservice.h new file mode 100644 index 0000000..8e5ac52 --- /dev/null +++ b/subversion/svnserve/winservice.h @@ -0,0 +1,64 @@ +/* + * winservice.h : Public definitions for Windows Service support + * + * ==================================================================== + * 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. + * ==================================================================== + */ + +#ifndef WINSERVICE_H +#define WINSERVICE_H + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +#ifdef WIN32 + +/* Connects to the Windows Service Control Manager and allows this + process to run as a service. This function can only succeed if the + process was started by the SCM, not directly by a user. After this + call succeeds, the service should perform whatever work it needs to + start the service, and then the service should call + winservice_running() (if no errors occurred) or winservice_stop() + (if something failed during startup). */ +svn_error_t *winservice_start(void); + +/* Notifies the SCM that the service is now running. The caller must + already have called winservice_start successfully. */ +void winservice_running(void); + +/* This function is called by the SCM in an arbitrary thread when the + SCM wants the service to stop. The implementation of this function + can return immediately; all that is necessary is that the service + eventually stop in response. */ +void winservice_notify_stop(void); + +/* Evaluates to TRUE if the SCM has requested that the service stop. + This allows for the service to poll, in addition to being notified + in the winservice_notify_stop callback. */ +svn_boolean_t winservice_is_stopping(void); + +#endif /* WIN32 */ + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* WINSERVICE_H */ |