/*
 * mod_limit_svn.c: an Apache filter that allows you to return arbitrary
 *                  errors for various types of Subversion requests.
 *
 * ====================================================================
 * Copyright (c) 2006 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

#include <httpd.h>
#include <http_config.h>
#include <http_request.h>
#include <http_log.h>
#include <util_filter.h>
#include <ap_config.h>
#include <apr_strings.h>

#include <expat.h>

#include "mod_dav_svn.h"
#include "svn_string.h"
#include "svn_config.h"

module AP_MODULE_DECLARE_DATA limit_svn_module;

typedef struct {
  const char *config_file;
  const char *base_path;
  svn_config_t *config;
} limit_svn_config_rec;

static void *create_limit_dir_config(apr_pool_t *pool, char *dir)
{
  limit_svn_config_rec *cfg = apr_pcalloc(pool, sizeof(*cfg));

  cfg->base_path = dir;

  return cfg;
}

static const command_rec limit_cmds[] =
{
  AP_INIT_TAKE1("LimitSVNConfigFile", ap_set_file_slot,
                (void *) APR_OFFSETOF(limit_svn_config_rec, config_file),
                OR_ALL,
                "Text file containing actions to take for specific requests"),
  { NULL }
};

typedef enum {
  STATE_BEGINNING,
  STATE_IN_UPDATE,
  STATE_IN_SRC_PATH,
  STATE_IN_DST_PATH
} parse_state_t;

typedef struct {
  /* Set to TRUE when we determine that the request is safe and should be
   * allowed to continue. */
  svn_boolean_t let_it_go;

  /* Set to TRUE when we determine that the request is unsafe and should be
   * stopped in its tracks. */
  svn_boolean_t no_soup_for_you;

  XML_Parser xmlp;

  /* The current location in the REPORT body. */
  parse_state_t state;

  /* The S:src-uri for an S:update-report request. */
  svn_stringbuf_t *src_uri;

  /* Set to TRUE if the report has a S:dst-path element. */
  svn_boolean_t has_dst_path;

  /* Set to TRUE if we hit the end of the REPORT body. */
  svn_boolean_t done;

  limit_svn_config_rec *cfg;

  /* The current request. */
  request_rec *r;
} limit_svn_filter_ctx;

typedef struct {
  svn_boolean_t *no_soup_for_you;
  const char *repos_path;
} legal_checkout_baton_t;

static svn_boolean_t
is_legal_checkout(const char *path,
                  const char *permitted,
                  void *b,
                  apr_pool_t *pool)
{
  legal_checkout_baton_t *lcb = b;

  ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, pool,
                "checking path '%s' against repos_path '%s'",
                path, lcb->repos_path);

  if (strcmp(path, lcb->repos_path) == 0)
    {
      /* XXX Check permitted, if the user is a member of that group, let
       *     them do it, otherwise don't.
       *
       *     Of course, that implies that we actually have a group section,
       *     which we don't.  Yet.
       *
       *     Of course, it's not clear if we can even do that, due to the
       *     way authentication works.  Sigh.  Maybe the groups are lists
       *     of IP addresses? */

      *lcb->no_soup_for_you = 1;

      return FALSE;
    }
  else
    {
      return TRUE;
    }  
}

static apr_status_t limit_filter(ap_filter_t *f,
                                 apr_bucket_brigade *bb,
                                 ap_input_mode_t mode,
                                 apr_read_type_e block,
                                 apr_off_t readbytes)
{
  limit_svn_filter_ctx *ctx = f->ctx;
  apr_status_t rv;
  apr_bucket *e;

  if (mode != AP_MODE_READBYTES)
    return ap_get_brigade(f->next, bb, mode, block, readbytes);

  rv = ap_get_brigade(f->next, bb, mode, block, readbytes);
  if (rv)
    return rv;

  for (e = APR_BRIGADE_FIRST(bb);
       e != APR_BRIGADE_SENTINEL(bb);
       e = APR_BUCKET_NEXT(e))
    {
      svn_boolean_t last = APR_BUCKET_IS_EOS(e);
      const char *str;
      apr_size_t len;

      if (last)
        {
          str = "";
          len = 0;
        }
      else
        {
          rv = apr_bucket_read(e, &str, &len, APR_NONBLOCK_READ);
          if (rv)
            return rv;
        }

      if (! XML_Parse(ctx->xmlp, str, len, last))
        {
          /* let_it_go so we clean up our parser, no_soup_for_you so that we
           * bail out before bothering to parse this stuff a second time. */
          ctx->let_it_go = TRUE;
          ctx->no_soup_for_you = TRUE;
        }
      else if (ctx->src_uri && ctx->has_dst_path)
        {
          ctx->let_it_go = TRUE; /* It's a diff, it gets a free pass. */
        }

      /* If we get to the end and we've got a src uri that means it's an
       * update/checkout/export, not a diff or switch, so we need to see
       * if this is something we want to block. */
      if ((last || ctx->done) && ctx->src_uri && ! ctx->let_it_go)
        {
          int trailing_slash;
          const char *cleaned_uri;
          const char *repos_name;
          const char *relative_path;
          dav_error *derr;
          char *uri;

          /* There can be trailing newlines after the URI... */
          svn_stringbuf_strip_whitespace(ctx->src_uri);

          /* Ok, so we need to skip past the scheme, host, etc. */
          uri = strstr(ctx->src_uri->data, "://");
          if (uri)
            uri = strchr(uri + 3, '/');

          if (uri)
            {
              legal_checkout_baton_t lcb;

              derr = dav_svn_split_uri(ctx->r,
                                       uri,
                                       ctx->cfg->base_path,
                                       &cleaned_uri,
                                       &trailing_slash,
                                       &repos_name,
                                       &relative_path,
                                       &lcb.repos_path);
              if (! derr)
                {
                  int nchecks;

                  if (! lcb.repos_path)
                    lcb.repos_path = "";

                  lcb.repos_path = apr_psprintf(ctx->r->pool,
                                                "/%s",
                                                lcb.repos_path);

                  lcb.no_soup_for_you = &ctx->no_soup_for_you;

                  /* Loop over the urls that are not allowed for checkouts,
                   * and see if any of them match our repos_path. */
                  nchecks = svn_config_enumerate2(ctx->cfg->config,
                                                  "checkouts",
                                                  is_legal_checkout,
                                                  &lcb,
                                                  ctx->r->pool);

                  ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                                "mod_limit_svn: checked %d paths", nchecks);
                }
            }

          /* At this point, if we're not going to block it, then we're done. */
          if (! ctx->no_soup_for_you)
            ctx->let_it_go = TRUE;
        }

      /* Clean up our parser if we're done. */
      if (last || ctx->let_it_go || ctx->no_soup_for_you)
        {
          XML_ParserFree(ctx->xmlp);

          ctx->xmlp = NULL;
        }

      /* If we found something that isn't allowed, set the correct status
       * and return an error so it'll bail out before it gets anywhere it
       * can do real damage. */
      if (ctx->no_soup_for_you)
        {
          ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r,
                        "mod_limit_svn: client broke the rules, "
                        "returning error");

          f->r->status = 403;
          f->r->status_line = "406 Forbidden, No Soup For You!";

          return APR_EGENERAL;
        }
      else if (ctx->let_it_go)
        {
          ap_remove_input_filter(f);

          ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r,
                        "mod_limit_svn: letting request go through");

          return rv;
        }
    }

  return rv;
}

static void
cdata(void *baton, const char *data, int len)
{
  limit_svn_filter_ctx *ctx = baton;

  switch (ctx->state)
    {
      case STATE_IN_SRC_PATH:
        if (! ctx->src_uri)
          ctx->src_uri = svn_stringbuf_ncreate(data, len, ctx->r->pool);
        else
          svn_stringbuf_appendbytes(ctx->src_uri, data, len);
        break;

      case STATE_IN_DST_PATH:
        ctx->has_dst_path = TRUE;
        break;

      default:
        break;
    }
}

static void
start_element(void *baton, const char *name, const char **attrs)
{
  limit_svn_filter_ctx *ctx = baton;

  /* XXX namespace handling needs to be fixed, right now this breaks down
   *     on anything other than the default prefixes we use in Subversion. */

  switch (ctx->state)
    {
      case STATE_BEGINNING:
        if (strcmp(name, "S:update-report") == 0)
          ctx->state = STATE_IN_UPDATE; /* XXX look for send-all attr? */
        else
          ctx->let_it_go = TRUE;
        break;

      case STATE_IN_UPDATE:
        if (strcmp(name, "S:src-path") == 0)
          ctx->state = STATE_IN_SRC_PATH;
        else if (strcmp(name, "S:dst-path") == 0)
          ctx->state = STATE_IN_DST_PATH;
        break;

      default:
        break;
    }
}

static void
end_element(void *baton, const char *name)
{
  limit_svn_filter_ctx *ctx = baton;

  if (ctx->state == STATE_IN_SRC_PATH)
    ctx->state = STATE_IN_UPDATE;
  else if (ctx->state == STATE_IN_DST_PATH)
    ctx->state = STATE_IN_UPDATE;
  else if (ctx->state == STATE_IN_UPDATE
           && strcmp(name, "S:update-report") == 0)
    ctx->done = TRUE;
}

static void limit_insert_filters(request_rec *r)
{
  limit_svn_config_rec *cfg = ap_get_module_config(r->per_dir_config,
                                                   &limit_svn_module);

  if (! cfg->config_file)
    return;

  if (strcmp("REPORT", r->method) == 0)
    {
      limit_svn_filter_ctx *ctx = apr_pcalloc(r->pool, sizeof(*ctx));
      svn_error_t *err;

      ctx->r = r;

      ctx->cfg = cfg;

      err = svn_config_read(&cfg->config, cfg->config_file, TRUE, r->pool);
      if (err)
        {
          ap_log_rerror(APLOG_MARK, APLOG_ERR, err->apr_err, r,
                        "mod_limit_svn: Couldn't parse config file '%s'",
                        cfg->config_file);

          svn_error_clear(err);

          return;
        }

      ctx->state = STATE_BEGINNING;

      ctx->xmlp = XML_ParserCreate(NULL);

      XML_SetUserData(ctx->xmlp, ctx);
      XML_SetElementHandler(ctx->xmlp, start_element, end_element);
      XML_SetCharacterDataHandler(ctx->xmlp, cdata);

      ap_add_input_filter("SVN_LIMIT_FILTER", ctx, r, r->connection);
    }
}

static void limit_register_hooks(apr_pool_t *pool)
{
  ap_hook_insert_filter(limit_insert_filters, NULL, NULL, APR_HOOK_FIRST);

  ap_register_input_filter("SVN_LIMIT_FILTER",
                           limit_filter,
                           NULL,
                           AP_FTYPE_RESOURCE);
}

module AP_MODULE_DECLARE_DATA limit_svn_module =
{
  STANDARD20_MODULE_STUFF,
  create_limit_dir_config,
  NULL,
  NULL,
  NULL,
  limit_cmds,
  limit_register_hooks
};

