/* Copyright 2006-2010 by Nils Maier
 *
 * Licensed 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.
 */

/*
 * mod_xsendfile.c: Process X-SENDFILE header cgi/scripts may set
 *     Written by Nils Maier, testnutzer123 at google mail, March 2006
 *
 * Whenever an X-SENDFILE header occures in the response headers drop
 * the body and send the replacement file idenfitied by this header instead.
 *
 * Method inspired by lighttpd <http://lighttpd.net/>
 * Code inspired by mod_headers, mod_rewrite and such
 *
 * Installation:
 *     apxs2 -cia mod_xsendfile.c
 */

/*
 * v0.12 (peer-review still required)
 *
 * $Id$
 */

#include "apr.h"
#include "apr_lib.h"
#include "apr_strings.h"
#include "apr_buckets.h"
#include "apr_file_io.h"

#include "apr_hash.h"
#define APR_WANT_IOVEC
#define APR_WANT_STRFUNC
#include "apr_want.h"

#include "httpd.h"
#include "http_log.h"
#include "http_config.h"
#include "http_log.h"
#define CORE_PRIVATE
#include "http_request.h"
#include "http_core.h" /* needed for per-directory core-config */
#include "util_filter.h"
#include "http_protocol.h" /* ap_hook_insert_error_filter */

#define AP_XSENDFILE_HEADER "X-SENDFILE"

module AP_MODULE_DECLARE_DATA xsendfile_module;

typedef enum {
  XSENDFILE_UNSET = 0,
  XSENDFILE_ENABLED = 1<<0,
  XSENDFILE_DISABLED = 1<<1
} xsendfile_conf_active_t;

typedef struct xsendfile_conf_t {
  xsendfile_conf_active_t enabled;
  xsendfile_conf_active_t ignoreETag;
  xsendfile_conf_active_t ignoreLM;
  apr_array_header_t *paths;
} xsendfile_conf_t;

static xsendfile_conf_t *xsendfile_config_create(apr_pool_t *p) {
  xsendfile_conf_t *conf;

  conf = (xsendfile_conf_t *) apr_pcalloc(p, sizeof(xsendfile_conf_t));
  conf->ignoreETag =
    conf->ignoreLM =
    conf->enabled =
    XSENDFILE_UNSET;

  conf->paths = apr_array_make(p, 1, sizeof(char*));

  return conf;
}

static void *xsendfile_config_server_create(apr_pool_t *p, server_rec *s) {
  return (void*)xsendfile_config_create(p);
}

#define XSENDFILE_CFLAG(x) conf->x = overrides->x != XSENDFILE_UNSET ? overrides->x : base->x

static void *xsendfile_config_merge(apr_pool_t *p, void *basev, void *overridesv) {
  xsendfile_conf_t *base = (xsendfile_conf_t *)basev;
  xsendfile_conf_t *overrides = (xsendfile_conf_t *)overridesv;
  xsendfile_conf_t *conf;

  conf = (xsendfile_conf_t *) apr_pcalloc(p, sizeof(xsendfile_conf_t));

  XSENDFILE_CFLAG(enabled);
  XSENDFILE_CFLAG(ignoreETag);
  XSENDFILE_CFLAG(ignoreLM);

  conf->paths = apr_array_append(p, overrides->paths, base->paths);

  return (void*)conf;
}

static void *xsendfile_config_perdir_create(apr_pool_t *p, char *path) {
  return (void*)xsendfile_config_create(p);
}
#undef XSENDFILE_CFLAG

static const char *xsendfile_cmd_flag(cmd_parms *cmd, void *perdir_confv, int flag) {
  xsendfile_conf_t *conf = (xsendfile_conf_t *)perdir_confv;
  if (cmd->path == NULL) {
    conf = (xsendfile_conf_t*)ap_get_module_config(
      cmd->server->module_config,
      &xsendfile_module
      );
  }
  if (!conf) {
    return "Cannot get configuration object";
  }
  if (!strcasecmp(cmd->cmd->name, "xsendfile")) {
    conf->enabled = flag ? XSENDFILE_ENABLED : XSENDFILE_DISABLED;
  }
  else if (!strcasecmp(cmd->cmd->name, "xsendfileignoreetag")) {
    conf->ignoreETag = flag ? XSENDFILE_ENABLED: XSENDFILE_DISABLED;
  }
  else if (!strcasecmp(cmd->cmd->name, "xsendfileignorelastmodified")) {
    conf->ignoreLM = flag ? XSENDFILE_ENABLED: XSENDFILE_DISABLED;
  }
  else {
    return apr_psprintf(cmd->pool, "Not a valid command in this context: %s %s", cmd->cmd->name, flag ? "On": "Off");
  }

  return NULL;
}

static const char *xsendfile_cmd_path(cmd_parms *cmd, void *pdc, const char *arg) {
  xsendfile_conf_t *conf = (xsendfile_conf_t*)ap_get_module_config(
    cmd->server->module_config,
    &xsendfile_module
    );
  char **newpath = (char**)apr_array_push(conf->paths);
  *newpath = apr_pstrdup(cmd->pool, arg);

  return NULL;
}

/*
  little helper function to get the original request path
  code borrowed from request.c and util_script.c
*/
static const char *ap_xsendfile_get_orginal_path(request_rec *rec) {
  const char
    *rv = rec->the_request,
    *last;

  int dir = 0;
  size_t uri_len;

  /* skip method && spaces */
  while (*rv && !apr_isspace(*rv)) {
    ++rv;
  }
  while (apr_isspace(*rv)) {
    ++rv;
  }
  /* first space is the request end */
  last = rv;
  while (*last && !apr_isspace(*last)) {
    ++last;
  }
  uri_len = last - rv;
  if (!uri_len) {
    return NULL;
  }

  /* alright, lets see if the request_uri changed! */
  if (strncmp(rv, rec->uri, uri_len) == 0) {
    rv = apr_pstrdup(rec->pool, rec->filename);
    dir = rec->finfo.filetype == APR_DIR;
  }
  else {
    /* need to lookup the url again as it changed */
    request_rec *sr = ap_sub_req_lookup_uri(
      apr_pstrmemdup(rec->pool, rv, uri_len),
      rec,
      NULL
      );
    if (!sr) {
      return NULL;
    }
    rv = apr_pstrdup(rec->pool, sr->filename);
    dir = rec->finfo.filetype == APR_DIR;
    ap_destroy_sub_req(sr);
  }

  /* now we need to truncate so we only have the directory */
  if (!dir && (last = ap_strrchr(rv, '/')) != NULL) {
    *((char*)last + 1) = '\0';
  }
  return rv;
}

/*
  little helper function to build the file path if available
*/
static apr_status_t ap_xsendfile_get_filepath(request_rec *r, xsendfile_conf_t *conf, const char *file, /* out */ char **path) {

  const char *root = ap_xsendfile_get_orginal_path(r);
  apr_status_t rv;

  apr_array_header_t *patharr;
  const char **paths;
  int i;


#ifdef _DEBUG
  ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: path is %s", root);
#endif

  /* merge the array */
  if (root) {
    patharr = apr_array_make(r->pool, conf->paths->nelts + 1, sizeof(char*));
    *(const char**)(apr_array_push(patharr)) = root;
    apr_array_cat(patharr, conf->paths);
  } else {
    patharr = conf->paths;
  }
  if (patharr->nelts == 0) {
    return APR_EBADPATH;
  }
  paths = (const char**)patharr->elts;

  for (i = 0; i < patharr->nelts; ++i) {
    if ((rv = apr_filepath_merge(
      path,
      paths[i],
      file,
      APR_FILEPATH_TRUENAME | APR_FILEPATH_NOTABOVEROOT,
      r->pool
    )) == OK) {
      break;
    }
  }
  if (rv != OK) {
    *path = NULL;
  }
  return rv;
}

static apr_status_t ap_xsendfile_output_filter(ap_filter_t *f, apr_bucket_brigade *in) {
  request_rec *r = f->r, *sr = NULL;

  xsendfile_conf_t
    *dconf = (xsendfile_conf_t *)ap_get_module_config(r->per_dir_config, &xsendfile_module),
    *sconf = (xsendfile_conf_t *)ap_get_module_config(r->server->module_config, &xsendfile_module),
    *conf = xsendfile_config_merge(r->pool, sconf, dconf);

  core_dir_config *coreconf = (core_dir_config *)ap_get_module_config(r->per_dir_config, &core_module);

  apr_status_t rv;
  apr_bucket *e;

  apr_file_t *fd = NULL;
  apr_finfo_t finfo;

  const char *file = NULL;
  char *translated = NULL;

  int errcode;

#ifdef _DEBUG
  ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: output_filter for %s", r->the_request);
#endif
  /*
    should we proceed with this request?

    * sub-requests suck
    * furthermore default-handled requests suck, as they actually shouldn't be able to set headers
  */
  if (
    r->status != HTTP_OK
    || r->main
    || (r->handler && strcmp(r->handler, "default-handler") == 0) /* those table-keys are lower-case, right? */
  ) {
#ifdef _DEBUG
    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: not met [%d]", r->status);
#endif
    ap_remove_output_filter(f);
    return ap_pass_brigade(f->next, in);
  }

  /*
    alright, look for x-sendfile
  */
  file = apr_table_get(r->headers_out, AP_XSENDFILE_HEADER);
  apr_table_unset(r->headers_out, AP_XSENDFILE_HEADER);

  /* cgi/fastcgi will put the stuff into err_headers_out */
  if (!file || !*file) {
    file = apr_table_get(r->err_headers_out, AP_XSENDFILE_HEADER);
    apr_table_unset(r->err_headers_out, AP_XSENDFILE_HEADER);
  }

  /* nothing there :p */
  if (!file || !*file) {
#ifdef _DEBUG
    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: nothing found");
#endif
    ap_remove_output_filter(f);
    return ap_pass_brigade(f->next, in);
  }

  /*
    drop *everything*
    might be pretty expensive to generate content first that goes straight to the bitbucket,
    but actually the scripts that might set this flag won't output too much anyway
  */
  while (!APR_BRIGADE_EMPTY(in)) {
    e = APR_BRIGADE_FIRST(in);
    apr_bucket_delete(e);
  }
  r->eos_sent = 0;

  /* as we dropped all the content this field is not valid anymore! */
  apr_table_unset(r->headers_out, "Content-Length");
  apr_table_unset(r->err_headers_out, "Content-Length");
  apr_table_unset(r->headers_out, "Content-Encoding");
  apr_table_unset(r->err_headers_out, "Content-Encoding");

  rv = ap_xsendfile_get_filepath(r, conf, file, &translated);
  if (rv != OK) {
    ap_log_rerror(
      APLOG_MARK,
      APLOG_ERR,
      rv,
      r,
      "xsendfile: unable to find file: %s",
      file
      );
    ap_remove_output_filter(f);
    ap_die(HTTP_NOT_FOUND, r);
    return HTTP_NOT_FOUND;
  }

#ifdef _DEBUG
  ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: found %s", translated);
#endif

  /*
    try open the file
  */
  if ((rv = apr_file_open(
    &fd,
    translated,
    APR_READ | APR_BINARY
#if APR_HAS_SENDFILE
    | (coreconf->enable_sendfile != ENABLE_SENDFILE_OFF ? APR_SENDFILE_ENABLED : 0)
#endif
    ,
    0,
    r->pool
  )) != APR_SUCCESS) {
    ap_log_rerror(
      APLOG_MARK,
      APLOG_ERR,
      rv,
      r,
      "xsendfile: cannot open file: %s",
      translated
      );
    ap_remove_output_filter(f);
    ap_die(HTTP_NOT_FOUND, r);
    return HTTP_NOT_FOUND;
  }
#if APR_HAS_SENDFILE && defined(_DEBUG)
  if (coreconf->enable_sendfile == ENABLE_SENDFILE_OFF) {
    ap_log_error(
      APLOG_MARK,
      APLOG_WARNING,
      0,
      r->server,
      "xsendfile: sendfile configured, but not active %d",
      coreconf->enable_sendfile
      );
    }
#endif
  /* stat (for etag/cache/content-length stuff) */
  if ((rv = apr_file_info_get(&finfo, APR_FINFO_NORM, fd)) != APR_SUCCESS) {
    ap_log_rerror(
      APLOG_MARK,
      APLOG_ERR,
      rv,
      r,
      "xsendfile: unable to stat file: %s",
      translated
      );
    apr_file_close(fd);
    ap_remove_output_filter(f);
    ap_die(HTTP_FORBIDDEN, r);
    return HTTP_FORBIDDEN;
  }
  /* no inclusion of directories! we're serving files! */
  if (finfo.filetype != APR_REG) {
    ap_log_rerror(
      APLOG_MARK,
      APLOG_ERR,
      APR_EBADPATH,
      r,
      "xsendfile: not a file %s",
      translated
      );
    apr_file_close(fd);
    ap_remove_output_filter(f);
    ap_die(HTTP_NOT_FOUND, r);
    return HTTP_NOT_FOUND;
  }

  /*
    need to cheat here a bit
    as etag generator will use those ;)
    and we want local_copy and cache
  */
  r->finfo.inode = finfo.inode;
  r->finfo.size = finfo.size;

  /*
    caching? why not :p
  */
  r->no_cache = r->no_local_copy = 0;

  /* some script (f?cgi) place stuff in err_headers_out */
  if (
    conf->ignoreLM == XSENDFILE_ENABLED
    || (
      !apr_table_get(r->headers_out, "last-modified")
      && !apr_table_get(r->headers_out, "last-modified")
    )
  ) {
    apr_table_unset(r->err_headers_out, "last-modified");
    ap_update_mtime(r, finfo.mtime);
    ap_set_last_modified(r);
  }
  if (
    conf->ignoreETag == XSENDFILE_ENABLED
    || (
      !apr_table_get(r->headers_out, "etag")
      && !apr_table_get(r->err_headers_out, "etag")
    )
  ) {
    apr_table_unset(r->err_headers_out, "etag");
    ap_set_etag(r);
  }

  ap_set_content_length(r, finfo.size);

  /* cache or something? */
  if ((errcode = ap_meets_conditions(r)) != OK) {
#ifdef _DEBUG
    ap_log_error(
      APLOG_MARK,
      APLOG_DEBUG,
      0,
      r->server,
      "xsendfile: met condition %d for %s",
      errcode,
      file
      );
#endif
    apr_file_close(fd);
    r->status = errcode;
  }
  else {
    /* For platforms where the size of the file may be larger than
     * that which can be stored in a single bucket (where the
     * length field is an apr_size_t), split it into several
     * buckets: */
    if (sizeof(apr_off_t) > sizeof(apr_size_t)
        && finfo.size > AP_MAX_SENDFILE) {
        apr_off_t fsize = finfo.size;
        e = apr_bucket_file_create(fd, 0, AP_MAX_SENDFILE, r->pool,
                                   in->bucket_alloc);
        while (fsize > AP_MAX_SENDFILE) {
            apr_bucket *ce;
            apr_bucket_copy(e, &ce);
            APR_BRIGADE_INSERT_TAIL(in, ce);
            e->start += AP_MAX_SENDFILE;
            fsize -= AP_MAX_SENDFILE;
        }
        e->length = (apr_size_t)fsize; /* Resize just the last bucket */
    }
    else {
        e = apr_bucket_file_create(fd, 0, (apr_size_t)finfo.size,
                                   r->pool, in->bucket_alloc);
    }


#if APR_HAS_MMAP
    if (coreconf->enable_mmap == ENABLE_MMAP_ON) {
      apr_bucket_file_enable_mmap(e, 0);
    }
#if defined(_DEBUG)
    else {
      ap_log_error(
        APLOG_MARK,
        APLOG_WARNING,
        0,
        r->server,
        "xsendfile: mmap configured, but not active %d",
        coreconf->enable_mmap
        );
      }
#endif /* _DEBUG */
#endif /* APR_HAS_MMAP */
    APR_BRIGADE_INSERT_TAIL(in, e);
  }

  e = apr_bucket_eos_create(in->bucket_alloc);
  APR_BRIGADE_INSERT_TAIL(in, e);

  /* remove ourselves from the filter chain */
  ap_remove_output_filter(f);

#ifdef _DEBUG
  ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "xsendfile: sending %d bytes", (int)finfo.size);
#endif

  /* send the data up the stack */
  return ap_pass_brigade(f->next, in);
}

static void ap_xsendfile_insert_output_filter(request_rec *r) {
  xsendfile_conf_active_t enabled = ((xsendfile_conf_t *)ap_get_module_config(r->per_dir_config, &xsendfile_module))->enabled;
  if (XSENDFILE_UNSET == enabled) {
    enabled = ((xsendfile_conf_t*)ap_get_module_config(r->server->module_config, &xsendfile_module))->enabled;
  }

  if (XSENDFILE_ENABLED != enabled) {
    return;
  }

  ap_add_output_filter(
    "XSENDFILE",
    NULL,
    r,
    r->connection
	  );
}
static const command_rec xsendfile_command_table[] = {
  AP_INIT_FLAG(
    "XSendFile",
    xsendfile_cmd_flag,
    NULL,
    OR_FILEINFO,
    "On|Off - Enable/disable(default) processing"
    ),
  AP_INIT_FLAG(
    "XSendFileIgnoreEtag",
    xsendfile_cmd_flag,
    NULL,
    OR_FILEINFO,
    "On|Off - Ignore script provided Etag headers (default: Off)"
    ),
  AP_INIT_FLAG(
    "XSendFileIgnoreLastModified",
    xsendfile_cmd_flag,
    NULL,
    OR_FILEINFO,
    "On|Off - Ignore script provided Last-Modified headers (default: Off)"
    ),
  AP_INIT_TAKE1(
    "XSendFilePath",
    xsendfile_cmd_path,
    NULL,
    RSRC_CONF|ACCESS_CONF,
    "Allow to serve files from that Path. Must be absolute"
    ),
  { NULL }
};
static void xsendfile_register_hooks(apr_pool_t *p) {
  ap_register_output_filter(
    "XSENDFILE",
    ap_xsendfile_output_filter,
    NULL,
    AP_FTYPE_CONTENT_SET
    );

  ap_hook_insert_filter(
    ap_xsendfile_insert_output_filter,
    NULL,
    NULL,
    APR_HOOK_LAST + 1
    );
}
module AP_MODULE_DECLARE_DATA xsendfile_module = {
  STANDARD20_MODULE_STUFF,
  xsendfile_config_perdir_create,
  xsendfile_config_merge,
  xsendfile_config_server_create,
  xsendfile_config_merge,
  xsendfile_command_table,
  xsendfile_register_hooks
};
