/*
 * Copyright 2008-2009 Michael Koziarski and Pratik Naik
 *
 * 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.
 */
#include <http_core.h>
#include <http_config.h>
#include <http_log.h>
#include <apr_strings.h>
#include <apr_sha1.h>
#include <apreq_util.h>
#include <apreq2/apreq_module_apache2.h>

#define HTTP_X_UPLOADS "X-Uploads"

#ifdef PORTER_DEBUG
  #define PORTER_LOG(expr) fprintf(stderr, "[PORTER_LOG] %s\n", expr) && fflush(stderr)
#else
  #define PORTER_LOG(expr)
#endif

#define PORTER_LOG_REQUEST_ERROR(error) ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, error)

#define PORTER_HANDLE_ERROR(expr) \
  rv = expr;                  \
  if (rv != APR_SUCCESS)      \
  {                           \
    return rv;                \
  }



AP_DECLARE_DATA ap_filter_rec_t *porter_input_filter_handle;

typedef struct porter_upload_request_t {
  apreq_handle_t *request;
  apr_array_header_t *param_names;
  apr_bucket_brigade *bucket_brigade;
  apr_status_t status;
  apr_pool_t *pool;
  request_rec *raw_request;
  const char *secret;
} porter_upload_request_t;

typedef struct porter_server_conf {
  int enabled;
  apr_uint64_t minimum_multipart_size;
  const char *secret;
  apr_fileperms_t permission;
  const char *directory;
} porter_server_conf;

extern module AP_MODULE_DECLARE_DATA porter_module;

int porter_should_rewrite_body(request_rec *r, porter_server_conf *config);
apr_status_t porter_process_upload(request_rec *r);
int porter_each_parameter(void *data, const char *key, const char *val);
apr_status_t porter_handle_parameter(porter_upload_request_t *ur, apreq_param_t *param);
apr_status_t porter_handle_parameter(porter_upload_request_t *ur, apreq_param_t *param);
apr_status_t porter_handle_upload(porter_upload_request_t *ur, apreq_param_t *p);
char *porter_sign_filename(porter_upload_request_t *ur, apr_finfo_t *finfo);
apr_status_t porter_stream_file_to_disk(apr_pool_t *pool, apreq_param_t *p, apr_finfo_t *finfo, const char *path);
apr_status_t porter_append_sub_parameter(apr_pool_t *pool, apr_bucket_brigade *bb,
                                     const char *parent_param,
                                     const char *sub_param,
                                     const char *sub_param_value,
                                     apr_size_t length);
porter_upload_request_t* porter_create_request(apreq_handle_t *req,
                                      request_rec *raw_request,
                                      porter_server_conf *config);
                                     
// This input filter handles the job of rewriting the request, it's injected at
// runtime by the porter fixup filter.  All it does is remove itself from the
// filter chain and pass the newly modified content up the filter chain.
static apr_status_t porter_input_filter(ap_filter_t *f, apr_bucket_brigade *bb,
                                    ap_input_mode_t mode, apr_read_type_e block,
                                    apr_off_t readbytes)
{
  porter_upload_request_t *ur = f->ctx;

  if (!ur) {
    // Because we add ourselves dynamically, this should never occur.
    // but handle it anyway.
    return ap_get_brigade(f->next, bb, mode, block, readbytes);
  }

  // Remove ourselves so we don't trigger again.
  ap_remove_input_filter(f);

  APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_eos_create(bb->bucket_alloc)) ;
  APR_BRIGADE_PREPEND(bb, ur->bucket_brigade);

  apr_brigade_destroy(ur->bucket_brigade);

  return APR_SUCCESS;
}

// The fixup filter's job is to determine if the content body needs to be rewritten,
// and if it does need to be rewritten, it triggers the work
static int porter_fixup(request_rec *r)
{
  apr_status_t rv;
  apr_table_setn(r->headers_in, HTTP_X_UPLOADS, NULL);

  porter_server_conf *config = (porter_server_conf *)ap_get_module_config(r->server->module_config, &porter_module);

  if(!config->enabled)
  {
    PORTER_LOG("Sadly you don't want your uploads to scale !! Good bye");
    return DECLINED;
  }

  int http_status = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR);
  if (http_status != OK) {
    return http_status;
  }

  if (porter_should_rewrite_body(r, config))
  {
    PORTER_HANDLE_ERROR(porter_process_upload(r));
  }

  return DECLINED;
}


// Determines whether or not porter should apply.  Return true if the content
// type is multipart/form-data and the content length is greater than
// PorterMinSize.
int porter_should_rewrite_body(request_rec *r, porter_server_conf *config)
{
  if (ap_should_client_block(r))
  {
    const char *content_type   = apr_table_get(r->headers_in, "Content-Type");
    const char *content_length = apr_table_get(r->headers_in, "Content-Length");
    if (content_type && strcasecmp(content_type, "multipart/form-data") > 0 && atol(content_length) > config->minimum_multipart_size)
    {
      return 1;
    }
  }
  return 0;
}


// This is the method which does all the work handling the uploads.  It's only
// triggered by the fixup filter if +porter_should_rewrite_body+ returns true.
apr_status_t porter_process_upload(request_rec *r)
{
  porter_server_conf *config = (porter_server_conf *) ap_get_module_config(r->server->module_config, &porter_module);

  // Prepare the apreq objects.
  apreq_handle_t *req = apreq_handle_apache2(r);
  const apr_table_t *request_body = apreq_params(req, r->pool);

  // Create our upload request object, this is fleshed out by the rest of this method
  porter_upload_request_t *upload_request = porter_create_request(req, r, config);

  // This happens with malformed requests.  apreq_params should never return null
  // when the content-length is > 0 and it's a multipart request.  So just assume
  // it's broken and return bad request.  Otherwise subsequent method calls will
  // cause a segfault
  if (request_body == NULL)
  {
    PORTER_LOG_REQUEST_ERROR("Invalid request body");
    return HTTP_BAD_REQUEST;
  }

  // loop over each parameter provided by the user (see porter_each_parameter)
  apr_table_do(porter_each_parameter, upload_request, request_body, NULL);

  // If any of the parameter handlers return an error, they save the error code
  // in the upload_request.  So return that same error code.
  if (upload_request->status != APR_SUCCESS)
  {
    return upload_request->status;
  }


  // Just because the content type is multipart and the content-length was > 0 doesn't
  // mean that the user actually uploaded any files.  If they didn't, just return success
  // and let the original body be passed upstream.
  if (!apr_is_empty_array(upload_request->param_names))
  {
    // Write the parameter names to the X-Uploads header (comma seperated)
    const char *upload_parameters = apr_array_pstrcat(r->pool, upload_request->param_names, ',');
    apr_off_t len;
    apr_table_setn(r->headers_in, HTTP_X_UPLOADS, upload_parameters);

    // figure out the length of the newly rewritten body and set it in the request
    // along with the right content type.
    apr_brigade_length(upload_request->bucket_brigade, 0, &len);
    apr_table_setn(r->headers_in, "Content-Length", apr_itoa(r->pool, len));
    apr_table_setn(r->headers_in, "Content-Type", "application/x-www-form-urlencoded");

    // Add our input filter to the filter chain, this allows
    // us to replace the request body with our own one, and ensure that
    // gets passed down to the handler.
    ap_add_input_filter_handle(porter_input_filter_handle, upload_request, r, r->connection);
  }
  return APR_SUCCESS;
}

// This is the iterator which handles every parameter and rewrites them as needed.
// If the parameter is an upload, it passes it to porter_handle_upload otherwise
// it calls porter_handle_parameter.
int porter_each_parameter(void *data, const char *key, const char *val)
{
  porter_upload_request_t *ur = (porter_upload_request_t*)data;
  apreq_param_t           *p  = apreq_value_to_param(val);
  apr_status_t             rv;

  if (p->upload == NULL) 
  {
    rv = porter_handle_parameter(ur, p);
  }
  else 
  {
    rv = porter_handle_upload(ur, p);
  }

  if (rv != APR_SUCCESS)
  {
    // save the error code and stop iterating, the filter will signal
    // the error.
    ur->status = rv;
    return 0;
  }
  return 1;
}



// Handles a simple parameter value, simply concats the name and value to the output 
// stream
apr_status_t porter_handle_parameter(porter_upload_request_t *ur, apreq_param_t *param)
{
  const char *new_parameter_value;
  apr_status_t rv;

  new_parameter_value = apr_pstrcat(ur->pool, apreq_escape(ur->pool, param->v.name, strlen(param->v.name)), "=",
                                              apreq_escape(ur->pool, param->v.data, strlen(param->v.data)), "&",
                                              NULL);
  PORTER_LOG("Writing plain parameter");
  PORTER_LOG(param->v.name);
  PORTER_LOG(new_parameter_value);

  PORTER_HANDLE_ERROR(apr_brigade_write(ur->bucket_brigade, NULL, NULL,
                                    new_parameter_value, strlen(new_parameter_value)));
  return APR_SUCCESS;
}

// Handles an upload parameter.  If the user didn't select a file, it calls the
// regular porter_handle_parameter function.  For uploads it appends several
// sub-parameters:
//
// * filename:     File name that the user has uploaded.
// * content_type: The content type the browser has provided (sometimes not present).
// * path:         The location of the tempfile where the contents were copied to.
// * signature:    A base64 encoded SHA1 hash of the filename and the PorterSharedSecret.
apr_status_t porter_handle_upload(porter_upload_request_t *ur, apreq_param_t *p)
{
  const char *content_disposition;
  const char *file_name;
  const char *content_type;
  const char *signature;

  apr_size_t size;
  apr_status_t rv;
  apr_finfo_t finfo;

  apr_pool_t *pool  = ur->pool;
  char *escaped_key = apreq_escape(pool, p->v.name, strlen(p->v.name));
  apr_table_t *info = p->info;
  
  PORTER_LOG("Handling Upload");
  PORTER_LOG(p->v.name);

  porter_server_conf *config = (porter_server_conf *)ap_get_module_config(ur->raw_request->server->module_config, &porter_module);

  content_disposition = apr_table_get(info, "content-disposition");
  apreq_header_attribute(content_disposition, "filename", 8, &file_name, &size);

  if (size == 0)
  {
    // There was no file, or at least it had no name, so let's
    // just skip it.
    PORTER_LOG("Appears there was no file, skipping the parameter");
    return APR_SUCCESS;
  }
  PORTER_LOG("Appears there was a file, continuing");
  

  // We know we have a file so push the param name into the array
  // of parameter names so it can be added to the request header.
  *(const char**)apr_array_push(ur->param_names) = p->v.name;

  PORTER_HANDLE_ERROR(porter_append_sub_parameter(pool, ur->bucket_brigade, escaped_key, "filename", file_name, size));

  // content type is optional, and safari doesn't send it if it's unsure.
  content_type = apr_table_get(info, "content-type");
  if (content_type)
  {
    PORTER_HANDLE_ERROR(porter_append_sub_parameter(pool, ur->bucket_brigade, escaped_key, "content_type", content_type, strlen(content_type)));
  }

  // Write the actual upload to disk
  PORTER_HANDLE_ERROR(porter_stream_file_to_disk(pool, p, &finfo, config->directory));

  // Set appropriate tempfile permissions
  PORTER_HANDLE_ERROR(apr_file_perms_set(finfo.fname, config->permission));

  PORTER_HANDLE_ERROR(porter_append_sub_parameter(pool, ur->bucket_brigade, escaped_key, "path", finfo.fname, strlen(finfo.fname)));

  signature = porter_sign_filename(ur, &finfo);

  PORTER_HANDLE_ERROR(porter_append_sub_parameter(pool, ur->bucket_brigade, escaped_key, "signature", signature, strlen(signature)));
  return APR_SUCCESS;
}


// Returns the base64 hash of the temp file's name and PorterSharedSecret.
char *porter_sign_filename(porter_upload_request_t *ur, apr_finfo_t *finfo)
{
  PORTER_LOG("Signing Filename");
  const char *value_to_hash = apr_pstrcat(ur->pool, finfo->fname, ur->secret, NULL);

  char *hash = apr_palloc(ur->pool, 100 * sizeof(char));

  apr_sha1_base64(value_to_hash, strlen(value_to_hash), hash);
  PORTER_LOG(hash);
  return hash + APR_SHA1PW_IDLEN;
}


// Creates a temporary file and copies the contents of the upload to it,
// or, if available, uses the spool file already created by Apache.
// Populates finfo with the file info for the temporary file.
apr_status_t porter_stream_file_to_disk(apr_pool_t *pool, apreq_param_t *p,
                                    apr_finfo_t *finfo,
                                    const char *path)
{
  apr_status_t rv;
  apr_file_t *spool_file = apreq_brigade_spoolfile(p->upload);
  if(spool_file == NULL)
  {
    //The upload brigade is not related to a spool file. Write out to a new file.
    apr_file_t *temp_file;
    apr_off_t length;
    PORTER_HANDLE_ERROR(apreq_file_mktemp(&temp_file, pool, path));
    PORTER_HANDLE_ERROR(apreq_brigade_fwrite(temp_file, &length, p->upload));
    PORTER_HANDLE_ERROR(apr_file_info_get(finfo, APR_FINFO_NORM, temp_file));
  }
  else
  {
    //The upload is already spooled into a file. Use it.
    PORTER_HANDLE_ERROR(apr_file_info_get(finfo, APR_FINFO_NORM, spool_file));
  }
  return APR_SUCCESS;
}

// Appends the sub parameter to the bucket brigade. Sub parameters are given
// 'rails style' with parent_param[sub_param_name]=sub_param_value
apr_status_t porter_append_sub_parameter(apr_pool_t *pool, apr_bucket_brigade *bb,
                                     const char *parent_param,
                                     const char *sub_param,
                                     const char *sub_param_value,
                                     apr_size_t length)
{
  const char *escaped_value = apreq_escape(pool, sub_param_value, length);
  const char *encoded_value = apr_pstrcat(pool, parent_param, "%5B", sub_param, "%5D=", escaped_value, "&", NULL);
  PORTER_LOG("appending sub param");
  PORTER_LOG(sub_param);
  PORTER_LOG(escaped_value);
  return apr_brigade_write(bb, NULL, NULL, encoded_value, strlen(encoded_value));
}
porter_upload_request_t* porter_create_request(apreq_handle_t *req,
                                       request_rec *raw_request,
                                       porter_server_conf *config)
{
  porter_upload_request_t *upload_request  = apr_palloc(req->pool, sizeof(*upload_request));

  upload_request->raw_request    = raw_request;
  upload_request->request        = req;
  upload_request->pool           = req->pool;
  upload_request->param_names    = apr_array_make(req->pool, 1, sizeof(const char*));
  upload_request->status         = APR_SUCCESS;
  upload_request->bucket_brigade = apr_brigade_create(req->pool, req->bucket_alloc);
  upload_request->secret         = config->secret;

  return upload_request;
}


// Apache Config hooks and related functions from here to the end.
static void *porter_create_server_config(apr_pool_t *p, server_rec *s)
{
  porter_server_conf *conf = (porter_server_conf *)apr_pcalloc(p, sizeof(*conf));

  // Disable Porter by default
  conf->enabled = 0;
  conf->minimum_multipart_size = 0;
  conf->permission = 0x0666;

  return conf;
}

static const char* porter_enable_upload(cmd_parms *cmd, void *dir, int argument)
{
  porter_server_conf *config = (porter_server_conf *)ap_get_module_config(cmd->server->module_config, &porter_module);

  config->enabled = argument;
  return NULL;
}

static const char* porter_set_minimum_multipart_size(cmd_parms *cmd, void *dir, const char *argument)
{
  porter_server_conf *config = (porter_server_conf *)ap_get_module_config(cmd->server->module_config, &porter_module);

  const char *error = ap_check_cmd_context(cmd, NOT_IN_LIMIT);

  if (error != NULL) {
    return error;
  }

  config->minimum_multipart_size = apreq_atoi64f(argument);
  return NULL;
}

static const char* porter_set_shared_secret(cmd_parms *cmd, void *dir, const char *argument)
{
  porter_server_conf *config = (porter_server_conf *)ap_get_module_config(cmd->server->module_config, &porter_module);

  const char *error = ap_check_cmd_context(cmd, NOT_IN_LIMIT);

  if (error != NULL) {
    return error;
  }

  config->secret = argument;
  return NULL;
}

static const char* porter_set_file_permission(cmd_parms *cmd, void *dir, const char *argument)
{
  porter_server_conf *config = (porter_server_conf *)ap_get_module_config(cmd->server->module_config, &porter_module);

  const char *error = ap_check_cmd_context(cmd, NOT_IN_LIMIT);

  if (error != NULL) {
    return error;
  }

  config->permission = apr_strtoi64(argument, NULL, 16);
  return NULL;
}

static const char* porter_set_directory(cmd_parms *cmd, void *dir, const char *argument)
{
  porter_server_conf *config = (porter_server_conf *)ap_get_module_config(cmd->server->module_config, &porter_module);

  const char *error = ap_check_cmd_context(cmd, NOT_IN_LIMIT);

  if (error != NULL) {
    return error;
  }

  config->directory = argument;
  return NULL;
}

// FIXME Make sure server doesn't start if PorterSharedSecret is not given and Porter is on
static const command_rec porter_commands[] = {
  AP_INIT_FLAG("Porter", porter_enable_upload, NULL, RSRC_CONF, "Enable or Disable the module. Default : Off") ,
  AP_INIT_TAKE1("PorterMinSize", porter_set_minimum_multipart_size, NULL, RSRC_CONF, "Set mininum content length to kick in parsing. Default : 0"),
  AP_INIT_TAKE1("PorterSharedSecret", porter_set_shared_secret, NULL, RSRC_CONF, "Set shared secret for signing parameters."),
  AP_INIT_TAKE1("PorterPermission", porter_set_file_permission, NULL, RSRC_CONF, "Set permission for temporary files. Default : 0x0666"),
  AP_INIT_TAKE1("PorterDir", porter_set_directory, NULL, RSRC_CONF, "Set directory for temporary files. Default : default apache tmp directory"),
  {NULL}
};

static void porter_register_hooks(apr_pool_t *p)
{
  porter_input_filter_handle = ap_register_input_filter("PORTER_INPUT_FILTER", porter_input_filter, NULL, AP_FTYPE_RESOURCE);
  ap_hook_fixups(porter_fixup, NULL, NULL, APR_HOOK_MIDDLE);
}

/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA porter_module = {
    STANDARD20_MODULE_STUFF,
    NULL,
    NULL,
    porter_create_server_config,
    NULL,
    porter_commands,
    porter_register_hooks
};
