Files
mod_webgate/mod_webgate.c
T

277 lines
8.3 KiB
C

/*
SPDX-License-Identifier: AGPL-3.0-or-later
Copyright (C) 2026 mrkubax10 <mrkubax10@onet.pl>
*/
#include <apr_base64.h>
#include <httpd.h>
#include <http_core.h>
#include <http_protocol.h>
#include <http_log.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <util_cookies.h>
#include <util_md5.h>
#define MAX_TOKEN_LENGTH 30
static void register_hooks(apr_pool_t* pool);
static void* create_directory_configuration(apr_pool_t* pool, char* context);
static void* merge_directory_configuration(apr_pool_t* pool, void* p_base, void* p_override);
static const char* global_config_set_tracked_token_count(cmd_parms* cmd, void* cfg, const char* count);
static const char* context_config_set_enabled(cmd_parms* cmd, void* cfg, const char* enabled);
static const command_rec config_directives[] = {
AP_INIT_TAKE1("webgateTrackedTokenCount", global_config_set_tracked_token_count, NULL, RSRC_CONF, "Set maximum amount of verified or blocked access tokens which module will track"),
AP_INIT_TAKE1("webgateEnabled", context_config_set_enabled, NULL, ACCESS_CONF, "Configure whether mod_webgate is supposed to be enabled for this location or directory"),
{}
};
module AP_MODULE_DECLARE_DATA webgate_module = {
STANDARD20_MODULE_STUFF,
create_directory_configuration,
merge_directory_configuration,
NULL,
NULL,
config_directives,
register_hooks,
AP_MODULE_FLAG_NONE
};
static const char* const TOKEN_COOKIE_NAME = "webgate_token";
typedef struct ModuleGlobalConfig {
// Maximum amount of access tokens which module will keep track of
size_t tracked_token_count;
} ModuleGlobalConfig;
static ModuleGlobalConfig g_global_config;
// Context aware configuration
typedef struct ModuleContextConfig {
// Whether module is enabled in current context (for example directory)
bool enabled;
} ModuleContextConfig;
typedef struct TokenArray {
char* tokens;
size_t count;
} TokenArray;
static void token_array_init(TokenArray* self) {
self->tokens = calloc(g_global_config.tracked_token_count, MAX_TOKEN_LENGTH);
memset(self->tokens, 0, g_global_config.tracked_token_count*MAX_TOKEN_LENGTH);
self->count = 0;
}
static void token_array_push(TokenArray* self, const char* token) {
// Wrap around when array gets full
if(self->count>=g_global_config.tracked_token_count)
self->count = 0;
strncpy(&self->tokens[self->count*MAX_TOKEN_LENGTH], token, MAX_TOKEN_LENGTH);
self->count++;
}
// Checks if token array contains token
static bool token_array_contains(const TokenArray* self, const char* token) {
for(size_t i = 0; i<self->count; i++) {
if(strcmp(&self->tokens[i*MAX_TOKEN_LENGTH], token)==0)
return true;
}
return false;
}
static TokenArray g_allowed_tokens = {0};
static TokenArray g_blocked_tokens = {0};
// Returns true if user agent of client which performed the request looks like Git
static bool check_user_agent_exception(request_rec* r) {
static const char GIT_USER_AGENT_PREFIX[] = "git/";
const char* const user_agent = apr_table_get(r->headers_in, "User-Agent");
if(!user_agent || strncmp(user_agent, GIT_USER_AGENT_PREFIX, sizeof(GIT_USER_AGENT_PREFIX))!=0)
return false;
return apr_table_get(r->headers_in, "Git-Protocol");
}
static const char* generate_token(apr_pool_t* pool) {
unsigned char rnd[32];
apr_generate_random_bytes(rnd, sizeof(rnd));
unsigned char rnd_md5[APR_MD5_DIGESTSIZE];
apr_md5(rnd_md5, rnd, sizeof(rnd));
const apr_size_t encoded_size = apr_base64_encode_len(sizeof(rnd_md5));
char* output = apr_palloc(pool, encoded_size);
apr_base64_encode(output, (char*)rnd_md5, sizeof(rnd_md5));
return output;
}
// Generates new token and sets HTTP cookie containing it then presents client with challenge
static void handle_challenge(request_rec* r) {
const char* const generated = generate_token(r->pool);
ap_cookie_write(r, TOKEN_COOKIE_NAME, generated, NULL, 0, r->headers_out, NULL);
// Send challenge page beginning
ap_set_content_type(r, "text/html");
ap_rvputs(r, "<!DOCTYPE HTML>"
"<html lang=\"en\">"
"<head>"
"<title>mod_webgate</title>"
"</head>"
"<body>"
"<h5>mod_webgate: Please select appropriate option</h5>",
NULL
);
static const char* OPTION_STRINGS[] = {
"<a href=\"/fail\">I would like to access this server</a><br>",
"<a href=\"/success\">I would not like to access this server</a><br>",
};
unsigned char option_order[] = {
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0
};
// Randomize options order
for(size_t i = 0; i<sizeof(option_order); i++) {
unsigned char swap_index;
apr_generate_random_bytes(&swap_index, 1);
swap_index%=sizeof(option_order);
if(swap_index==0)
swap_index = 1;
// Temporary value for swap
unsigned char temp = option_order[1];
option_order[1] = option_order[swap_index];
option_order[swap_index] = temp;
}
// Send options
for(size_t i = 0; i<sizeof(option_order); i++)
ap_rvputs(r, OPTION_STRINGS[option_order[i]], NULL);
// Finish page
ap_rvputs(r, "</body>"
"</html>",
NULL
);
}
static void handle_verification(request_rec* r) {
// Send information about access granted
ap_set_content_type(r, "text/html");
ap_rvputs(r, "<!DOCTYPE HTML>"
"<html lang=\"en\">"
"<head>"
"<title>mod_webgate</title>"
"</head>"
"<body>"
"<h5>mod_webgate: Challenge successful</h5>"
"<a href=\"/\">Click here to go to main page</a>"
"</body>"
"</html>",
NULL
);
}
static void handle_blocked(request_rec* r) {
// Send information about access rejection
ap_set_content_type(r, "text/html");
ap_rvputs(r, "<!DOCTYPE HTML>"
"<html lang=\"en\">"
"<head>"
"<title>mod_webgate</title>"
"</head>"
"<body>"
"Fuck off clanker"
"</body>"
"</html>",
NULL
);
}
static apr_status_t post_config_hook(apr_pool_t* pool, apr_pool_t* pool_log, apr_pool_t* pool_temp, server_rec* s) {
// Initialize global variables
token_array_init(&g_allowed_tokens);
token_array_init(&g_blocked_tokens);
return APR_SUCCESS;
}
static int request_check_handler(request_rec* r) {
// Check if module is enabled for current context
const ModuleContextConfig* config = (ModuleContextConfig*)ap_get_module_config(r->per_dir_config, &webgate_module);
if(!config->enabled)
return DECLINED;
// Provide exception for requests performed by Git
if(check_user_agent_exception(r))
return DECLINED;
// Check if client already has the token
const char* token;
if(ap_cookie_read(r, TOKEN_COOKIE_NAME, &token, 0)!=APR_SUCCESS || !token) {
handle_challenge(r);
return OK;
}
// Check if client was blocked
if(token_array_contains(&g_blocked_tokens, token)) {
handle_blocked(r);
return OK;
}
// Check if client is verified
if(token_array_contains(&g_allowed_tokens, token))
return DECLINED;
// If client is not blocked and not verified then perhaps they are trying to verify
if(strcmp(r->uri, "/fail")==0) {
token_array_push(&g_allowed_tokens, token);
handle_verification(r);
return OK;
}
if(strcmp(r->uri, "/success")==0) {
token_array_push(&g_blocked_tokens, token);
handle_blocked(r);
return OK;
}
// If it's something else redirect to challenge page
handle_challenge(r);
return OK;
}
static void register_hooks(apr_pool_t* pool) {
// Initialize global config
g_global_config.tracked_token_count = 1000;
ap_hook_post_config(post_config_hook, NULL, NULL, APR_HOOK_LAST);
ap_hook_handler(request_check_handler, NULL, NULL, APR_HOOK_FIRST);
}
static void* create_directory_configuration(apr_pool_t* pool, char* context) {
ModuleContextConfig* config = apr_palloc(pool, sizeof(ModuleContextConfig));
if(config)
config->enabled = false;
return config;
}
// Merges configuration of parent directory and subdirectory and produces new configuration
static void* merge_directory_configuration(apr_pool_t* pool, void* p_base, void* p_override) {
const ModuleContextConfig* base = (ModuleContextConfig*)p_base;
const ModuleContextConfig* override = (ModuleContextConfig*)p_override;
ModuleContextConfig* result = create_directory_configuration(pool, NULL);
if(result)
result->enabled = base->enabled || override->enabled;
return result;
}
static const char* global_config_set_tracked_token_count(cmd_parms* cmd, void* cfg, const char* count) {
g_global_config.tracked_token_count = atol(count);
return NULL;
}
static const char* context_config_set_enabled(cmd_parms* cmd, void* cfg, const char* enabled) {
if(!cfg)
return NULL;
ModuleContextConfig* config = (ModuleContextConfig*)cfg;
config->enabled = strcmp(enabled, "on")==0;
return NULL;
}