/* SPDX-License-Identifier: AGPL-3.0-or-later Copyright (C) 2026 mrkubax10 */ #include #include #include #include #include #include #include #include #include #include #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; icount; 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, "" "" "" "mod_webgate" "" "" "
mod_webgate: Please select appropriate option
", NULL ); static const char* OPTION_STRINGS[] = { "I would like to access this server
", "I would not like to access this server
", }; unsigned char option_order[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 }; // Randomize options order for(size_t i = 0; i" "", NULL ); } static void handle_verification(request_rec* r) { // Send information about access granted ap_set_content_type(r, "text/html"); ap_rvputs(r, "" "" "" "mod_webgate" "" "" "
mod_webgate: Challenge successful
" "Click here to go to main page" "" "", NULL ); } static void handle_blocked(request_rec* r) { // Send information about access rejection ap_set_content_type(r, "text/html"); ap_rvputs(r, "" "" "" "mod_webgate" "" "" "Fuck off clanker" "" "", 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; }