/*******************************************************************************
 mod_unified_s3_auth.cpp

 mod_unified_s3_auth - An Apache module for S3 Authentication

 Copyright (C) 2020-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>
#include <apr_version.h>
#include <apr_strings.h>
#include <apr_base64.h>
#include <apr_md5.h>

#include <mod_streaming_export.h>
#include <s3_util.h>

#include <ap_dump.h>
#include <ap_log.h>
#include <log_util.h>

extern "C" {

extern struct module_struct MOD_STREAMING_DLL_EXPORT unified_s3_auth_module;

} // extern C definitions

namespace // anonymous
{

typedef struct
{
  char const* s3_secret_key;
  char const* s3_access_key;
  char const* s3_region;
  char const* s3_security_token;
  char const* s3_role_arn;
  char const* s3_role_session_name;
  int s3_role_duration;
  int s3_use_headers;
} dir_config_t;

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->s3_secret_key = nullptr;
  config->s3_access_key = nullptr;
  config->s3_region = nullptr;
  config->s3_security_token = nullptr;
  config->s3_role_arn = nullptr;
  config->s3_role_session_name = nullptr;
  config->s3_role_duration = -1;
  config->s3_use_headers = -1;

  return config;
}

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

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

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->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_role_arn = merge_str(base->s3_role_arn, add->s3_role_arn);
  res->s3_role_session_name = merge_str(base->s3_role_session_name, add->s3_role_session_name);
  res->s3_role_duration = merge_int(base->s3_role_duration, add->s3_role_duration);
  res->s3_use_headers = merge_int(base->s3_use_headers, add->s3_use_headers);

  return res;
}

struct server_config_t
{
  s3_credentials_cache_t* s3_credentials_cache;
};

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->s3_credentials_cache = nullptr;

  return config;
}

#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__

command_rec const s3_auth_cmds[] =
{
  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_TAKE1("S3RoleARN",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_role_arn)),
    ACCESS_CONF,
    "Pass the Amazon S3 Role ARN"),

  AP_INIT_TAKE1("S3RoleSessionName",
    reinterpret_cast<cmd_func>(ap_set_string_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_role_session_name)),
    ACCESS_CONF,
    "Pass the Amazon S3 Role Session Name"),

  AP_INIT_TAKE1("S3RoleDuration",
    reinterpret_cast<cmd_func>(ap_set_int_slot),
    reinterpret_cast<void*>(APR_OFFSETOF(dir_config_t, s3_role_duration)),
    ACCESS_CONF,
    "Pass the Amazon S3 Role Duration"),

  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"),

  { nullptr }
};

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

apr_status_t cleanup_s3_credentials_cache(void* data)
{
  s3_credentials_cache_exit(static_cast<s3_credentials_cache_t*>(data));

  return APR_SUCCESS;
}

int s3_auth_post_config(apr_pool_t* pool, apr_pool_t* /* plog */,
                        apr_pool_t* /* ptemp */, server_rec* svr)
{
  // This function will be called twice. Don't bother going through all of the
  // initialization on the first call because it will just be thrown away.
  if (ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_CREATE_PRE_CONFIG)
    return OK;

  auto* s3_credentials_cache = s3_credentials_cache_init();
  if(s3_credentials_cache == nullptr)
  {
    constexpr int status = HTTP_INTERNAL_SERVER_ERROR;
    ap_log_perror(APLOG_MARK, APLOG_ERR, status, pool,
      "failed to create S3 credentials cache");
    return status;
  }

  apr_pool_cleanup_register(pool,
    reinterpret_cast<void const*>(s3_credentials_cache),
    cleanup_s3_credentials_cache, apr_pool_cleanup_null);

  for(auto* s = svr; s != nullptr; s = s->next)
  {
    auto* conf = static_cast<server_config_t*>(
      ap_get_module_config(s->module_config, &unified_s3_auth_module));
    conf->s3_credentials_cache = s3_credentials_cache;
  }

  return OK;
}

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);
}

struct s3_credentials_t
{
  apr_pool_t* pool;
  char const** secret_key;
  char const** access_key;
  char const** security_token;
};

void s3_credentials_callback(void* context,
                             char const* secret_key,
                             char const* access_key,
                             char const* security_token)
{
  auto* credentials = static_cast<s3_credentials_t*>(context);
  *credentials->secret_key = apr_pstrdup(credentials->pool, secret_key);
  *credentials->access_key = apr_pstrdup(credentials->pool, access_key);
  *credentials->security_token = apr_pstrdup(credentials->pool, security_token);
}

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);
}

constexpr char prefix_proxy[] = "proxy:";

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, nullptr);
}

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

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

  constexpr char prefix_ismproxy[] = "ismproxy:";
  if(strncmp(r->filename, prefix_ismproxy, sizeof prefix_ismproxy - 1) == 0)
  {
    DEBUG_R(r, "Skipping S3 auth for ismproxy URL %s", r->filename);
    return DECLINED;
  }

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

  constexpr char prefix_http[] = "http://";
  constexpr char 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, &unified_s3_auth_module));
  char const* src = "request";
  if(dir_conf->s3_secret_key == nullptr || dir_conf->s3_access_key == nullptr)
  {
    // If the request itself does not have any S3 parameters, try again with the
    // configuration of the parent request, if there is one.
    if(r->main != nullptr)
    {
      dir_conf = static_cast<dir_config_t*>(
        ap_get_module_config(r->main->per_dir_config, &unified_s3_auth_module));
      if(dir_conf->s3_secret_key == nullptr || dir_conf->s3_access_key == nullptr)
      {
        DEBUG_R(r, "Skipping S3 auth, no secret or access keys (req & parent)");
        return DECLINED;
      }
      src = "parent";
    }
    else
    {
      DEBUG_R(r, "Skipping S3 auth, no secret or access keys");
      return DECLINED;
    }
  }

  if(dir_conf->s3_use_headers <= 0)
  {
    // If we need to add arguments, check first whether there is already an
    // Authorization header, and bail out in that case. AWS does not accept
    // multiple authentication mechanisms simultaneously.
    char const* auth_header = apr_table_get(r->headers_in, "Authorization");
    if(auth_header != nullptr)
    {
      WARNING_R(r, "Not adding S3 authentication query arguments, because an "
        "Authorization header is already set in the request: %s", auth_header);
      return DECLINED;
    }
  }

  auto const* secret_key = dir_conf->s3_secret_key;
  auto const* access_key = dir_conf->s3_access_key;
  auto const* security_token = dir_conf->s3_security_token;

  if(dir_conf->s3_role_arn != nullptr && dir_conf->s3_role_session_name != nullptr)
  {
    auto* server_conf = static_cast<server_config_t*>(
      ap_get_module_config(r->server->module_config, &unified_s3_auth_module));
    auto role_duration = dir_conf->s3_role_duration < 900 ? 900U :
      static_cast<unsigned int>(dir_conf->s3_role_duration);
    s3_credentials_t credentials{r->pool, &secret_key, &access_key,
      &security_token};
    auto log_level = apache_log_level_to_fmp4_log_level(
      ap_get_request_module_loglevel(r, APLOG_MODULE_INDEX));
    char result_text[256] = { 0 };
    auto status = s3_credentials_cache_obtain(server_conf->s3_credentials_cache,
      secret_key, access_key, dir_conf->s3_region, dir_conf->s3_role_arn,
      dir_conf->s3_role_session_name, role_duration, log_level,
      log_error_callback, r, s3_credentials_callback, &credentials,
      result_text, sizeof result_text);
    if(status != 200)
    {
      ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
        "Obtaining S3 credentials failed with status %d: %s", status, result_text);
      return status;
    }

    DEBUG_R(r, "Obtained temporary S3 secret key %s, access key %s, "
      "security token %s, duration %u",
      secret_key, access_key, security_token, role_duration);
  }

  DEBUG_R(r, "Adding S3 auth %s, src=%s, url=%s, args=%s, region=%s",
    dir_conf->s3_use_headers <= 0 ? "query arguments" : "headers", src, url,
    r->args, dir_conf->s3_region);

  char result_text[256] = { 0 };
  auto status = mp4_add_s3_authentication(r, url, secret_key,
    access_key, dir_conf->s3_region, security_token,
    dir_conf->s3_use_headers, set_header_in, set_url, result_text,
    sizeof result_text);
  if(status != 200)
  {
    ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r,
      "Adding S3 authentication %s failed with status %d: %s",
      dir_conf->s3_use_headers <= 0 ? "query arguments" : "headers",
      status, result_text);
    return status;
  }

  TRACE3_R(r, "s3_auth_fixups: leave, DECLINED");
  return DECLINED;
}

char const* const unified_modules[] =
{
  "mod_smooth_streaming.cpp",
  "mod_unified_remix.cpp",
  "mod_unified_transcode.cpp",
  nullptr
};

void register_hooks(apr_pool_t* /* pool */)
{
  ap_hook_post_config(s3_auth_post_config, nullptr, nullptr, APR_HOOK_MIDDLE);

  // at end of request preparation, but before the handler
  ap_hook_fixups(s3_auth_fixups, unified_modules, nullptr, APR_HOOK_LAST);
}

} // anonymous

extern "C" {

struct module_struct MOD_STREAMING_DLL_EXPORT unified_s3_auth_module =
{
  STANDARD20_MODULE_STUFF,
  create_dir_config,
  merge_dir_config,
  create_server_config,
  nullptr,
  s3_auth_cmds,
  register_hooks,
  AP_MODULE_FLAG_ALWAYS_MERGE
};

} // extern C definitions

// End Of File
