/*******************************************************************************
 mod_smooth_streaming.cpp - An Apache module for streaming Quicktime/MPEG4 files

 Copyright (C) 2007-2026 CodeShop B.V.

 For licensing see the LICENSE file
******************************************************************************/

#include <httpd.h>
#include <http_core.h>
#include <http_config.h>
#include <http_protocol.h>
#include <http_log.h>
#include <http_request.h> // for ap_update_mtime
#include <apr_version.h>
#include <apr_strings.h>  // for apr_itoa, apr_pstrcat
#include <apr_buckets.h>
#include <apr_date.h>
#include <ap_regex.h>

#ifdef _WIN32
#define snprintf _snprintf
#endif

#include <mod_streaming_export.h>
#include <mp4_process.h>
#include <api_process.h>
#include <mp4_rewrite.h>
#include <mp4_error.h>
#include <moov.h>
#include <mp4_options.h>
#include <output_bucket.h>
#include <post_handler.h>
#include <s3_util.h>
#include <headers_t.h>
#include <buckets_t.h>

#include <ap_log.h>
#include <apr_buckets_usp.h>
#include <log_util.h>
#include <subrequest.h>
#define SMOOTH_STREAMING_HANDLER "smooth-streaming.extensions"

#define STRINGIFY(a) STRINGIFY2(a)
#define STRINGIFY2(a) #a

#define MODULE_VERSION STRINGIFY(MOD_SMOOTH_STREAMING_VERSION_MAJOR) "." \
                       STRINGIFY(MOD_SMOOTH_STREAMING_VERSION_MINOR) "." \
                       STRINGIFY(MOD_SMOOTH_STREAMING_VERSION_REVISION)

// Various code below assumes apr_uint64_t and uint64_t can be losslessly cast
// into each other. Check that here before "should not happen" happens.
#include <assert.h>
static_assert(sizeof(apr_uint64_t) == sizeof(uint64_t),
              "apr_uint64_t is not the same size as uint64_t");

#if 0

/* Mod-Smooth-Streaming configuration

[httpd.conf]
LoadModule smooth_streaming_module /usr/lib/apache2/modules/mod_smooth_streaming.so
AddHandler smooth-streaming.extensions .ism .isml

*/
#endif

typedef struct
{
  char const* fake;
  char const* real;
  int in_location;
} proxy_alias_t;

typedef struct
{
  apr_array_header_t* aliases;
  mp4_global_context_t const* global_context;
  char const* license;
} server_config_t;

typedef struct
{
  int usp_enable_mmap;
  int usp_enable_subreq;
  int usp_prefer_static;
  int usp_iss_pass_through;
  int usp_dynamic_time_shift_buffer_depth;
  int usp_skip_rewrite;
  int usp_handle_ism;
  int usp_handle_f4f;
  int usp_handle_api;
  int usp_handle_sitemap;
  int usp_admin_api;
  int usp_handle_smpd;

  int fmp4_iss_fragment_not_found;
  int fmp4_iss_fragment_not_available;

  int fmp4_hds_fragment_not_found;
  int fmp4_hds_fragment_not_available;

  int fmp4_hls_fragment_not_found;
  int fmp4_hls_fragment_not_available;

  int fmp4_mpd_fragment_not_found;
  int fmp4_mpd_fragment_not_available;

  char const* s3_secret_key;
  char const* s3_access_key;
  char const* s3_region;
  char const* s3_security_token;
  int s3_use_headers;

  char const* transcode_proxy_pass;

  ap_expr_info_t const* usp_output_filter;

  int usp_force_content_length;

  int usp_content_id;
} dir_config_t;

static void* create_server_config(apr_pool_t* pool, server_rec* svr);
static void* merge_server_config(apr_pool_t* pool, void* basev, void* addv);
static void* create_dir_config(apr_pool_t* pool, char* dummy);
static void* merge_dir_config(apr_pool_t* pool, void* basev, void* addv);

static char const* set_usp_license_key(cmd_parms* cmd, void* cfg, char const* arg);
static char const* set_ism_proxy_pass(cmd_parms* cmd, void* cfg, char const* arg1, char const* arg2);
static char const* set_output_filter(cmd_parms* cmd, void* cfg, char const* arg);

static void register_hooks(apr_pool_t* p);

typedef struct
{
  char const* name;
  int count;
} deprecated_option_t;

static deprecated_option_t deprecated_options[] =
{
  { "Fmp4IssFragmentMissing", 0 },
  { "Fmp4HdsFragmentMissing", 0 },
  { "Fmp4HlsFragmentMissing", 0 },
  { "Fmp4MpdFragmentMissing", 0 }
};

static char const* deprecated_option(cmd_parms* cmd, void* /* cfg */, char const* /* arg */)
{
  for(size_t i = 0; i < sizeof(deprecated_options)/sizeof(deprecated_options[0]); ++i)
  {
    if(strcmp(cmd->cmd->name, deprecated_options[i].name) == 0)
    {
      ++deprecated_options[i].count;
    }
  }
  return NULL;
}

#if defined(__GNUC__)
// Suppress unavoidable gcc and clang warnings in command_rec initialization.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#endif // __GNUC__

static command_rec const smooth_streaming_cmds[] =
{
  AP_INIT_TAKE1("UspLicenseKey",
    reinterpret_cast<cmd_func>(set_usp_license_key),
    NULL,
    RSRC_CONF|ACCESS_CONF,
    "Set to USP license key"),

  AP_INIT_TAKE12("IsmProxyPass",
    reinterpret_cast<cmd_func>(set_ism_proxy_pass),
    NULL,
    RSRC_CONF|ACCESS_CONF,
    "Pass .ism to proxy (a virtual path and url)"),

  AP_INIT_TAKE1("S3SecretKey",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_secret_key)),
    ACCESS_CONF,
    "Pass the Amazon S3 Secret Key"),

  AP_INIT_TAKE1("S3AccessKey",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_access_key)),
    ACCESS_CONF,
    "Pass the Amazon S3 Access Key)"),

  AP_INIT_TAKE1("S3Region",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_region)),
    ACCESS_CONF,
    "Pass the Amazon S3 Region"),

  AP_INIT_TAKE1("S3SecurityToken",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_security_token)),
    ACCESS_CONF,
    "Pass the Amazon S3 Security Token"),

  AP_INIT_FLAG("S3UseHeaders",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_use_headers)),
    ACCESS_CONF,
    "Set to 'on' to use headers for Amazon S3 authentication"),

  AP_INIT_TAKE1("TranscodeProxyPass",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, transcode_proxy_pass)),
    ACCESS_CONF,
    "Pass transcoding to proxy"),

  AP_INIT_FLAG("UspPreferStatic",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_prefer_static)),
    ACCESS_CONF,
    "Set to 'on' to prefer static files over dynamically generated files"),

  AP_INIT_FLAG("UspIssPassThrough",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_iss_pass_through)),
    ACCESS_CONF,
    "Set to 'on' to pass through MP4 fragments directly to Smooth Streaming"),

  AP_INIT_FLAG("UspDynamicTimeShiftBufferDepth",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_dynamic_time_shift_buffer_depth)),
    ACCESS_CONF,
    "Set to 'on' to dynamically adjust the @timeShiftBufferDepth"),

  AP_INIT_FLAG("UspSkipRewrite",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_skip_rewrite)),
    ACCESS_CONF,
    "Set to 'off' to enable the internal rewrite rules"),

  AP_INIT_FLAG("UspEnableMMAP",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_enable_mmap)),
    ACCESS_CONF,
    "Set to 'off' to disable use of memory-mapping"),

  AP_INIT_FLAG("UspEnableSubreq",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_enable_subreq)),
    ACCESS_CONF,
    "Set to 'on' to enable use of subrequests"),

  // Smooth Streaming
  AP_INIT_FLAG("UspHandleIsm",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_handle_ism)),
    ACCESS_CONF,
    "Set to 'on' to enable mod_smooth_streaming functionality"),

  AP_INIT_FLAG("UspHandleF4f",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_handle_f4f)),
    ACCESS_CONF,
    "Set to 'on' to enable mod_f4fhttp functionality"),

  AP_INIT_FLAG("UspHandleApi",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_handle_api)),
    ACCESS_CONF,
    "Set to 'on' to enable live publishing point api functionality"),

  AP_INIT_FLAG("UspHandleSitemap",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_handle_sitemap)),
    ACCESS_CONF,
    "Set to 'on' to enable live publishing point sitemap functionality"),

  AP_INIT_FLAG("UspAdminApi",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_admin_api)),
    ACCESS_CONF,
    "Set to 'on' to enable live publishing point admin functionality"),

  AP_INIT_FLAG("UspHandleStorageMPD",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_handle_smpd)),
    ACCESS_CONF,
    "Set to 'on' to enable safe HTTP file access to Storage MPD"),

  AP_INIT_TAKE1("Fmp4IssFragmentNotFound",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, fmp4_iss_fragment_not_found)),
    ACCESS_CONF,
    "HTTP code (404) returned when a fragment is requested before the DVR window (too old)"),

  AP_INIT_TAKE1("Fmp4IssFragmentMissing",
    reinterpret_cast<cmd_func>(deprecated_option),
    NULL,
    ACCESS_CONF,
    "Deprecated: HTTP code (412) returned when a fragment is missing in the DVR window"),

  AP_INIT_TAKE1("Fmp4IssFragmentNotAvailable",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, fmp4_iss_fragment_not_available)),
    ACCESS_CONF,
    "HTTP code (412) returned when a fragment is requested after the DVR window (too new)"),

  // HTTP Dynamic Streaming
  AP_INIT_TAKE1("Fmp4HdsFragmentNotFound",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, fmp4_hds_fragment_not_found)),
    ACCESS_CONF,
    "HTTP code (404) returned when a fragment is requested before the DVR window (too old)"),

  AP_INIT_TAKE1("Fmp4HdsFragmentMissing",
    reinterpret_cast<cmd_func>(deprecated_option),
    NULL,
    ACCESS_CONF,
    "Deprecated: HTTP code (503) returned when a fragment is missing in the DVR window"),

  AP_INIT_TAKE1("Fmp4HdsFragmentNotAvailable",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, fmp4_hds_fragment_not_available)),
    ACCESS_CONF,
    "HTTP code (503) returned when a fragment is requested after the DVR window (too new)"),

  // HTTP Live Streaming
  AP_INIT_TAKE1("Fmp4HlsFragmentNotFound",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, fmp4_hls_fragment_not_found)),
    ACCESS_CONF,
    "HTTP code (404) returned when a fragment is requested before the DVR window (too old)"),

  AP_INIT_TAKE1("Fmp4HlsFragmentMissing",
    reinterpret_cast<cmd_func>(deprecated_option),
    NULL,
    ACCESS_CONF,
    "Deprecated: HTTP code (503) returned when a fragment is missing in the DVR window"),

  AP_INIT_TAKE1("Fmp4HlsFragmentNotAvailable",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, fmp4_hls_fragment_not_available)),
    ACCESS_CONF,
    "HTTP code (503) returned when a fragment is requested after the DVR window (too new)"),

  // MPEG DASH Streaming
  AP_INIT_TAKE1("Fmp4MpdFragmentNotFound",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, fmp4_mpd_fragment_not_found)),
    ACCESS_CONF,
    "HTTP code (404) returned when a fragment is requested before the DVR window (too old)"),

  AP_INIT_TAKE1("Fmp4MpdFragmentMissing",
    reinterpret_cast<cmd_func>(deprecated_option),
    NULL,
    ACCESS_CONF,
    "Deprecated: HTTP code (503) returned when a fragment is missing in the DVR window"),

  AP_INIT_TAKE1("Fmp4MpdFragmentNotAvailable",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, fmp4_mpd_fragment_not_available)),
    ACCESS_CONF,
    "HTTP code (503) returned when a fragment is requested after the DVR window (too new)"),

  AP_INIT_TAKE1("UspOutputFilter",
    reinterpret_cast<cmd_func>(set_output_filter),
    NULL,
    ACCESS_CONF,
    "Filter output through an external command"),

  AP_INIT_FLAG("UspForceContentLength",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_force_content_length)),
    ACCESS_CONF,
    "Set to 'on' to force determining Content-Length even for dynamic content"),

  AP_INIT_FLAG("UspContentId",
    reinterpret_cast<cmd_func>(ap_set_flag_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, usp_content_id)),
    ACCESS_CONF,
    "Set to 'on' to add X-USP-Content-ID header"),

  { NULL }
};

#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif // __GNUC__

extern "C" {

#define usp_module_name smooth_streaming_module

struct module_struct MOD_STREAMING_DLL_EXPORT usp_module_name =
{
  STANDARD20_MODULE_STUFF,
  create_dir_config,
  merge_dir_config,
  create_server_config,
  merge_server_config,
  smooth_streaming_cmds,
  register_hooks,
  AP_MODULE_FLAG_ALWAYS_MERGE
};

} // extern C definitions

static void* create_server_config(apr_pool_t* pool, server_rec* /* svr */)
{
  auto* config = static_cast<server_config_t*>(apr_pcalloc(pool, sizeof(server_config_t)));

  config->aliases = apr_array_make(pool, 10, sizeof(proxy_alias_t));
  config->global_context = NULL;
  config->license = NULL;

  return config;
}

static int merge_int(int base, int add)
{
  return add == -1 ? base : add;
}

static char const* merge_str(char const* base, char const* add)
{
  return add == NULL ? base : add;
}

static void* merge_server_config(apr_pool_t* pool, void* basev, void* addv)
{
  auto* base = static_cast<server_config_t*>(basev);
  auto* add = static_cast<server_config_t*>(addv);
  auto* res = static_cast<server_config_t*>(
    apr_pcalloc(pool, sizeof(server_config_t)));

  res->aliases = apr_array_append(pool, base->aliases, add->aliases);
  res->license = merge_str(base->license, add->license);

  return res;
}

static void* create_dir_config(apr_pool_t* pool, char* /* dummy */)
{
  auto* config = static_cast<dir_config_t*>(apr_pcalloc(pool, sizeof(dir_config_t)));

  config->usp_enable_mmap = -1;
  config->usp_enable_subreq = -1;
  config->usp_prefer_static = -1;
  config->usp_iss_pass_through = -1;
  config->usp_dynamic_time_shift_buffer_depth = -1;
  config->usp_skip_rewrite = -1;
  config->usp_handle_ism = -1;
  config->usp_handle_f4f = -1;
  config->usp_handle_api = -1;
  config->usp_handle_sitemap = -1;
  config->usp_admin_api = -1;
  config->usp_handle_smpd = -1;

  config->fmp4_iss_fragment_not_found = -1;
  config->fmp4_iss_fragment_not_available = -1;

  config->fmp4_hds_fragment_not_found = -1;
  config->fmp4_hds_fragment_not_available = -1;

  config->fmp4_hls_fragment_not_found = -1;
  config->fmp4_hls_fragment_not_available = -1;

  config->fmp4_mpd_fragment_not_found = -1;
  config->fmp4_mpd_fragment_not_available = -1;

  config->s3_secret_key = NULL;
  config->s3_access_key = NULL;
  config->s3_region = NULL;
  config->s3_security_token = NULL;
  config->s3_use_headers = -1;

  config->transcode_proxy_pass = NULL;

  config->usp_output_filter = NULL;

  config->usp_force_content_length = -1;

  config->usp_content_id = -1;

  return config;
}

static void* merge_dir_config(apr_pool_t* pool, void* basev, void* addv)
{
  auto* base = static_cast<dir_config_t*>(basev);
  auto* add = static_cast<dir_config_t*>(addv);
  auto* res = static_cast<dir_config_t*>(apr_pcalloc(pool, sizeof(dir_config_t)));

  res->usp_enable_mmap = merge_int(base->usp_enable_mmap, add->usp_enable_mmap);
  res->usp_enable_subreq = merge_int(base->usp_enable_subreq, add->usp_enable_subreq);
  res->usp_prefer_static = merge_int(base->usp_prefer_static, add->usp_prefer_static);
  res->usp_iss_pass_through = merge_int(base->usp_iss_pass_through, add->usp_iss_pass_through);
  res->usp_dynamic_time_shift_buffer_depth = merge_int(base->usp_dynamic_time_shift_buffer_depth, add->usp_dynamic_time_shift_buffer_depth);
  res->usp_skip_rewrite = merge_int(base->usp_skip_rewrite, add->usp_skip_rewrite);
  res->usp_handle_ism = merge_int(base->usp_handle_ism, add->usp_handle_ism);
  res->usp_handle_f4f = merge_int(base->usp_handle_f4f, add->usp_handle_f4f);
  res->usp_handle_api = merge_int(base->usp_handle_api, add->usp_handle_api);
  res->usp_handle_sitemap = merge_int(base->usp_handle_sitemap, add->usp_handle_sitemap);
  res->usp_admin_api = merge_int(base->usp_admin_api, add->usp_admin_api);
  res->usp_handle_smpd = merge_int(base->usp_handle_smpd, add->usp_handle_smpd);

  res->fmp4_iss_fragment_not_found = merge_int(base->fmp4_iss_fragment_not_found, add->fmp4_iss_fragment_not_found);
  res->fmp4_iss_fragment_not_available = merge_int(base->fmp4_iss_fragment_not_available, add->fmp4_iss_fragment_not_available);
  res->fmp4_hds_fragment_not_found = merge_int(base->fmp4_hds_fragment_not_found, add->fmp4_hds_fragment_not_found);
  res->fmp4_hds_fragment_not_available = merge_int(base->fmp4_hds_fragment_not_available, add->fmp4_hds_fragment_not_available);
  res->fmp4_hls_fragment_not_found = merge_int(base->fmp4_hls_fragment_not_found, add->fmp4_hls_fragment_not_found);
  res->fmp4_hls_fragment_not_available = merge_int(base->fmp4_hls_fragment_not_available, add->fmp4_hls_fragment_not_available);
  res->fmp4_mpd_fragment_not_found = merge_int(base->fmp4_mpd_fragment_not_found, add->fmp4_mpd_fragment_not_found);
  res->fmp4_mpd_fragment_not_available = merge_int(base->fmp4_mpd_fragment_not_available, add->fmp4_mpd_fragment_not_available);

  res->s3_secret_key = merge_str(base->s3_secret_key, add->s3_secret_key);
  res->s3_access_key = merge_str(base->s3_access_key, add->s3_access_key);
  res->s3_region = merge_str(base->s3_region, add->s3_region);
  res->s3_security_token = merge_str(base->s3_security_token, add->s3_security_token);
  res->s3_use_headers = merge_int(base->s3_use_headers, add->s3_use_headers);

  res->transcode_proxy_pass = merge_str(base->transcode_proxy_pass, add->transcode_proxy_pass);

  res->usp_output_filter = add->usp_output_filter == NULL ? base->usp_output_filter : add->usp_output_filter;

  res->usp_force_content_length = merge_int(base->usp_force_content_length, add->usp_force_content_length);

  res->usp_content_id = merge_int(base->usp_content_id, add->usp_content_id);

  return res;
}

static int remap_status(fmp4_result result, int http_status, dir_config_t const* conf)
{
  int new_status = -1;

  switch(result)
  {
    case FMP4_ISS_FRAGMENT_NOT_FOUND:
      new_status = conf->fmp4_iss_fragment_not_found;
      break;
    case FMP4_ISS_FRAGMENT_NOT_YET_AVAILABLE:
      new_status = conf->fmp4_iss_fragment_not_available;
      break;
    case FMP4_HDS_FRAGMENT_NOT_FOUND:
      new_status = conf->fmp4_hds_fragment_not_found;
      break;
    case FMP4_HDS_FRAGMENT_NOT_YET_AVAILABLE:
      new_status = conf->fmp4_hds_fragment_not_available;
      break;
    case FMP4_HLS_FRAGMENT_NOT_FOUND:
      new_status = conf->fmp4_hls_fragment_not_found;
      break;
    case FMP4_HLS_FRAGMENT_NOT_YET_AVAILABLE:
      new_status = conf->fmp4_hls_fragment_not_available;
      break;
    case FMP4_MPD_FRAGMENT_NOT_FOUND:
      new_status = conf->fmp4_mpd_fragment_not_found;
      break;
    case FMP4_MPD_FRAGMENT_NOT_YET_AVAILABLE:
      new_status = conf->fmp4_mpd_fragment_not_available;
      break;
    default:
      break;
  }

  return new_status != -1 ? new_status : http_status;
}

static apr_status_t cleanup_libfmp4(void* data)
{
  auto const* global_context = static_cast<mp4_global_context_t const*>(data);
  libfmp4_global_exit(global_context);

  return APR_SUCCESS;
}

static int smooth_streaming_post_config(apr_pool_t* p, apr_pool_t* /* plog */,
                                        apr_pool_t* /* ptemp */, server_rec* s)
{
  char const* src = "apache mod_smooth_streaming";
  char const* version = X_MOD_SMOOTH_STREAMING_VERSION;

  mp4_global_context_t* global_context = NULL;
  server_config_t* conf = NULL;
  char const* license = NULL;
  void* tmp = NULL;
  char const* key = "mp4_global_context";

  ap_add_version_component(p, "USP/" MODULE_VERSION);
  ap_add_version_component(p, "IISMS/4.0");

#if 1 // prevent double initialization
  apr_pool_userdata_get(&tmp, key, s->process->pool);
  if(tmp == NULL)
  {
    apr_pool_userdata_set(reinterpret_cast<void const*>(1), key,
                          apr_pool_cleanup_null, s->process->pool);
    return OK;
  }
#endif

  global_context = libfmp4_global_init();
  apr_pool_cleanup_register(p, reinterpret_cast<void const*>(global_context),
                            cleanup_libfmp4, apr_pool_cleanup_null);

  for (; s; s = s->next)
  {
    conf = static_cast<server_config_t*>(
      ap_get_module_config(s->module_config, &usp_module_name));

    conf->global_context = global_context; // set pointer

    if(conf->license) // find license key
    {
      license = conf->license;
    }
  }

  char const* policy_check_result =
    libfmp4_load_license(global_context, src, version, license);

  if(policy_check_result != NULL)
  {
    ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, "%s", policy_check_result);
  }
  else if(license)
  {
    ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, s,
                 "License key found: %s", license);
  }

  for(size_t i = 0; i < sizeof(deprecated_options)/sizeof(deprecated_options[0]); ++i)
  {
    if(deprecated_options[i].count)
    {
      ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, s,
                   "Use of DEPRECATED option %s (ignored)",
                   deprecated_options[i].name);
    }
  }

  return OK;
}

static fmp4_http_method_t get_http_method(request_rec* r)
{
  switch(r->method_number)
  {
  case M_GET:  return FMP4_HTTP_GET;
  case M_POST: return FMP4_HTTP_POST;
  case M_PUT: return FMP4_HTTP_PUT;
  case M_DELETE: return FMP4_HTTP_DELETE;
  default:
    return static_cast<fmp4_http_method_t>(r->method_number);
  }
}

static int map_apr_status_to_http(apr_status_t status)
{
  // Can't use a switch because of the APR_STATUS_IS_xxx() macros
  if(status == APR_SUCCESS)
  {
    return HTTP_OK;
  }
  else if(APR_STATUS_IS_TIMEUP(status))
  {
    return HTTP_REQUEST_TIME_OUT;
  }
  else if(ap_is_HTTP_VALID_RESPONSE(status))
  {
    return status;
  }
  else
  {
    return HTTP_INTERNAL_SERVER_ERROR;
  }
}

static apr_status_t
remove_output_filter(ap_filter_t* next, ap_filter_rec_t const* filter)
{
  for(; next; next = next->next)
  {
    if(next->frec == filter)
    {
      ap_remove_output_filter(next);
      return APR_SUCCESS;
    }
  }

  return APR_NOTFOUND;
}

static char const prefix_ismproxy[] = "ismproxy:";
static size_t const prefix_ismproxy_len = sizeof prefix_ismproxy - 1;

static char const prefix_usp_admin[] = "usp-admin:";
static size_t const prefix_usp_admin_len = sizeof prefix_usp_admin - 1;

static int
drive_smooth_streaming_get(request_rec* r, dir_config_t const* dir_conf,
                           mp4_process_context_t* context,
                           fmp4_http_method_t http_method,
                           int usp_handle_api,
                           int usp_handle_sitemap)
{
  apr_status_t rv = APR_SUCCESS;
  int http_status;
  headers_t* headers;
  buckets_t* buckets;
  uint64_t content_length = 0;
  mp4_options_t const* options = mp4_process_context_get_options(context);
  char const* file = mp4_options_get_file(options);
  char const* filename = r->filename;

  ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                "smooth_streaming_handler filename=%s file=%s",
                filename, file);

  // If we are requesting the server manifest file, then it may an encoding tool
  // that is using the GET request to get the authorization setup for the POST
  // or it's simply checking that the publishing point exists.
  if(mp4_ends_with(filename, ".isml"))
  {
    if(mp4_starts_with(file, "Streams("))
    {
      return HTTP_OK;
    }
  }

  if(strncmp(filename, prefix_ismproxy, prefix_ismproxy_len) == 0)
  {
    filename = filename + prefix_ismproxy_len;
  }

  if(strncmp(filename, prefix_usp_admin, prefix_usp_admin_len) == 0)
  {
    filename = filename + prefix_usp_admin_len;
    http_status = mp4_admin_api(context, http_method, filename);
  }
  else if(usp_handle_api)
  {
    http_status = mp4_channel_api(context, http_method, filename, r->path_info);
  }
  else if(usp_handle_sitemap)
  {
    http_status = mp4_sitemap_api(context, http_method, filename, r->path_info);
  }
  else
  {
    http_status = mp4_process(context, http_method, filename);
  }

  headers = mp4_process_context_get_headers(context);
  buckets = mp4_process_context_get_buckets(context);

  char const* x_usp_header1 = headers_get_x_usp_header1(headers);
  if(x_usp_header1 && x_usp_header1[0])
  {
    apr_table_set(r->err_headers_out, "X-USP-Info1", x_usp_header1);
  }
  char const* x_usp_header2 = headers_get_x_usp_header2(headers);
  if(x_usp_header2 && x_usp_header2[0])
  {
    apr_table_set(r->err_headers_out, "X-USP-Info2", x_usp_header2);
  }

  // Check if we already know the content length.
  content_length = buckets_size(buckets);
  if(content_length != UINT64_MAX)
  {
    TRACE1_R(r, "content_length is %" APR_UINT64_T_FMT
                ", no need to establish buckets size",
                static_cast<apr_uint64_t>(content_length));
  }
  else if(dir_conf->usp_force_content_length == 1 ||
          (mp4_process_context_get_output_filter(context) != NULL &&
           r->proto_num < HTTP_VERSION(1,1)))
  {
    // In these special cases:
    // 1) UspForceContentLength is on, or
    // 2) Using an output_filter in combination with HTTP/1.0, such as with
    //    ApacheBench
    // establish the buckets size now, to get the actual content length.
    // NOTE: this can possibly read large amounts of data into memory.
    TRACE1_R(r, "establishing buckets size due to %s",
                dir_conf->usp_force_content_length == 1 ?
                  "UspForceContentLength" : "output_filter with HTTP/1.0");
    fmp4_result result = buckets_establish_size(buckets, &content_length);
    if(result != FMP4_OK)
    {
      ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                    "smooth_streaming_handler: failed to establish buckets "
                    "size (%s)", fmp4_result_to_string(result));
      return HTTP_INTERNAL_SERVER_ERROR;
    }
    TRACE1_R(r, "established buckets size: %" APR_UINT64_T_FMT,
                static_cast<apr_uint64_t>(content_length));
  }
  else
  {
    TRACE1_R(r, "content_length is unknown, but not going to establish buckets "
                "size");
  }

  // If we know the content length then we can pass it in the header.
  if(content_length != UINT64_MAX)
  {
    ap_set_content_length(r, content_length);

    // Allow byte range requests. The ap_byterange_filter only works on
    // brigades containing an EOS and no buckets with unknown length.
    apr_table_setn(r->headers_out, "Accept-Ranges", "bytes");
  }

  // Add Last-Modified
  uint64_t updated_at = headers_get_updated_at(headers);
  if(updated_at)
  {
    ap_update_mtime(r, updated_at);
    ap_set_last_modified(r);
    TRACE1_R(r,
      "updated mtime to %" APR_UINT64_T_FMT ", set Last-Modified to %s",
      updated_at, apr_table_get(r->headers_out, "Last-Modified"));
  }

  // Add X-USP-Last-Modified
  uint64_t updated_at_experimental = headers_get_updated_at_experimental(headers);
  if(updated_at_experimental)
  {
    apr_time_t last_modified = apr_time_sec(updated_at_experimental);
    auto* timestr = static_cast<char*>(apr_palloc(r->pool, APR_RFC822_DATE_LEN));
    apr_rfc822_date(timestr, apr_time_from_sec(last_modified));
    apr_table_setn(r->headers_out, "X-USP-Last-Modified", timestr);
    TRACE1_R(r, "set X-USP-Last-Modified to %s", timestr);
  }

  // Add Cache-Control and Expires
  char const* cache_control = headers_get_cache_control(headers);
  apr_time_t expires_at = headers_get_expires_at(headers);
  if(cache_control && cache_control[0])
  {
    apr_table_setn(r->headers_out, "Cache-Control", cache_control);
  }
  else if(expires_at)
  {
    // compute expiry with seconds accuracy for interoperable results with nginx
    apr_time_t expires_time = apr_time_sec(expires_at);
    apr_time_t request_time = apr_time_sec(r->request_time);

    // use expiry from backend only if it's at least a second ahead of request
    // otherwise, we skip insertion of Expires/Cache-Control and leave this up
    // to the customer (see: mod_headers, mod_expires)
    if(expires_time < request_time + 1)
    {
      expires_time = request_time + 1;
    }

    apr_time_t max_age = expires_time - request_time;
    apr_table_setn(r->headers_out, "Cache-Control",
      apr_psprintf(r->pool, "max-age=%" APR_TIME_T_FMT, max_age));

    apr_time_t max_age_ms = expires_at >= r->request_time ?
      (expires_at - r->request_time) / 1000 : 0;
    apr_table_setn(r->headers_out, "USP-Cache-Control",
      apr_psprintf(r->pool, "max-age-ms=%" APR_TIME_T_FMT, max_age_ms));

    auto* timestr = static_cast<char*>(apr_palloc(r->pool, APR_RFC822_DATE_LEN));
    apr_rfc822_date(timestr, apr_time_from_sec(expires_time));
    apr_table_setn(r->headers_out, "Expires", timestr);
  }

  // Add ETag
  if(cache_control != cache_control_no_cache)
  {
    char const* etag = headers_get_etag(headers);
    if(etag && etag[0])
    {
      apr_table_setn(r->headers_out, "ETag", etag);
    }
    else
    {
      // The ETag takes into account the last modified time (r->mtime). Since
      // for dynamically generated content we set the (high resolution)
      // updated_at as the last modified time, we can simply generate an ETag
      // and do nothing special.
      ap_set_etag(r);
    }
  }

  uint64_t sunset_at = headers_get_sunset_at(headers);
  if(sunset_at)
  {
    apr_time_t sunset_time = apr_time_sec(sunset_at);
    auto* timestr = static_cast<char*>(apr_palloc(r->pool, APR_RFC822_DATE_LEN));
    apr_rfc822_date(timestr, apr_time_from_sec(sunset_time));
    apr_table_setn(r->headers_out, "Sunset", timestr);
  }

  // Add Link: header for start/next
  char const* link = headers_get_link(headers);
  if(link && link[0])
  {
    apr_table_setn(r->headers_out, "Link", link);
  }

  // Add Location
  char const* location = headers_get_location(headers);
  if(location && location[0])
  {
    apr_table_setn(r->headers_out, "Location", location);
  }

  // Add X-USP-Content-ID
  char const* content_id = headers_get_content_id(headers);
  if(content_id && content_id[0])
  {
    apr_table_setn(r->headers_out, "X-USP-Content-ID", content_id);
  }

  // Add CMSD-Static
  char const* cmsd_static = headers_get_cmsd_static(headers);
  if(cmsd_static)
  {
    apr_table_setn(r->headers_out, "CMSD-Static", cmsd_static);
  }

  // Add CMSD-Dynamic
  char const* cmsd_dynamic = headers_get_cmsd_dynamic(headers);
  if(cmsd_dynamic)
  {
    apr_table_setn(r->headers_out, "CMSD-Dynamic", cmsd_dynamic);
  }

  if(http_status != HTTP_OK)
  {
    if(!usp_handle_api && !usp_handle_sitemap)
    {
      http_status = remap_status(
        mp4_process_context_get_result(context), http_status, dir_conf);
    }

    r->status = http_status;

    return OK;
  }

  // Check for conditional requests
  {
    int errstatus;
    if((errstatus = ap_meets_conditions(r)) != OK)
    {
      // For the HTTP_NOT_MODIFIED case set r->status and return OK in order to
      // prevent running through the error processing stack.
      if(errstatus == HTTP_NOT_MODIFIED)
      {
        ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r,
          "smooth_streaming_handler: not modified, setting status %d",
          errstatus);
        r->status = errstatus;
        return OK;
      }

      ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
        "smooth_streaming_handler: ap_meets_conditions returned status %d",
        errstatus);
      return errstatus;
    }
  }

  // Set the content type
  ap_set_content_type(r, headers_get_content_type(headers));

  if(!r->header_only)
  {
    apr_bucket_brigade* bb;
    bucket_t* head = buckets->bucket_;
    bucket_t* bucket = bucket_next(head);

    if(content_length == UINT64_MAX)
    {
      if(r->proto_num >= HTTP_VERSION(1,1))
      {
        // if we are transforming the buckets dynamically, and we don't know the
        // size yet, so switch to "Transfer-Encoding: chunked".
        r->chunked = 1;

        // we have to remove the core content-length filter. It will never set
        // the Content-Length anyways, but it always iterates over the complete
        // brigate (and as a side-effect evaluates all the xfrm buckets which
        // may read large amounts).
        remove_output_filter(r->output_filters, ap_content_length_filter_handle);
      }
      else
      {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "smooth_streaming_handler: unknown content length with "
                      "HTTP < 1.1, failing request");

        return HTTP_VERSION_NOT_SUPPORTED;
      }
    }

    bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);

    while(bucket != head)
    {
      apr_bucket* e;

      bucket_t* next = bucket_next(bucket);
      bucket_remove(bucket);
      e = apr_bucket_usp_create(bucket, bb->bucket_alloc, r);
      APR_BRIGADE_INSERT_TAIL(bb, e);

      bucket = next;
    }

    // Add EOS bucket
    APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_eos_create(bb->bucket_alloc));

    TRACE1_R(r, "calling ap_pass_brigade");
    rv = ap_pass_brigade(r->output_filters, bb);
    TRACE1_R(r, "called ap_pass_brigade, rv=%d", rv);

    if(r->connection->aborted)
    {
      char errmsg[120];
      if(APR_STATUS_IS_EPIPE(rv))
      {
        // "Broken pipe" is too confusing, so change this error into a more
        // informative message.
        apr_snprintf(errmsg, sizeof errmsg, "%s",
          "client terminated connection abruptly");
      }
      else
      {
        apr_strerror(rv, errmsg, sizeof errmsg);
      }
      ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r,
        "smooth_streaming_handler: connection aborted: %s", errmsg);

      // We want to log a 4xx client error instead of a 5xx server error, since
      // it is not the server's fault and there is nothing we can do to "fix" it
      // from our side. nginx uses "499 Client Closed Request" for this case, so
      // imitate their behavior.
      return 499;
    }

    if(rv != APR_SUCCESS)
    {
      char errmsg[120];
      apr_strerror(rv, errmsg, sizeof errmsg);
      ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                    "smooth_streaming_handler: ap_pass_brigade failed: %s",
                    errmsg);

      if(r->chunked)
      {
        // Assuming the status line has already been sent, we force a protocol
        // error to ensure the client at least notices *some* kind of failure.
        // See modules/http/chunk_filter.c in the Apache source.
        //
        // With curl as a client, this results in an error "curl: (18) transfer
        // closed with outstanding read data remaining"

        apr_brigade_cleanup(bb);
        APR_BRIGADE_INSERT_TAIL(bb, ap_bucket_error_create(
          HTTP_BAD_GATEWAY, NULL, r->pool, bb->bucket_alloc));
        APR_BRIGADE_INSERT_TAIL(bb, apr_bucket_eos_create(bb->bucket_alloc));

        TRACE1_R(r, "calling ap_pass_brigade with error bucket");
        rv = ap_pass_brigade(r->output_filters, bb);
        TRACE1_R(r, "called ap_pass_brigade with error bucket, rv=%d", rv);

        // We need to return something else than HTTP_INTERNAL_SERVER_ERROR or
        // OK here: with the former, Apache will helpfully append a "the server
        // has encountered an internal error" document to the output, and with
        // the latter, the access log will pretend that the request was fine
        // (http status 200).
        return DONE;
      }

      return map_apr_status_to_http(rv);
    }
  }
  else
  {
    TRACE1_R(r, "header only, not processing buckets");
  }

  return OK;
}

static int
drive_smooth_streaming_post(request_rec* r, mp4_process_context_t* context,
                            fmp4_http_method_t http_method, int is_rest_api)
{
  apr_size_t chunk_size = 8 * 1024;
  mp4_options_t const* options = mp4_process_context_get_options(context);
  char const* file = mp4_options_get_file(options);
  char const* filename = r->filename;

  if(!mp4_ends_with(filename, ".isml") && !is_rest_api)
  {
    return HTTP_FORBIDDEN;
  }

  // Connect
  if(!file[0] && !is_rest_api)
  {
    return 0;
  }

  // Pushing to Streams(...), disable api
  if(file[0] && is_rest_api)
  {
    is_rest_api = 0;
  }

  if(strncmp(filename, prefix_ismproxy, prefix_ismproxy_len) == 0)
  {
    filename = filename + prefix_ismproxy_len;
  }

  if(!strcmp(file, "purge"))
  {
    int http_status = mp4_process(context, http_method, filename);

    if(http_status != HTTP_OK)
    {
      return http_status;
    }

    return OK;
  }

  {
    int http_status = HTTP_OK;
    auto* buf = static_cast<char*>(apr_palloc(r->pool, chunk_size));

    apr_bucket_brigade* bb =
      apr_brigade_create(r->pool, r->connection->bucket_alloc);

    post_handler_t* post_handler = is_rest_api ?
      create_post_handler_api(context, http_method, filename, r->path_info) :
        create_post_handler(context, filename);

    if(!post_handler)
    {
      char const* result_text = mp4_process_context_get_result_text(context);
      if(!result_text || !result_text[0])
      {
        auto result = mp4_process_context_get_result(context);
        result_text = fmp4_result_to_string(result);
      }
      ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                    "usp_post_handler returned: %s", result_text);

      http_status = HTTP_INTERNAL_SERVER_ERROR;
    }

    if(http_status == HTTP_OK)
    {
      for(;;)
      {
        apr_size_t len = chunk_size;

        // If you get an error similar to:
        // (70007)The timeout specified has expired: Error reading chunk this
        // means that we didn't receive any data for the timeout specified.
        // Note that the timeout value may also be set by mod_reqtimeout and
        // the default setting may be quite agressive (10 seconds).
        // Note also that setting the RequestReadTimeout in a virtual host to
        // override the global settings, does *not* work. You have to set the
        // global RequestReadTimeout to a larger timeout value, or disable it
        // completely with:
        //   RequestReadTimeout header=0 body=0
        //
        // Also make sure to include in your (vhost) configuration the
        // following:
        //   LimitRequestBody 0

        apr_status_t status = ap_get_brigade(r->input_filters, bb,
                                             AP_MODE_READBYTES,
                                             APR_BLOCK_READ, len);

        if(status != APR_SUCCESS)
        {
          apr_brigade_destroy(bb);
          ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
                        "mod_smooth_streaming(%s): ap_get_brigade",
                        file);
          http_status = map_apr_status_to_http(status);
          break;
        }

        status = apr_brigade_flatten(bb, buf, &len);
        if(status != APR_SUCCESS)
        {
          apr_brigade_destroy(bb);
          ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
                        "mod_smooth_streaming(%s): apr_brigade_flatten",
                        file);
          http_status = map_apr_status_to_http(status);
          break;
        }

        apr_brigade_cleanup(bb);

#if 0 && defined(_DEBUG)
        printf("mod_smooth_streaming.cpp(%s): len=%" APR_SIZE_T_FMT "\n",
               r->args ? r->args : "", len);
#endif

#if 1
        char result_text[256] = { 0 };

        if(post_handler)
        {
          ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
              "ingest stream %s (%" APR_SIZE_T_FMT " bytes)", file, len);
          http_status = post_handler_insert(post_handler,
            (unsigned char*)buf, (unsigned char*)buf + len,
            result_text, sizeof(result_text) / sizeof(result_text[0]));
        }

        if(!ap_is_HTTP_SUCCESS(http_status))
        {
          if(result_text[0])
          {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
              "usp_post_handler(%s) returned: %u %s",
              file,
              http_status, result_text);
          }

          // abort the connection in case we are receiving the POST data as
          // chunked. Unfortunately we lose the original http_status
          // code and Apache always returns a 500 Internal Server Error.
          if(len)
          {
            http_status = HTTP_INTERNAL_SERVER_ERROR;
          }

          break;
        }
#endif

        if(len == 0)
        {
          break;
        }
      }
    }

    if(post_handler)
    {
      TRACE1_R(r, "calling post_handler_exit");
      post_handler_exit(post_handler);
      TRACE1_R(r, "called post_handler_exit");
    }

    TRACE1_R(r, "http_status=%d", http_status);
    return http_status == HTTP_OK ? OK : http_status;
  }
}

static int handle_ism_proxy(request_rec* r, int apply)
{
  // check or apply ism_proxy_pass
  const char* prefix = apply ? "apply" : "check";
  auto* conf = static_cast<server_config_t*>(
    ap_get_module_config(r->server->module_config, &usp_module_name));
  int i;
  auto* ent = reinterpret_cast<proxy_alias_t*>(conf->aliases->elts);
  for (i = 0; i < conf->aliases->nelts; i++)
  {
    const char* fake = ent[i].fake;
    const char* real = ent[i].real;
    int in_location = ent[i].in_location;

    DEBUG_R(r, "%s_ism_proxy(step1) fake=%s real=%s in_location=%d uri=%s "
      "filename=%s", prefix, fake, real, in_location, r->uri, r->filename);

    // filename: /var/www/test/ism_proxy_pass.ssm/ism proxy pass.ism
    //     fake: /var/www/test/ism_proxy_pass.ssm/
    // OR
    //      uri: /ism_proxy_pass.ssm/ism proxy pass.ism
    //     fake: /ism_proxy_pass.ssm/
    //
    //     real: http://localhost:81/test/ism_proxy_pass.ssm/
    //    final: ismproxy:http://localhost:81/test/ism_proxy_pass.ssm/ism%20proxy%20pass.ism
    char const* to_match = in_location ? r->uri : r->filename;
    if(mp4_starts_with(to_match, fake))
    {
      char* escaped = ap_escape_uri(r->pool, to_match + strlen(fake));
      char* final = apr_psprintf(r->pool, "%s%s%s", prefix_ismproxy, real, escaped);
      DEBUG_R(r, "%s_ism_proxy(step2) final=%s", prefix, final);
      if(apply)
      {
        r->filename = final;
      }
      return OK;
    }
  }

  TRACE1_R(r, "%s_ism_proxy: declined", prefix);
  return DECLINED;
}

static int admin_api_translate_name(request_rec* r)
{
  // the admin API is all virtual URLs, nothing file based.
  r->handler = SMOOTH_STREAMING_HANDLER;
  r->filename = apr_psprintf(r->pool, "%s%s", prefix_usp_admin, r->uri);
  return OK;
}

static int channel_api_translate_name(request_rec* r)
{
  // the channel REST API, always backed by a server manifest. Apache stops
  // translation on non existent directory and stores the rest of the URL's path
  // in r->path_info.
  r->handler = SMOOTH_STREAMING_HANDLER;
  int res = ap_core_translate(r);
  if(res != OK || !r->filename)
  {
    DEBUG_R(r, "channel_api_translate_name: core translate failed, return %d",
      res);
    return res;
  }

  return DECLINED;
}

static int sitemap_translate_name(request_rec* r)
{
  r->handler = SMOOTH_STREAMING_HANDLER;
  int res = ap_core_translate(r);
  if(res != OK || !r->filename)
  {
    DEBUG_R(r, "sitemap_translate_name: core translate failed, return %d", res);
    return res;
  }

  return DECLINED;
}

static int smooth_streaming_translate_name(request_rec* r)
{
  int res = OK;

  auto* conf = static_cast<dir_config_t*>(
    ap_get_module_config(r->per_dir_config, &usp_module_name));

  int usp_handle_ism = merge_int(0, conf->usp_handle_ism);
  int usp_handle_f4f = merge_int(0, conf->usp_handle_f4f);
  int usp_handle_api = merge_int(0, conf->usp_handle_api);
  int usp_admin_api = merge_int(0, conf->usp_admin_api);
  int usp_handle_smpd = merge_int(0, conf->usp_handle_smpd);
  int usp_handle_sitemap = merge_int(0, conf->usp_handle_sitemap);

  // the admin API is all virtual URLs, nothing file based.
  if(usp_admin_api)
  {
    return admin_api_translate_name(r);
  }

  if(usp_handle_api)
  {
    return channel_api_translate_name(r);
  }

  if(usp_handle_sitemap)
  {
    return sitemap_translate_name(r);
  }

  // Check if we are enabled for this location
  if(!(usp_handle_ism || usp_handle_f4f || usp_handle_smpd))
  {
    TRACE1_R(r, "translate_name: declined, neither ism nor f4f handler");
    return DECLINED;
  }

  // from here on out, our main handler should pick this up. We may have older
  // configurations where UspHandleIsm is enabled for a "/" Location, so we
  // only set the handler when rewriting took place.
  if(usp_handle_f4f || usp_handle_smpd)
  {
    r->handler = SMOOTH_STREAMING_HANDLER;
  }

  DEBUG_R(r, "translate(step1) hostname=%s uri=%s args=%s filename=%s",
    r->hostname, r->uri, r->args, r->filename);

  int usp_skip_rewrite = merge_int(1, conf->usp_skip_rewrite);

  char new_path[4096];
  char new_args[4096];

  char const* path_first = r->uri;
  char const* path_last = r->uri + strlen(r->uri);
  char const* query_first = r->args;
  char const* query_last = r->args + (r->args ? strlen(r->args) : 0);

  // mp4_rewrite_url returns 1 when the request needs to be handled by us
  TRACE1_R(r, "translate_name: calling mp4_rewrite_url, path=%s, query=%s,"
    " usp_handle_ism=%d, usp_handle_f4f=%d", r->uri, r->args, usp_handle_ism,
    usp_handle_f4f);
  if(!mp4_rewrite_url(path_first, path_last,
                      query_first, query_last,
                      new_path, sizeof(new_path),
                      new_args, sizeof(new_args),
                      usp_handle_ism || usp_handle_api, usp_handle_f4f))
  {
    if(usp_handle_smpd)
    {
      TRACE1_R(r, "translate_name: declined, read-mutex");
      return DECLINED;
    }

    TRACE1_R(r, "translate_name: declined, no rewrite done");
    res = DECLINED;
  }
  else
  {
    TRACE1_R(r, "translate_name: called mp4_rewrite_url, new path=%s, args=%s",
      new_path, new_args);

    // from here on out, our main handler should pick this up.
    r->handler = SMOOTH_STREAMING_HANDLER;

    TRACE1_R(r, "translate_name: usp_skip_rewrite=%d, usp_handle_f4f=%d",
      usp_skip_rewrite, usp_handle_f4f);
    if(usp_skip_rewrite && !usp_handle_f4f)
    {
      TRACE1_R(r, "translate_name: calling ap_core_translate");
      res = ap_core_translate(r);
      if(res != OK || !r->filename)
      {
        TRACE1_R(r, "translate_name: core translate failed, return %d", res);
        return res;
      }
      // Now it should be safe to check for IsmProxyPass directives.
      int use_ism_proxy = handle_ism_proxy(r, 0);
      TRACE1_R(r, "translate_name: use_ism_proxy=%d", use_ism_proxy);
      if(use_ism_proxy != OK)
      {
        TRACE1_R(r, "translate_name: declined, rewrite skipped");
        r->args = apr_psprintf(r->pool, "%s", new_args);
        return DECLINED;
      }
    }

    // store the rewritten url
    r->uri = apr_psprintf(r->pool, "%s", new_path);
    r->args = apr_psprintf(r->pool, "%s", new_args);
    r->filename = NULL;

    TRACE1_R(r, "translate_name: rewrote uri=%s args=%s", r->uri, r->args);
    res = OK;
  }
  //assert(res == OK || res == DECLINED);

  if(usp_handle_ism)
  {
    res = ap_core_translate(r);
    if(res != OK || !r->filename)
    {
      TRACE1_R(r, "translate_name: core translate failed, return %d",
        res);
      return res;
    }

    DEBUG_R(r, "translate(step2) hostname=%s uri=%s args=%s filename=%s",
      r->hostname, r->uri, r->args, r->filename);

    if((res = handle_ism_proxy(r, 1)) == OK)
    {
      // if the name was rewritten by ism_proxy_pass, then return
      TRACE1_R(r, "translate_name: handled by ism_proxy, filename=%s",
        r->filename);
      return OK;
    }
  }

  // otherwise allow other modules to run their hooks, e.g. mod_auth_token
  TRACE1_R(r, "translate_name: declined");
  return DECLINED;
}

static int smooth_streaming_map_location(request_rec* r)
{
  DEBUG_R(r, "map_to_storage(step1) filename=%s", r->filename);

  // Handle API case when not backed by a real filename (e.g.
  // http://usp-api/license)
  if(r->filename && !strncmp(r->filename, prefix_usp_admin, prefix_usp_admin_len))
  {
    DEBUG_R(r, "map_to_storage(api) uri=%s", r->uri);
    return OK;
  }

  if(!r->filename || strncmp(r->filename, prefix_ismproxy, prefix_ismproxy_len) != 0)
  {
    TRACE1_R(r, "map_to_storage: declined");
    return DECLINED;
  }

  DEBUG_R(r, "map_to_storage(step2) filename=%s", r->filename);

  return OK;
}

static void set_header_in(void* p, char const* key, char const* value)
{
  auto* r = static_cast<request_rec*>(p);
  DEBUG_R(r, "set header=%s, value=%s", key, value);
  apr_table_set(r->headers_in, key, value);
}

static char const prefix_proxy[] = "proxy:";
static size_t const prefix_proxy_len = sizeof prefix_proxy - 1;

static void set_url(void *p, char const* url)
{
  auto* r = static_cast<request_rec*>(p);
  DEBUG_R(r, "set url=%s%s", prefix_proxy, url);
  r->filename = apr_pstrcat(r->pool, prefix_proxy, url, NULL);
}

static int smooth_streaming_fixups(request_rec* r)
{
  DEBUG_R(r, "fixups, subreq=%s hostname=%s uri=%s filename=%s args=%s",
    (r->main == NULL ? "no" : "yes"), r->hostname, r->uri, r->filename, r->args);

  if(r->main == NULL)
  {
    DEBUG_R(r, "Not adding S3 headers for main request");
    return DECLINED;
  }

  if(r->filename == NULL)
  {
    DEBUG_R(r, "Skipping S3 auth, filename is NULL");
    return DECLINED;
  }

  if(strncmp(r->filename, prefix_proxy, prefix_proxy_len) != 0)
  {
    DEBUG_R(r, "Skipping S3 auth for non-proxy URL %s", r->filename);
    return DECLINED;
  }
  char const* url = r->filename + prefix_proxy_len;
  DEBUG_R(r, "proxy url=%s", url);

  static char const prefix_http[] = "http://";
  static char const prefix_https[] = "https://";
  if(strncmp(url, prefix_http, sizeof prefix_http - 1) != 0 &&
     strncmp(url, prefix_https, sizeof prefix_https - 1) != 0)
  {
    DEBUG_R(r, "Skipping S3 auth for non-http/https proxy URL %s", r->filename);
    return DECLINED;
  }

  auto* dir_conf = static_cast<dir_config_t*>(
    ap_get_module_config(r->per_dir_config, &usp_module_name));
  char const* src = "request";
  if(dir_conf->s3_secret_key == NULL || dir_conf->s3_access_key == NULL)
  {
    // If the request itself does not have any S3 parameters, try again with the
    // configuration of the parent request, if there is one.
    dir_conf = static_cast<dir_config_t*>(
      ap_get_module_config(r->main->per_dir_config, &usp_module_name));
    if(dir_conf->s3_secret_key == NULL || dir_conf->s3_access_key == NULL)
    {
      DEBUG_R(r, "Skipping S3 auth, no secret or access keys (req & parent)");
      return DECLINED;
    }
    src = "parent";
  }

  if(dir_conf->s3_use_headers <= 0)
  {
    DEBUG_R(r, "Not adding S3 headers, S3UseHeaders is off");
    return DECLINED;
  }

  DEBUG_R(r, "Adding S3 headers, src=%s, url=%s, args=%s, region=%s",
    src, url, r->args, dir_conf->s3_region);

  char result_text[256] = { 0 };
  int status = mp4_add_s3_authentication(r, url, dir_conf->s3_secret_key,
    dir_conf->s3_access_key, dir_conf->s3_region, dir_conf->s3_security_token,
    dir_conf->s3_use_headers, set_header_in, set_url, result_text,
    sizeof result_text);
  if(status != 200)
  {
    ERROR_R(r, "adding S3 signature headers failed with status %d: %s", status,
      result_text);
    return status;
  }

  return DECLINED;
}

static int get_server_variable(void* context, char const* name, char const** value)
{
  auto const* r = static_cast<request_rec const*>(context);

  if(!strcmp(name, "REQUEST_SCHEME"))
  {
    *value = ap_http_scheme(r);
    return static_cast<int>(strlen(*value));
  } else
  if(!strcmp(name, "HTTP_HOST"))
  {
    *value = apr_table_get(r->headers_in, "Host");
    return static_cast<int>(strlen(*value));
  } else
  if(!strcmp(name, "REQUEST_URI"))
  {
    *value = r->uri;
    return static_cast<int>(strlen(*value));
  }

  return 0;
}

static void log_error_callback(void* context, fmp4_log_level_t fmp4_level,
                               char const* msg, size_t size)
{
  auto const* request = static_cast<request_rec const*>(context);

  int ap_level = fmp4_log_level_to_apache_log_level(fmp4_level);
  int clamped_size = size < INT_MAX ? static_cast<int>(size) : INT_MAX;
  ap_log_rerror(APLOG_MARK, ap_level, 0, request, "%.*s", clamped_size, msg);
}

static apr_status_t mp4_process_context_cleanup(void* data)
{
  auto const* context = static_cast<mp4_process_context_t const*>(data);
  mp4_process_context_exit(context);

  return APR_SUCCESS;
}

static int smooth_streaming_handler(request_rec* r)
{
  int http_status;

  if((!r->handler) || (strcmp(r->handler, SMOOTH_STREAMING_HANDLER)))
  {
    return DECLINED;
  }

  auto const* server_conf = static_cast<server_config_t const*>(
    ap_get_module_config(r->server->module_config, &usp_module_name));

  auto const* dir_conf = static_cast<dir_config_t const*>(
    ap_get_module_config(r->per_dir_config, &usp_module_name));

  r->allowed = (AP_METHOD_BIT << M_GET);
  r->allowed |= (AP_METHOD_BIT << M_POST);
  r->allowed |= (AP_METHOD_BIT << M_PUT);
  if(dir_conf->usp_handle_api > 0)
  {
    r->allowed |= (AP_METHOD_BIT << M_DELETE);
  }

  // let the default Apache handler deal with the OPTIONS method.
  if(r->method_number == M_OPTIONS)
  {
    return DECLINED;
  }

  http_status = HTTP_METHOD_NOT_ALLOWED;

  if((AP_METHOD_BIT << r->method_number) & r->allowed)
  {
    mp4_process_context_t* context =
      mp4_process_context_init(server_conf->global_context);

    apr_pool_cleanup_register(r->pool, context, mp4_process_context_cleanup,
                              apr_pool_cleanup_null);

    ap_set_module_config(r->request_config, &usp_module_name, context);
    mp4_process_context_set_verbose(context,
      apache_log_level_to_fmp4_log_level(
        ap_get_request_module_loglevel(r, APLOG_MODULE_INDEX)));

    if(dir_conf->usp_enable_subreq > 0)
    {
      TRACE1_R(r, "enabling download via subrequest, hostname=%s uri=%s, "
        "filename=%s args=%s", r->hostname, r->uri, r->filename, r->args);
      mp4_process_context_set_download_callback(context, subreq_download, r);
    }

    mp4_process_context_set_variable_callback(context, get_server_variable, r);
    mp4_process_context_set_log_error_callback(context, log_error_callback, r);

    int usp_handle_api = dir_conf->usp_handle_api > 0;
    int usp_handle_sitemap = dir_conf->usp_handle_sitemap > 0;

    {
      mp4_process_context_set_prefer_static(context,
        merge_int(0, dir_conf->usp_prefer_static));
      mp4_process_context_set_iss_pass_through(context,
        merge_int(0, dir_conf->usp_iss_pass_through));
      mp4_process_context_set_dynamic_time_shift_buffer_depth(context,
        merge_int(0, dir_conf->usp_dynamic_time_shift_buffer_depth));

      if(!dir_conf->usp_enable_mmap)
      {
        mp4_process_context_set_enable_mmap(context, 0);
      }

      mp4_process_context_set_s3_parameters(context, dir_conf->s3_secret_key,
        dir_conf->s3_access_key, dir_conf->s3_region,
        dir_conf->s3_security_token, merge_int(0, dir_conf->s3_use_headers));
      mp4_process_context_set_transcode_proxy_pass(context,
        dir_conf->transcode_proxy_pass);

      mp4_process_context_set_content_id(context,
        merge_int(0, dir_conf->usp_content_id));

      if(dir_conf->usp_handle_smpd > 0)
      {
        mp4_process_context_set_is_storage_mpd(context, 1);
      }
    }

    // Module version info
    apr_table_setn(r->headers_out, "X-USP", fmp4_version_string());

    if(r->args && !mp4_options_set(mp4_process_context_get_options(context),
                                   r->args, strlen(r->args)))
    {
      return HTTP_BAD_REQUEST;
    }

    auto http_method = get_http_method(r);

    switch(r->method_number)
    {
    case M_GET:
      if(dir_conf->usp_output_filter != NULL)
      {
        char const* err;
        char const* val = ap_expr_str_exec(r, dir_conf->usp_output_filter,
                                           &err);
        if(err != NULL)
        {
          ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                        "smooth_streaming_handler: failed to evaluate "
                        "UspOutputFilter value expression: %s", err);
          r->status = HTTP_INTERNAL_SERVER_ERROR;
          return r->status;
        }
        TRACE1_R(r, "evaluated output_filter to %s", val);
        mp4_process_context_set_output_filter(context, val);
      }
      http_status = drive_smooth_streaming_get(r, dir_conf, context,
        http_method, usp_handle_api, usp_handle_sitemap);
      break;
    case M_POST:
    case M_PUT:
      http_status = drive_smooth_streaming_post(r, context, http_method,
        usp_handle_api);
      break;
    case M_DELETE:
      if(usp_handle_api)
      {
        http_status = mp4_channel_api(context, http_method, r->filename,
          r->path_info);
      }
      break;
    }

    fmp4_result result = mp4_process_context_get_result(context);
    if(result != FMP4_OK)
    {
      mp4_options_t const* options = mp4_process_context_get_options(context);
      char const* file = mp4_options_get_file(options);
      char const* result_text = mp4_process_context_get_result_text(context);

      ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                    "mp4_process(uri %s, file %s) returned: %s %s",
                    r->filename,
                    file,
                    fmp4_result_to_string(result),
                    result_text);
    }
  }

  if(http_status != OK)
  {
    TRACE1_R(r, "set request status to %d", http_status);
    r->status = http_status;
  }

  return ap_is_HTTP_SUCCESS(http_status) ? OK : http_status;
}

static char const* set_usp_license_key(cmd_parms* cmd, void* /* cfg */, char const* arg)
{
  server_rec* s = cmd->server;
  auto* conf = static_cast<server_config_t*>(
    ap_get_module_config(s->module_config, &usp_module_name));

  conf->license = arg;

  return NULL;
}

static char const* set_ism_proxy_pass(cmd_parms* cmd, void* /* cfg */, char const* arg1, char const* arg2)
{
  server_rec* s = cmd->server;
  auto* conf = static_cast<server_config_t*>(
    ap_get_module_config(s->module_config, &usp_module_name));
  char const* fake = NULL;
  char const* real = NULL;

  // NOTE: when inside an If/ElseIf/Else section, cmd->path will be equal to
  // "*If", so it cannot be used directly. Also, for some other variations of
  // Apache "sections, cmd->path might not be sensible.
  //
  // Instead, we need to travel up parent directives to find any surrounding
  // Directory/DirectoryMatch/Location/LocationMatch section, and grab the path
  // from there.
  //
  // If such a section cannot be found, we are directly in the top level, either
  // of the VirtualHost, or the global Apache configuration.

  int in_location = 0;
  for(auto const* dirp = cmd->directive; dirp != NULL; dirp = dirp->parent)
  {
    TRACE1_S(s, "directive=%s, args=%s, filename=%s, line_num=%d",
      dirp->directive, dirp->args, dirp->filename, dirp->line_num);
    in_location =
      strcasecmp(dirp->directive, "<Location") == 0 ||
      strcasecmp(dirp->directive, "<LocationMatch") == 0;
    if(in_location ||
      strcasecmp(dirp->directive, "<Directory") == 0 ||
      strcasecmp(dirp->directive, "<DirectoryMatch") == 0)
    {
      auto const* endp = ap_strrchr_c(dirp->args, '>');
      if(endp == NULL)
      {
        return apr_pstrcat(cmd->pool,
          dirp->directive, "> directive missing closing '>'", NULL);
      }
      else
      {
        char const* arg = apr_pstrndup(cmd->temp_pool, dirp->args, endp - dirp->args);
        fake = ap_getword_conf(cmd->temp_pool, &arg);
        DEBUG_S(s, "found %s> directive, fake=%s", dirp->directive, fake);
      }
      break;
    }
  }

  if(fake == NULL)
  {
    // Outside a Directory or Location section, two arguments (fake and real)
    // are required.
    if(arg2 == NULL)
    {
      return "IsmProxyPass needs two arguments (virtual path and real url) "
             "when used outside a Directory or Location section";
    }
    fake = arg1;
    real = arg2;
  }
  else
  {
    // Inside a Directory or Location section, one argument (real) is required.
    if(arg2 != NULL)
    {
      return "IsmProxyPass needs only one argument (real url) when used inside "
             "a Directory or Location section";
    }
    real = arg1;
  }

  auto* alias = static_cast<proxy_alias_t*>(apr_array_push(conf->aliases));
  alias->fake = apr_pstrdup(cmd->pool, fake);
  alias->real = apr_pstrdup(cmd->pool, real);
  alias->in_location = in_location;
  DEBUG_S(s, "alias: fake=%s, real=%s, in_location=%d",
    alias->fake, alias->real, alias->in_location);

  return NULL;
}

static char const* set_output_filter(cmd_parms* cmd, void* cfg, char const* arg)
{
  char const* err;
  ap_expr_info_t const* expr =
    ap_expr_parse_cmd(cmd, arg, AP_EXPR_FLAG_STRING_RESULT, &err, NULL);
  if(err != NULL)
  {
    return apr_pstrcat(cmd->temp_pool, "Failed to parse UspOutputFilter "
                       "expression: ", err, NULL);
  }

  auto* dir_cfg = static_cast<dir_config_t*>(cfg);
  dir_cfg->usp_output_filter = expr;
  return NULL;
}

static void register_hooks(apr_pool_t* /* p */)
{
  // NOTE: mod_auth_token has mod_alias as its successor, so we need to have
  // that too. Otherwise, if mod_auth_token is not loaded, we will not run
  // before mod_alias, and that conflicts with our translate_name processing.
  static const char* const we_run_before[] = { "mod_alias.c",
                                               "mod_auth_token.c", NULL };

  /* module initializer */
  ap_hook_post_config(smooth_streaming_post_config, NULL, NULL,
                      APR_HOOK_MIDDLE);

  subreq_init();

  /* URI-to-filename translation */
  ap_hook_translate_name(smooth_streaming_translate_name, NULL, we_run_before,
                         APR_HOOK_MIDDLE);
  /* storage mapping */
  ap_hook_map_to_storage(smooth_streaming_map_location, NULL, NULL,
                         APR_HOOK_FIRST);

  /* at end of request preparation, but before the handler */
  ap_hook_fixups(smooth_streaming_fixups, NULL, NULL, APR_HOOK_LAST);

  /* the handler */
  ap_hook_handler(smooth_streaming_handler, NULL, NULL, APR_HOOK_MIDDLE);

}

// End Of File

