/* -*-mode: c; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ /** * mod_qos.c: Quality of service module for Apache Web Server. * * The Apache Web Servers requires threads and processes to serve * requests. Each TCP connection to the web server occupies one * thread or process. Sometimes, a server gets too busy to serve * every request due the lack of free processes or threads. * * This module implements control mechanisms that can provide * different priority to different requests. * * mod_qos requires OpenSSL, PCRE, threading and shared memory * support. It has been designed, developed and fully * tested for Apache httpd MPM worker binaries and it is optimized * to be used in a reverse proxy. * * See http://mod-qos.sourceforge.net/ for further * details and to obtain the latest version of this module. * * Copyright (C) 2023 Pascal Buchbinder * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. * */ /************************************************************************ * Version ***********************************************************************/ static const char revision[] = "$Id: mod_qos.c 2706 2023-05-16 19:52:59Z pbuchbinder $"; static const char g_revision[] = "11.74"; /************************************************************************ * Includes ***********************************************************************/ /* std */ #include #include #ifndef WIN32 # include # include #else # include # include # include #endif #include // for socket options #ifdef __unix__ #include #include #endif /* apache */ #include #include #include #include #include #include #define CORE_PRIVATE #include #include #include #include #include #include #include #include #include /* apr / scrlib */ #include #include #include #include #include #ifdef AP_NEED_SET_MUTEX_PERMS #include #endif /* mod_qos requires OpenSSL */ #include #include #include /* additional modules */ #include "mod_status.h" /* Preprocessor Definitions * this (optional header file allowing other modules to register to our hoos: */ #ifdef QS_MOD_EXT_HOOKS #include "mod_qos.h" #endif /************************************************************************ * defines ***********************************************************************/ #define QOS_LOG_PFX(id) "mod_qos("#id"): " #define QOS_LOGD_PFX "mod_qos(): " #define QOS_LOG_MSGCT 200 #define QOS_HASH_LEN 16 #define QOS_MAX_AGE "3600" #define QOS_COOKIE_NAME "MODQOS" #define QOS_USER_TRACKING "mod_qos_user_id" #define QOS_USER_TRACKING_NEW "QOS_USER_ID_NEW" #define QOS_USER_TRACKING_RENEW "QOS_USER_ID_RENEW" #define QOS_DISABLE_UTC_ENFORCEMENT "DISABLE_UTC_ENFORCEMENT" #define QOS_MILESTONE "mod_qos_milestone" #define QOS_MILESTONE_TIMEOUT 3600 #define QOS_MILESTONE_COOKIE "QSSCD" #define QS_SIM_IP_LEN 100 #define QS_USR_SPE "mod_qos::user" #define QS_REC_COOKIE "mod_qos::gc" #define QS_R010_ALREADY_BLOCKED "R010B" #define QS_R012_ALREADY_BLOCKED "R012B" #define QS_R013_ALREADY_BLOCKED "R013B" #define QS_PKT_RATE_TH 3 #define QS_BW_SAMPLING_RATE 10 // split linear QS_SrvMaxConnPerIP* entry (conn->conn_ip) search: #ifndef QS_MEM_SEG #define QS_MEM_SEG 4 #endif // buffer if srv opens new connections faster than it closes existing ones #define QS_DOUBLE_CONN_H 128 #define QS_DOUBLE_CONN 32 #define QS_CONN_ABORT "mod_qos_connection_aborted" /* Preprocessor Definitions * QSLOG_CLID, QSLOG_EVENT, QSLOG_AVERAGE define parameters for QSLog: */ #ifndef QSLOG_CLID #define QSLOG_CLID "mod_qos_user_id" #endif #ifndef QSLOG_EVENT #define QSLOG_EVENT "Event" #endif #ifndef QSLOG_AVERAGE #define QSLOG_AVERAGE "QS_AllConn" #endif /* Preprocessor Definitions * logs repeating messages only once: */ #ifndef QS_LOG_REPEAT #define QS_LOG_REPEAT 20 #endif #define QS_IP4IN6 "::ffff:" #define QS_PARP_Q "qos-parp-query" #define QS_PARP_QUERY "qos-query" #define QS_PARP_PATH "qos-path" #define QS_PARP_LOC "qos-loc" #define QSLOGFORMAT "ISBiDUkEQaC" #define QS_SET_DSCP "QS_Set_DSCP" #define QS_RESDELYATIME "QS_ResponseDelayTime" #define QS_CONNID "QS_ConnectionId" #define QS_COUNTRY "QS_Country" #define QS_SERIALIZE "QS_Serialize" #define QS_SRVSERIALIZE "QS_SrvSerialize" #define QS_ErrorNotes "QS_ErrorNotes" #define QS_BLOCK "QS_Block" #define QS_BLOCK_SEEN "QS_Block_seen" #define QS_BLOCK_DEC "QS_Block_Decrement" #define QS_LIMIT_NAME_PFX "QS_Limit_VAR_NAME_IDX" #define QS_LIMIT_DEFAULT "QS_Limit" #define QS_LIMIT_SEEN "QS_Limit_seen" #define QS_COUNTER_SUFFIX "_Counter" #define QS_LIMIT_CLEAR "_Clear" #define QS_LIMIT_DEC "_Decrement" #define QS_LIMIT_REMAINING "_Remaining" #define QS_EVENT "QS_Event" #define QS_COND "QS_Cond" #define QS_ISVIPREQ "QS_IsVipRequest" #define QS_VipRequest "QS_VipRequest" #define QS_KEEPALIVE "QS_KeepAliveTimeout" #define QS_MAXKEEPALIVEREQ "QS_MaxKeepAliveRequests" #define QS_TIMEOUT "QS_Timeout" #define QS_CLOSE "QS_SrvMinDataRate" #define QS_MAXIP "QS_SrvMaxConnPerIP" #define QS_EMPTY_CON "NullConnection" #define QS_BROKEN_CON "BrokenConnection" #define QS_RuleId "QS_RuleId" // enable connection counter if one of the following feature is used #define QS_COUNT_CONNECTIONS(sconf) (sconf->max_conn != -1) || \ (sconf->min_rate_max != -1) || \ (sconf->max_conn_close != -1) || \ (sconf->max_conn_per_ip_connections != 1) || \ sconf->geodb // "3758096128","3758096383","AU" #define QS_GEO_PATTERN "\"([0-9]+)\",\"([0-9]+)\",\"([A-Z0-9]{2}|-)\"" static const char *m_env_variables[] = { QS_ErrorNotes, QS_SERIALIZE, QS_SRVSERIALIZE, QS_BLOCK, QS_BLOCK_SEEN, QS_BLOCK_DEC, QS_LIMIT_DEFAULT, QS_LIMIT_SEEN, QS_EVENT, QS_COND, QS_ISVIPREQ, QS_VipRequest, QS_KEEPALIVE, QS_MAXKEEPALIVEREQ, QS_CLOSE, QS_EMPTY_CON, QS_BROKEN_CON, QS_RuleId, NULL }; static const char *m_note_variables[] = { QS_PARP_PATH, QS_PARP_QUERY, NULL }; #define QS_INCTX_ID inctx->id /* Preprocessor Definitions This is the measure rate for QS_SrvRequestRate/QS_SrvMinDataRate which may be increased to 10 or 30 seconds in order to compensate bandwidth variations. You may also use the QS_SrvSampleRate directive to override this default. Set it greater than the Apache Timeout directive to prevent from closing unused speculative TCP pre-connections. */ #ifndef QS_REQ_RATE_TM #define QS_REQ_RATE_TM 5 #endif #define QS_MAX_DELAY 5000000 #define QOS_DEC_MODE_FLAGS_URL 0x00 #define QOS_DEC_MODE_FLAGS_HTML 0x01 #define QOS_DEC_MODE_FLAGS_UNI 0x02 #define QOS_DEC_MODE_FLAGS_ANSI 0x04 #define QOS_LOW_FLAG_PKGRATE 0x01 #define QOS_LOW_FLAG_BEHAVIOR_OK 0x02 #define QOS_LOW_FLAG_BEHAVIOR_BAD 0x04 #define QOS_LOW_FLAG_EVENTBLOCK 0x08 #define QOS_LOW_FLAG_EVENTLIMIT 0x10 #define QOS_LOW_FLAG_TIMEOUT 0x20 #define QOS_LOW_TIMEOUT 86400 #define QOS_CC_BEHAVIOR_THR 50000 #define QOS_CC_BEHAVIOR_THR_SINGLE 50 #ifdef QS_INTERNAL_TEST #undef QOS_CC_BEHAVIOR_THR #undef QOS_CC_BEHAVIOR_THR_SINGLE #define QOS_CC_BEHAVIOR_THR 50 #define QOS_CC_BEHAVIOR_THR_SINGLE 20 #endif #define QOS_CC_BEHAVIOR_TOLERANCE_STR "20" /* Apache 2.2 testing needs patch in util_pcre.c line 134 as it does not know AP_REG_DOTALL * Add: * if ((cflags & 0x40) != 0) options |= 0x04; */ #ifndef AP_REG_DOTALL #define AP_REG_DOTALL 0x40 #endif #define QS_ERR_TIME_FORMAT "%a %b %d %H:%M:%S %Y" #define QSMOD 4 #define QOS_DELIM ";" // Apache 2.4 compat #if (AP_SERVER_MINORVERSION_NUMBER == 4) #define QS_APACHE_24 1 #if (AP_SERVER_PATCHLEVEL_NUMBER > 17) #define QS_CONN_REMOTEIP(c) c->master ? c->master->client_ip : c->client_ip #define QS_CONN_REMOTEADDR(c) c->master ? c->master->client_addr : c->client_addr #define QS_CONN_MASTER(c) (c->master ? c->master : c) #else #define QS_CONN_REMOTEIP(c) c->client_ip #define QS_CONN_REMOTEADDR(c) c->client_addr #define QS_CONN_MASTER(c) (c) #endif #define QOS_MY_GENERATION(g) ap_mpm_query(AP_MPMQ_GENERATION, &g) #define qos_unixd_set_global_mutex_perms ap_unixd_set_global_mutex_perms #define QS_ISDEBUG(s) APLOG_IS_LEVEL(s, APLOG_DEBUG) #else #define QS_CONN_REMOTEIP(c) c->remote_ip #define QS_CONN_REMOTEADDR(c) c->remote_addr #define QS_CONN_MASTER(c) (c) #define QOS_MY_GENERATION(g) g = ap_my_generation #define qos_unixd_set_global_mutex_perms unixd_set_global_mutex_perms #define QS_ISDEBUG(s) s->loglevel >= APLOG_DEBUG #endif #ifdef QS_MOD_EXT_HOOKS APR_IMPLEMENT_OPTIONAL_HOOK_RUN_ALL(qos, QOS, apr_status_t, path_decode_hook, (request_rec *r, char **path, int *len), (r, path, len), OK, DECLINED) APR_IMPLEMENT_OPTIONAL_HOOK_RUN_ALL(qos, QOS, apr_status_t, query_decode_hook, (request_rec *r, char **query, int *len), (r, query, len), OK, DECLINED) #endif /************************************************************************ * structures ***********************************************************************/ typedef struct { const char *name; /* variable name */ ap_regex_t *preg; const char *url; /* redirect url */ int code; } qos_redirectif_entry_t; typedef struct { unsigned long start; unsigned long end; char country[3]; } qos_geo_entry_t; typedef struct { qos_geo_entry_t *data; // fist element int size; // number of elements const char *path; } qos_geo_t; typedef struct { const char *url; const char *path; } qos_errelt_t; static const qos_errelt_t m_error_pages[] = { { "/errorpages/server_error.html", "work/errorpages/server_error.html" }, { "/errorpages/forbidden.html", "work/errorpages/forbidden.html" }, { "/errorpages/500.html", "work/errorpages/500.html" }, { "/errorpages/error.html", "work/errorpages/error.html" }, { "/errorpages/error500.html", "work/errorpages/error500.html" }, { "/errorpages/gateway_error.html", "work/errorpages/gateway_error.html" }, { NULL, NULL } }; typedef struct { int id; const char *name; } qos_dscp_t; static const qos_dscp_t m_dscp_desc[] = { { 0, "none" }, { 8, "class selector 1" }, { 10, "assured forwarding 11" }, { 12, "assured forwarding 12" }, { 14, "assured forwarding 13" }, { 16, "class selector 2" }, { 18, "assured forwarding 21" }, { 20, "assured forwarding 22" }, { 22, "assured forwarding 23" }, { 24, "class selector 3" }, { 26, "assured forwarding 31" }, { 28, "assured forwarding 32" }, { 30, "assured forwarding 33" }, { 32, "class selector 4" }, { 34, "assured forwarding 41" }, { 36, "assured forwarding 42" }, { 38, "assured forwarding 43" }, { 40, "class selector 5" }, { 44, "voice admit" }, { 46, "expedited forwarding" }, { 48, "class selector 6" }, { 56, "class selector 7" }, { -1, "unknown" } }; typedef struct { unsigned short int limit; time_t limitTime; } qos_s_entry_limit_t; typedef struct { unsigned short int limit; time_t limitTime; const char *eventClearStr; // name of the var clearing the counter const char *eventDecStr; // name of the var decrementing the counter const char *condStr; ap_regex_t *preg; } qos_s_entry_limit_conf_t; typedef struct { apr_uint64_t ip6[2]; time_t lowrate; unsigned int lowratestatus; /* behavior */ unsigned int html; unsigned int cssjs; unsigned int img; unsigned int other; unsigned int notmodified; unsigned int events; /* serialization flag */ unsigned int serialize; apr_time_t serializeQueue; /* prefer */ short int vip; /* ev block */ unsigned short int block; short int blockMsg; time_t time; time_t blockTime; qos_s_entry_limit_t *limit; /* ev/sec */ time_t interval; long req; long req_per_sec; int req_per_sec_block_rate; int event_req; } qos_s_entry_t; typedef struct { time_t t; /* index */ qos_s_entry_t **ipd; qos_s_entry_t **timed; /* shm */ apr_shm_t *m; char *lock_file; apr_global_mutex_t *lock; /* size */ int num; int max; int msize; /* limit table settings */ apr_table_t *limitTable; /* av. behavior */ unsigned long long html; unsigned long long cssjs; unsigned long long img; unsigned long long other; unsigned long long notmodified; /* data */ int connections; /* remember for which clients this rec has created (shared mem) */ int generation_locked; // indicates the the counters have been cleared for this generation /* log counter */ unsigned long long eventTotal[QOS_LOG_MSGCT]; // total number of events since initial start unsigned long long eventLast[QOS_LOG_MSGCT]; // number of events since last read } qos_s_t; typedef enum { QS_IP_V6_DEFAULT = 0, QS_IP_V6, QS_IP_V4 } qs_ip_type_e; typedef enum { QS_CONN_STATE_NEW = 0, QS_CONN_STATE_HEAD, QS_CONN_STATE_BODY, QS_CONN_STATE_CHUNKED, QS_CONN_STATE_KEEP, QS_CONN_STATE_RESPONSE, QS_CONN_STATE_END, QS_CONN_STATE_DESTROY } qs_conn_state_e; typedef enum { QS_HEADERFILTER_OFF_DEFAULT = 0, QS_HEADERFILTER_OFF, QS_HEADERFILTER_ON, QS_HEADERFILTER_SIZE_ONLY, QS_HEADERFILTER_SILENT } qs_headerfilter_mode_e; typedef enum { QS_FLT_ACTION_DROP, QS_FLT_ACTION_DENY } qs_flt_action_e; typedef enum { QS_EVENT_ACTION_DENY = 0 } qs_event_action_e; typedef enum { QS_DENY_REQUEST_LINE, QS_DENY_PATH, QS_DENY_QUERY, QS_DENY_EVENT, QS_PERMIT_URI } qs_rfilter_type_e; typedef enum { QS_LOG = 0, QS_DENY, QS_OFF_DEFAULT, QS_OFF } qs_rfilter_action_e; enum qos_cmp { QS_CMP_EQ, QS_CMP_NE, QS_CMP_GT, QS_CMP_LT }; typedef struct { enum qos_cmp cmp; const char *left; const char *right; const char *variable; const char *value; } qos_cmp_entry_t; typedef struct { char *variable1; char *variable2; ap_regex_t *preg; char *name; char *value; } qos_setenvif_t; typedef struct { ap_regex_t *preg; char *name; char *value; } qos_setenvifquery_t; typedef struct { ap_regex_t *pregx; char *name; char *value; } qos_setenvifparpbody_t; /** * generic request filter */ typedef struct { ap_regex_t *preg; char *text; char *id; qs_rfilter_type_e type; qs_rfilter_action_e action; } qos_rfilter_t; /** * list of in_filter ctx */ typedef struct { apr_table_t *table; #if APR_HAS_THREADS apr_thread_mutex_t *lock; apr_thread_t *thread; #endif int exit; } qos_ifctx_list_t; /** * ip entry */ typedef struct qs_ip_entry_st { apr_uint64_t ip6[2]; int counter; int error; } qs_ip_entry_t; typedef struct { qs_ip_entry_t *conn_ip; int conn_ip_len; int connections; int max_client; } qs_conn_t; typedef struct { apr_time_t q1; // first request in queue apr_time_t q2; // second request in queue int locked; } qs_serial_t; /** * session cookie */ typedef struct { time_t time; } qos_session_t; /** * cfg/act entry for event limitation */ typedef struct { const char *env_var;// configured environment variable name const char *eventDecStr; int max; // configured max. num int seconds; // configured duration int limit; // event counter time_t limitTime; // timer qs_event_action_e action; const char *condStr; ap_regex_t *preg; } qos_event_limit_entry_t; /** * access control table entry */ typedef struct qs_acentry_st { int id; /** pointer to lock of the actable */ apr_global_mutex_t *lock; /** location rules */ char *url; int url_len; char *event; ap_regex_t *regex; ap_regex_t *regex_var; ap_regex_t *condition; int counter; int limit; /* measurement */ apr_time_t interval; long req; long req_per_sec; long req_per_sec_limit; int req_per_sec_block_rate; long bytes; // transferred bytes apr_time_t kbytes_interval_us; // elapsed time apr_off_t kbytes_per_sec; // actual kbytes/sec (measured) apr_off_t kbytes_per_sec_limit; // configured limitation apr_off_t kbytes_per_sec_block_rate; // current wait time struct qs_acentry_st *next; } qs_acentry_t; /** * access control table (act) */ typedef struct qs_actable_st { apr_size_t size; apr_shm_t *m; apr_pool_t *pool; /** process pool is used to create user space data */ apr_pool_t *ppool; /** rule entry list */ qs_acentry_t *entry; /* shm pointer */ int has_events; /** event limit list */ qos_event_limit_entry_t *event_entry; /** mutex */ char *lock_file; apr_global_mutex_t *lock; /** ip/conn data */ qs_conn_t *conn; /* shm pointer */ unsigned int timeout; /* settings */ int child_init; /* serialize */ qs_serial_t *serialize; /* shm pointer */ time_t *qsstatustimer; /* shm pointer */ } qs_actable_t; /** * network table (total connections, vip connections, first update, last update) */ typedef struct qs_netstat_st { // int counter; int vip; // time_t first; // time_t last; } qs_netstat_t; /** * user space */ typedef struct { int server_start; apr_table_t *act_table; /* client control */ qos_s_t *qos_cc; } qos_user_t; /** * directory config */ typedef struct { char *path; apr_table_t *rfilter_table; int inheritoff; qs_headerfilter_mode_e headerfilter; qs_headerfilter_mode_e resheaderfilter; int bodyfilter_d; int bodyfilter_p; int dec_mode; apr_off_t maxpost; qs_rfilter_action_e urldecoding; const char *response_pattern; int response_pattern_len; const char *response_pattern_var; apr_array_header_t *redirectif; int decodings; apr_table_t *disable_reqrate_events; apr_table_t *setenvstatus_t; apr_array_header_t *setenvif_t; apr_table_t *setenvifquery_t; apr_array_header_t* setenvcmp; } qos_dir_config; /** * server configuration */ typedef struct { apr_pool_t *pool; int is_virtual; server_rec *base_server; char *mfile; qs_actable_t *act; const char *error_page; apr_table_t *location_t; apr_table_t *setenv_t; apr_table_t *setreqheader_t; apr_table_t *setreqheaderlate_t; apr_table_t *unsetresheader_t; apr_table_t *unsetreqheader_t; apr_array_header_t *setenvif_t; apr_table_t *setenvifquery_t; apr_table_t *setenvifparp_t; apr_table_t *setenvifparpbody_t; apr_table_t *setenvstatus_t; apr_table_t *setenvresheader_t; apr_table_t *setenvresheadermatch_t; apr_table_t *setenvres_t; qs_headerfilter_mode_e headerfilter; qs_headerfilter_mode_e resheaderfilter; apr_array_header_t *redirectif; char *cookie_name; char *cookie_path; char *user_tracking_cookie; char *user_tracking_cookie_force; int user_tracking_cookie_session; int user_tracking_cookie_jsredirect; char *user_tracking_cookie_domain; int max_age; unsigned char key[EVP_MAX_KEY_LENGTH]; const unsigned char *rawKey; int rawKeyLen; int keyset; char *header_name; int header_name_drop; ap_regex_t *header_name_regex; apr_table_t *disable_reqrate_events; char *ip_header_name; int ip_header_name_drop; ap_regex_t *ip_header_name_regex; int vip_user; int vip_ip_user; int has_conn_counter; int max_conn; int max_conn_close; int max_conn_close_percent; int max_conn_per_ip; int max_conn_per_ip_connections; int max_conn_per_ip_ignore_vip; int serialize; int serializeTMO; apr_table_t *exclude_ip; qos_ifctx_list_t *inctx_t; apr_table_t *hfilter_table; /* GLOBAL ONLY */ apr_table_t *reshfilter_table; /* GLOBAL ONLY */ /* event rule (enables rule validation) */ int has_event_filter; int has_event_limit; apr_array_header_t *event_limit_a; /* min data rate */ int req_rate; /* GLOBAL ONLY */ int req_rate_start; /* GLOBAL ONLY */ int min_rate; /* GLOBAL ONLY */ int min_rate_max; /* GLOBAL ONLY */ int min_rate_off; int req_ignore_vip_rate; /* GLOBAL ONLY */ int max_clients; int max_clients_conf; #ifdef QS_INTERNAL_TEST apr_table_t *testip; int enable_testip; #endif int disable_handler; int log_only; /* GLOBAL ONLY */ int log_env; /* client control */ int has_qos_cc; /* GLOBAL ONLY */ int qos_cc_size; /* GLOBAL ONLY */ int qos_cc_prefer; /* GLOBAL ONLY */ apr_table_t *cc_exclude_ip; /* GLOBAL ONLY */ int qos_cc_prefer_limit; int qos_cc_event; /* GLOBAL ONLY */ int qos_cc_event_req; /* GLOBAL ONLY */ int qos_cc_block; /* GLOBAL ONLY */ int qos_cc_blockTime; /* GLOBAL ONLY */ apr_table_t *qos_cc_limitTable; /* GLOBAL ONLY */ char *qos_cc_forwardedfor; /* GLOBAL ONLY */ int qos_cc_serialize; /* GLOBAL ONLY */ apr_off_t maxpost; int cc_tolerance; /* GLOBAL ONLY */ int cc_tolerance_max; /* GLOBAL ONLY */ int cc_tolerance_min; /* GLOBAL ONLY */ int qs_req_rate_tm; /* GLOBAL ONLY */ qos_geo_t *geodb; /* GLOBAL ONLY */ int geo_limit; /* GLOBAL ONLY */ apr_table_t *geo_priv; /* GLOBAL ONLY */ int geo_excludeUnknown; /* GLOBAL ONLY */ qs_ip_type_e ip_type; /* GLOBAL ONLY */ int qsstatus; /* GLOBAL ONLY */ int qsevents; /* GLOBAL ONLY */ apr_array_header_t *milestones; time_t milestoneTimeout; /* predefined client behavior */ int static_on; unsigned long long static_html; unsigned long long static_cssjs; unsigned long long static_img; unsigned long long static_other; unsigned long long static_notmodified; apr_file_t *qslog_p; /* GLOBAL ONLY */ const char *qslog_str; /* GLOBAL ONLY */ } qos_srv_config; #if APR_HAS_THREADS typedef struct { apr_thread_t *thread; int exit; int maxclients; time_t *qsstatustimer; /* shm in act */ apr_global_mutex_t *lock; /* lock of act */ apr_pool_t *pool; qos_srv_config *sconf; } qsstatus_t; #endif /** * in_filter ctx */ typedef struct { apr_socket_t *clientSocket; qs_conn_state_e status; apr_off_t cl_val; conn_rec *c; request_rec *r; /* upload bandwidth (received bytes and start time) */ time_t time; apr_size_t nbytes; // measuring the bytes/sec int hasBytes; int shutdown; int errors; int disabled; int lowrate; char *id; qos_srv_config *sconf; } qos_ifctx_t; /** * connection configuration */ typedef struct { apr_uint64_t ip6[2]; conn_rec *mc; // master (real) connection char *evmsg; qos_srv_config *sconf; int is_vip; /* is vip, either by request or by session or by ip */ int set_vip_by_header; /* received vip header from application/or auth. user (propagate to IP store)*/ int has_lowrate; qs_conn_t *conn; } qs_conn_ctx; typedef struct { qs_conn_ctx *cconf; conn_rec *c; qos_srv_config *sconf; int requests; // number of requests processed (received) by this connection apr_socket_t *clientSocket; } qs_conn_base_ctx; /** * request configuration */ typedef struct { qs_acentry_t *entry; qs_acentry_t *entry_cond; apr_table_t *event_entries; char *evmsg; int is_vip; apr_off_t maxpostcount; int cc_event_req_set; apr_uint64_t cc_event_ip[2]; int cc_serialize_set; apr_uint64_t cc_serialize_ip[2]; int srv_serialize_set; char *body_window; apr_off_t response_delayed; // indicates, if the response has been delayed (T) } qs_req_ctx; /** * Delay filter context */ typedef struct { qs_acentry_t *entry; qs_req_ctx *rctx; } qos_delay_ctx_t; /** * rule set */ typedef struct { char *url; char *event; int limit; ap_regex_t *regex; ap_regex_t *regex_var; ap_regex_t *condition; long req_per_sec_limit; apr_off_t kbytes_per_sec_limit; } qs_rule_ctx_t; typedef struct { const char* name; const char* pattern; qs_flt_action_e action; int size; } qos_her_t; typedef struct { ap_regex_t *preg; char *name; char *value; } qos_pregval_t; typedef struct { int num; int thinktime; const char* pattern; ap_regex_t *preg; qs_rfilter_action_e action; } qos_milestone_t; typedef struct { char *text; ap_regex_t *preg; qs_flt_action_e action; int size; } qos_fhlt_r_t; typedef struct { apr_time_t request_time; unsigned int in_addr; unsigned int conn; #if APR_HAS_THREADS apr_os_thread_t tid; #endif unsigned int unique_id_counter; } qos_unique_id_t; /************************************************************************ * Globals ***********************************************************************/ module AP_MODULE_DECLARE_DATA qos_module; static int m_forced_close = 1; static int m_retcode = HTTP_INTERNAL_SERVER_ERROR; static int m_threaded_mpm = 1; // note: mod_qos is fully tested for Apache 2.2 worker MPM only static int m_event_mpm = 0; static double m_event_mpm_worker_factor = 2; static unsigned int m_hostcode = 0; static int m_generation = 0; // parent process (restart generation) static int m_qos_cc_partition = QSMOD; static qos_unique_id_t m_unique_id; static const char qos_basis_64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"; #ifdef QS_INTERNAL_TEST static int m_qs_sim_ip_len = QS_SIM_IP_LEN; #endif #ifdef QS_APACHE_24 APLOG_USE_MODULE(qos); #endif /* mod_parp, forward and optional function */ static apr_status_t qos_cleanup_conn(void *p); static apr_status_t qos_base_cleanup_conn(void *p); static qs_ip_type_e m_ip_type = QS_IP_V6_DEFAULT; APR_DECLARE_OPTIONAL_FN(apr_table_t *, parp_hp_table, (request_rec *)); APR_DECLARE_OPTIONAL_FN(char *, parp_body_data, (request_rec *, apr_size_t *)); static APR_OPTIONAL_FN_TYPE(parp_hp_table) *qos_parp_hp_table_fn = NULL; static APR_OPTIONAL_FN_TYPE(parp_body_data) *qos_parp_body_data_fn = NULL; static int m_requires_parp = 0; static int m_enable_audit = 0; /* mod_ssl, forward and optional function */ APR_DECLARE_OPTIONAL_FN(int, ssl_is_https, (conn_rec *)); static APR_OPTIONAL_FN_TYPE(ssl_is_https) *qos_is_https = NULL; APR_DECLARE_OPTIONAL_FN(char *, ssl_var_lookup, (apr_pool_t *, server_rec *, conn_rec *, request_rec *, char *)); static APR_OPTIONAL_FN_TYPE(ssl_var_lookup) *qos_ssl_var = NULL; static void qs_inc_eventcounter(apr_pool_t *ppool, int event, int locked); #define QS_INC_EVENT(sconf, event) if(sconf->qsevents) qs_inc_eventcounter(sconf->act->ppool, event, 0) #define QS_INC_EVENT_LOCKED(sconf, event) if(sconf->qsevents) qs_inc_eventcounter(sconf->act->ppool, event, 1) static int m_knownEvents[] = { 10, 11, 12, 13, 21, 23, 25, 30, 31, 34, 35, 36, 37, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 60, 61, 62, 65, 66, 67, 68, 69, 101, 147, 0 }; /* simple header rules allowing "the usual" header formats only (even drop requests using extensions which are used rarely) */ /* reserved (to be escaped): {}[]()^$.|*+?\ */ static const qos_her_t qs_header_rules[] = { #define QS_URL_UNRESERVED "a-zA-Z0-9._~% -" #define QS_URL_GEN ":/?#\\[\\]@" #define QS_URL_SUB "!$&'()*+,;=" #define QS_URL "["QS_URL_GEN""QS_URL_SUB""QS_URL_UNRESERVED"]" #define QS_2616TOKEN "[\\x21\\x23-\\x27\\x2a-\\x2e0-9A-Z\\x5-\\x60a-z\\x7e]+" #define QS_B64_SP "[a-zA-Z0-9 +/$=:]" #define QS_PIPE "\\|" #define QS_WEAK "(W/)?" #define QS_H_ACCEPT "[a-zA-Z0-9_*+-]+/[a-zA-Z0-9_*+.-]+(;[ ]?[a-zA-Z0-9]+=[0-9]+)?[ ]?(;[ ]?[qv]=[a-z0-9.]+)?" #define QS_H_ACCEPT_C "[a-zA-Z0-9*-]+(;[ ]?q=[0-9.]+)?" #define QS_H_ACCEPT_E "[a-zA-Z0-9*-]+(;[ ]?q=[0-9.]+)?" #define QS_H_ACCEPT_L "[a-zA-Z*-]+[0-9]{0,3}(;[ ]?q=[0-9.]+)?" #define QS_H_CACHE "no-cache|no-store|max-age=[0-9]+|max-stale(=[0-9]+)?|min-fresh=[0-9]+|no-transform|only-if-chached" #define QS_H_CONTENT "[\"a-zA-Z0-9*/; =-]+" #define QS_H_COOKIE "["QS_URL_GEN""QS_URL_SUB"\""QS_URL_UNRESERVED"]" #define QS_H_EXPECT "[a-zA-Z0-9= ;.,-]" #define QS_H_PRAGMA "[a-zA-Z0-9= ;.,-]" #define QS_H_FROM "[a-zA-Z0-9=@;.,()-]" #define QS_H_HOST "[a-zA-Z0-9.-]+(:[0-9]+)?" #define QS_H_IFMATCH "[a-zA-Z0-9=@;.,*\"-]" #define QS_H_DATE "[a-zA-Z0-9 :,]" #define QS_H_TE "[a-zA-Z0-9*-]+(;[ ]?q=[0-9.]+)?" { "Accept", "^("QS_H_ACCEPT"){1}([ ]?,[ ]?("QS_H_ACCEPT"))*$", QS_FLT_ACTION_DROP, 300 }, { "Accept-Charset", "^("QS_H_ACCEPT_C"){1}([ ]?,[ ]?("QS_H_ACCEPT_C"))*$", QS_FLT_ACTION_DROP, 300 }, { "Accept-Encoding", "^("QS_H_ACCEPT_E"){1}([ ]?,[ ]?("QS_H_ACCEPT_E"))*$", QS_FLT_ACTION_DROP, 500 }, { "Accept-Language", "^("QS_H_ACCEPT_L"){1}([ ]?,[ ]?("QS_H_ACCEPT_L"))*$", QS_FLT_ACTION_DROP, 200 }, { "Access-Control-Request-Method", "^[a-zA-Z]+$", QS_FLT_ACTION_DROP, 10 }, { "Access-Control-Request-Headers", "^([a-zA-Z0-9-]+){1}([ ]?,[ ]?([a-zA-Z0-9-]+))*$", QS_FLT_ACTION_DROP, 500 }, { "Authorization", "^"QS_B64_SP"+$", QS_FLT_ACTION_DROP, 4000 }, { "Cache-Control", "^("QS_H_CACHE"){1}([ ]?,[ ]?("QS_H_CACHE"))*$", QS_FLT_ACTION_DROP, 100 }, { "Connection", "^([teTE]+,[ ]?)?([a-zA-Z0-9-]+){1}([ ]?,[ ]?([teTE]+))?$", QS_FLT_ACTION_DROP, 100 }, { "Content-Encoding", "^[a-zA-Z0-9-]+(,[ ]*[a-zA-Z0-9-]+)*$", QS_FLT_ACTION_DENY, 100 }, { "Content-Language", "^([0-9a-zA-Z]{0,8}(-[0-9a-zA-Z]{0,8})*)(,[ ]*([0-9a-zA-Z]{0,8}(-[0-9a-zA-Z]{0,8})*))*$", QS_FLT_ACTION_DROP, 100 }, { "Content-Length", "^[0-9]+$", QS_FLT_ACTION_DENY, 10 }, { "Content-Location", "^"QS_URL"+$", QS_FLT_ACTION_DENY, 200 }, { "Content-md5", "^"QS_B64_SP"+$", QS_FLT_ACTION_DENY, 50 }, { "Content-Range", "^(bytes[ ]+([0-9]+-[0-9]+)/([0-9]+|\\*))$", QS_FLT_ACTION_DENY, 50 }, { "Content-Type", "^("QS_H_CONTENT"){1}([ ]?,[ ]?("QS_H_CONTENT"))*$", QS_FLT_ACTION_DENY, 200 }, { "Cookie", "^"QS_H_COOKIE"+$", QS_FLT_ACTION_DROP, 3000 }, { "Cookie2", "^"QS_H_COOKIE"+$", QS_FLT_ACTION_DROP, 3000 }, { "DNT", "^[0-9]+$", QS_FLT_ACTION_DROP, 3 }, { "Expect", "^"QS_H_EXPECT"+$", QS_FLT_ACTION_DROP, 200 }, { "From", "^"QS_H_FROM"+$", QS_FLT_ACTION_DROP, 100 }, { "Host", "^"QS_H_HOST"$", QS_FLT_ACTION_DROP, 100 }, { "If-Invalid", "^[a-zA-Z0-9_.:;() /+!-]+$", QS_FLT_ACTION_DROP, 500 }, { "If-Match", "^"QS_WEAK""QS_H_IFMATCH"+$", QS_FLT_ACTION_DROP, 100 }, { "If-Modified-Since", "^"QS_H_DATE"+$", QS_FLT_ACTION_DROP, 100 }, { "If-None-Match", "^"QS_WEAK""QS_H_IFMATCH"+$", QS_FLT_ACTION_DROP, 100 }, { "If-Range", "^"QS_H_IFMATCH"+$", QS_FLT_ACTION_DROP, 100 }, { "If-Unmodified-Since", "^"QS_H_DATE"+$", QS_FLT_ACTION_DROP, 100 }, { "If-Valid", "^[a-zA-Z0-9_.:;() /+!-]+$", QS_FLT_ACTION_DROP, 500 }, { "Keep-Alive", "^[0-9]+$", QS_FLT_ACTION_DROP, 20 }, { "Max-Forwards", "^[0-9]+$", QS_FLT_ACTION_DROP, 20 }, { "Origin", "^"QS_URL"+$", QS_FLT_ACTION_DROP, 2000 }, { "Proxy-Authorization", "^"QS_B64_SP"+$", QS_FLT_ACTION_DROP, 400 }, { "Pragma", "^"QS_H_PRAGMA"+$", QS_FLT_ACTION_DROP, 200 }, { "Range", "^[a-zA-Z0-9=_.:;() /+!-]+$", QS_FLT_ACTION_DROP, 200 }, { "Referer", "^"QS_URL"+$", QS_FLT_ACTION_DROP, 2000 }, { "TE", "^("QS_H_TE"){1}([ ]?,[ ]?("QS_H_TE"))*$", QS_FLT_ACTION_DROP, 100 }, { "Transfer-Encoding", "^(chunked|Chunked|compress|Compress|deflate|Deflate|gzip|Gzip|identity|Identity)([ ]?,[ ]?(chunked|Chunked|compress|Compress|deflate|Deflate|gzip|Gzip|identity|Identity))*$", QS_FLT_ACTION_DENY, 100 }, { "Unless-Modified-Since", "^"QS_H_DATE"+$", QS_FLT_ACTION_DROP, 100 }, { "User-Agent", "^[a-zA-Z0-9]+[a-zA-Z0-9_.:;()\\[\\]@ /+!=,-]+$", QS_FLT_ACTION_DROP, 300 }, { "Upgrade-Insecure-Requests", "^1$", QS_FLT_ACTION_DROP, 1 }, { "Via", "^[a-zA-Z0-9_.:;() /+!-]+$", QS_FLT_ACTION_DROP, 100 }, { "X-Forwarded-For", "^[a-zA-Z0-9_.:-]+(, [a-zA-Z0-9_.:-]+)*$", QS_FLT_ACTION_DROP, 100 }, { "X-Forwarded-Host", "^[a-zA-Z0-9_.:-]+$", QS_FLT_ACTION_DROP, 100 }, { "X-Forwarded-Server", "^[a-zA-Z0-9_.:-]+$", QS_FLT_ACTION_DROP, 100 }, { "X-lori-time-1", "^[0-9]+$", QS_FLT_ACTION_DROP, 20 }, { "X-Do-Not-Track", "^[0-9]+$", QS_FLT_ACTION_DROP, 20 }, { NULL, NULL, 0, 0 } }; /* list of allowed standard response headers */ static const qos_her_t qs_res_header_rules[] = { { "Age", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Accept-Ranges", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Access-Control-Allow-Origin", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Access-Control-Allow-Methods", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Access-Control-Allow-Headers", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Access-Control-Max-Age", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Access-Control-Allow-Credentials", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Access-Control-Expose-Headers", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Allow", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Cache-Control", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Content-Disposition", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Content-Encoding", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Content-Language", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Content-Length", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Content-Location", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Content-MD5", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Content-Range", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Content-Security-Policy", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 8000 }, { "Content-Type", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Connection", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Date", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "ETag", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Expect", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Expires", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Keep-Alive", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Last-Modified", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Location", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Proxy-Authenticate", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Public-Key-Pins", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Public-Key-Pins-Report-Only", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Retry-After", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Pragma", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Server", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Set-Cookie", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Set-Cookie2", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Strict-Transport-Security", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "Vary", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "WWW-Authenticate", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "X-Content-Security-Policy", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 8000 }, { "X-Content-Type-Options", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "X-Frame-Options", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { "X-XSS-Protection", "^[\\x20-\\xFF]*$", QS_FLT_ACTION_DROP, 4000 }, { NULL, NULL, 0, 0 } }; /************************************************************************ * private functions ***********************************************************************/ static int qos_regexec_len(apr_pool_t *pool, const ap_regex_t *preg, const char *buff, apr_size_t len) { #if (AP_SERVER_MINORVERSION_NUMBER < 4) && defined QS_INTERNAL_TEST // Apache 2.2 is no longer supported (test only) char *data = apr_palloc(pool, len + 1); memcpy(data, buff, len); data[len] = '\0'; return ap_regexec(preg, data, 0, NULL, 0); // won't work for null chars! #else return ap_regexec_len(preg, buff, len, 0, NULL, 0); #endif } /** * Converts an ip long array back to a string representation * * @param pool * @param src Array of two unsigned long * @return String or null for an invalid address */ static char *qos_ip_long2str(apr_pool_t *pool, const void *src) { char *dst = apr_pcalloc(pool, INET6_ADDRSTRLEN); char *ret = (char *)inet_ntop(AF_INET6, src, dst, INET6_ADDRSTRLEN); if(ret) { if((strncmp(ret, QS_IP4IN6, 7) == 0) && strchr(ret, '.')) { ret = &ret[7]; } } return ret; } /** * Converts an ip string to long array (128 bit) representation * * @param src String representation, e.g. 139.12.33.1 or 1::8 * @param dst Pointer to array of unsigned long (2) (contains "{ 0, 0 }" on error) * @return 1 on success, 0 on error */ static int qos_ip_str2long(const char *src, apr_uint64_t *dst) { char str[INET6_ADDRSTRLEN]; const char *convert = src; apr_uint64_t *n = dst; n[0] = 0; n[1] = 0; if(convert == NULL) { return 0; } if((strchr(convert, ':') == NULL) && (strlen(convert) <= 15)) { // looks like an IPv4 address sprintf(str, QS_IP4IN6"%s", src); convert = str; } return inet_pton(AF_INET6, convert, dst); } static int qos_encode64_binary(char *encoded, const char *string, int len) { int i; char *p; p = encoded; for (i = 0; i < len - 2; i += 3) { *p++ = qos_basis_64[(string[i] >> 2) & 0x3F]; *p++ = qos_basis_64[((string[i] & 0x3) << 4) | ((int) (string[i + 1] & 0xF0) >> 4)]; *p++ = qos_basis_64[((string[i + 1] & 0xF) << 2) | ((int) (string[i + 2] & 0xC0) >> 6)]; *p++ = qos_basis_64[string[i + 2] & 0x3F]; } if (i < len) { *p++ = qos_basis_64[(string[i] >> 2) & 0x3F]; if (i == (len - 1)) { *p++ = qos_basis_64[((string[i] & 0x3) << 4)]; *p++ = '='; } else { *p++ = qos_basis_64[((string[i] & 0x3) << 4) | ((int) (string[i + 1] & 0xF0) >> 4)]; *p++ = qos_basis_64[((string[i + 1] & 0xF) << 2)]; } *p++ = '='; } *p++ = '\0'; return (int)(p - encoded); } /** * loads the default header rules into the server configuration (see rules * above). * @param pool To allocate memory * @param outHdrFltTable Table to add rules to * @param hdrFltRuleDefArray built-in header rules * @return error message (NULL on success) */ static char *qos_load_headerfilter(apr_pool_t *pool, apr_table_t *outHdrFltTable, const qos_her_t *hdrFltRuleDefArray) { const qos_her_t* hdrFltRuleDefEntry; for(hdrFltRuleDefEntry = hdrFltRuleDefArray; hdrFltRuleDefEntry->name != NULL ; ++hdrFltRuleDefEntry) { qos_fhlt_r_t *hdrFltElement = apr_pcalloc(pool, sizeof(qos_fhlt_r_t)); hdrFltElement->text = apr_pstrdup(pool, hdrFltRuleDefEntry->pattern); hdrFltElement->preg = ap_pregcomp(pool, hdrFltRuleDefEntry->pattern, AP_REG_DOTALL); hdrFltElement->action = hdrFltRuleDefEntry->action; hdrFltElement->size = hdrFltRuleDefEntry->size; if(hdrFltElement->preg == NULL) { return apr_psprintf(pool, "could not compile regular expression '%s' for %s header", hdrFltElement->text, hdrFltRuleDefEntry->name); } apr_table_setn(outHdrFltTable, hdrFltRuleDefEntry->name, (char *)hdrFltElement); } return NULL; } /** * Returns string representation of filter type (for logging purposes) * @param pool To allocate string * @param type Rule type * @return Name of the directive used to configure the rule */ static char *qos_rfilter_type2text(apr_pool_t *pool, qs_rfilter_type_e type) { if(type == QS_DENY_REQUEST_LINE) return apr_pstrdup(pool, "QS_DenyRequestLine"); if(type == QS_DENY_PATH) return apr_pstrdup(pool, "QS_DenyPath"); if(type == QS_DENY_QUERY) return apr_pstrdup(pool, "QS_DenyQuery"); if(type == QS_DENY_EVENT) return apr_pstrdup(pool, "QS_DenyEvent"); if(type == QS_PERMIT_URI) return apr_pstrdup(pool, "QS_PermitUri"); return apr_pstrdup(pool, "UNKNOWN"); } /** * Sets unique apache instance id (hopefully) to the global m_hostcore variable * @param ptemp Pool to allocate memory from * @param s Base server record */ static void qos_hostcode(apr_pool_t *ptemp, server_rec *s) { char *key = apr_psprintf(ptemp, "%s%s%s%d%s" #ifdef ap_http_scheme /* Apache 2.2 */ "%s" #endif "%s", s->defn_name ? s->defn_name : "", s->server_admin ? s->server_admin : "", s->server_hostname ? s->server_hostname : "", s->addrs ? s->addrs->host_port : 0, s->path ? s->path : "", s->error_fname ? s->error_fname : "" #ifdef ap_http_scheme /* Apache 2.2 */ ,s->server_scheme ? s->server_scheme : "" #endif ); int len = strlen(key); int i; char *p; for(p = key, i = len; i; i--, p++) { m_hostcode = m_hostcode * 33 + *p; } } /** * temp file name for the main/virtual serve * @param pool Pool to allocate the file name from * @param s Server record * @return absolute file name */ static char *qos_tmpnam(apr_pool_t *pool, server_rec *s) { qos_srv_config *sconf = (qos_srv_config*)ap_get_module_config(s->module_config, &qos_module); const char *path = NULL; char *ret; char *file; if(apr_temp_dir_get(&path, pool) != APR_SUCCESS) { path = apr_pstrdup(pool, "/var/tmp"); } if(sconf && sconf->mfile) { path = sconf->mfile; } ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, QOS_LOGD_PFX"temporary directory for semaphores/shared memory: %s" " (use QS_SemMemFile to override it).", path); if(s) { unsigned int scode = 0; char *key = apr_psprintf(pool, "%u%s.%s.%d", m_hostcode, s->is_virtual ? "v" : "b", s->server_hostname == NULL ? "-" : s->server_hostname, s->addrs == NULL ? 0 : s->addrs->host_port); int len = strlen(key); int i; char *p; for(p = key, i = len; i; i--, p++) { scode = scode * 33 + *p; } file = apr_psprintf(pool, "%u", scode); } else { file = apr_psprintf(pool, "%u", m_hostcode); } file[0] += 25; /* non numeric */ apr_filepath_merge(&ret, path, file, APR_FILEPATH_NATIVE, pool); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, QOS_LOGD_PFX"temporary file: %s", ret); return ret; } /** * QS_LimitRequestBody settings. Environment variable (dynamic) has higher prio than * configuration (static) value. * @param r * @param sconf * @param dconf */ static apr_off_t qos_maxpost(request_rec *r, qos_srv_config *sconf, qos_dir_config *dconf) { if(r->subprocess_env) { const char *bytes = apr_table_get(r->subprocess_env, "QS_LimitRequestBody"); if(bytes) { apr_off_t s; #ifdef ap_http_scheme /* Apache 2.2 */ char *errp = NULL; if(APR_SUCCESS == apr_strtoff(&s, bytes, &errp, 10)) { return s; } #else if((s = apr_atoi64(bytes)) >= 0) { return s; } #endif } } if(dconf->maxpost != -1) { return dconf->maxpost; } return sconf->maxpost; } /** * Similar to strstr but restricting the length of s1 (supports strings which * are not NULL terminated). * * @param s1 String to search in * @param s2 Pattern to ind * @param len Length of s1 * @return pointer to the beginning of the substring s2 within s1, or NULL * if the substring is not found */ static char *qos_strnstr(const char *s1, const char *s2, int len) { const char *e1 = &s1[len-1]; char *p1, *p2; if (*s2 == '\0') { /* an empty s2 */ return((char *)s1); } while(1) { for ( ; (*s1 != '\0') && (s1 <= e1) && (apr_tolower(*s1) != apr_tolower(*s2)); s1++); if (*s1 == '\0' || s1 > e1) { return(NULL); } /* found first character of s2, see if the rest matches */ p1 = (char *)s1; p2 = (char *)s2; for (++p1, ++p2; (apr_tolower(*p1) == apr_tolower(*p2)) && (p1 <= e1); ++p1, ++p2) { if((p1 > e1) && (*p2 != '\0')) { // reached the end without match return NULL; } if (*p2 == '\0') { /* both strings ended together */ return((char *)s1); } } if (*p2 == '\0') { /* second string ended, a match */ break; } /* didn't find a match here, try starting at next character in s1 */ s1++; } return((char *)s1); } /** * Determines, if the client IP shall be excluded from rule enforcement * * @param connection Connection to get the IP * @param exclude_ip Table containing the rules * @return 1 on match otherwise 0 */ static int qos_is_excluded_ip(conn_rec *connection, apr_table_t *exclude_ip) { conn_rec *c = connection; if(apr_table_elts(exclude_ip)->nelts > 0) { int i; apr_table_entry_t *entry = (apr_table_entry_t *)apr_table_elts(exclude_ip)->elts; for(i = 0; i < apr_table_elts(exclude_ip)->nelts; i++) { if(entry[i].val[0] == 'r') { if(strncmp(entry[i].key, QS_CONN_REMOTEIP(c), strlen(entry[i].key)) == 0) { return 1; } } else { if(strcmp(entry[i].key, QS_CONN_REMOTEIP(c)) == 0) { return 1; } } } } return 0; } /** * Comperator (ip search) for the client ip store qos_cc_*() * functions (used by bsearch/qsort) */ static int qos_cc_comp(const void *_pA, const void *_pB) { qos_s_entry_t *pA=*(( qos_s_entry_t **)_pA); qos_s_entry_t *pB=*(( qos_s_entry_t **)_pB); if(pA->ip6[0] > pB->ip6[0]) return 2; if(pA->ip6[0] < pB->ip6[0]) return -2; if(pA->ip6[1] > pB->ip6[1]) return 1; if(pA->ip6[1] < pB->ip6[1]) return -1; return 0; } static int qos_cc_compv4(const void *_pA, const void *_pB) { qos_s_entry_t *pA=*(( qos_s_entry_t **)_pA); qos_s_entry_t *pB=*(( qos_s_entry_t **)_pB); if(pA->ip6[1] > pB->ip6[1]) return 1; if(pA->ip6[1] < pB->ip6[1]) return -1; return 0; } /** * Comperator (time search) for the client ip store qos_cc_*() * functions (used by bsearch/qsort) */ static int qos_cc_comp_time(const void *_pA, const void *_pB) { qos_s_entry_t *pA=*(( qos_s_entry_t **)_pA); qos_s_entry_t *pB=*(( qos_s_entry_t **)_pB); if(pA->time > pB->time) return 1; if(pA->time < pB->time) return -1; return 0; } /** * creates new per client store * @param pool Persistent process pool * @param srec Server rec for sem/mutex * @param size Number of entries * @param limitTable Table of "QS_Limit" events * @return pointer to the per client data array */ static qos_s_t *qos_cc_new(apr_pool_t *pool, server_rec *srec, int size, apr_table_t *limitTable) { char *file = "-"; apr_shm_t *clientMem; // per client memory table apr_shm_t *limitMem; // "limit" memory table apr_status_t res; int limitTableSize = apr_table_elts(limitTable)->nelts; int limitMemSize = 0; int clientMemSize = APR_ALIGN_DEFAULT(sizeof(qos_s_t)) + (APR_ALIGN_DEFAULT(sizeof(qos_s_entry_t)) * size) + (2 * APR_ALIGN_DEFAULT(sizeof(qos_s_entry_t *)) * size); int i; qos_s_t *clientDataArray; qos_s_entry_t *clientDataEntry; qos_s_entry_limit_t *limitTableEntry = NULL; clientMemSize = clientMemSize + 1024; if(limitTableSize > 0) { limitMemSize = APR_ALIGN_DEFAULT(sizeof(qos_s_entry_limit_t)) * limitTableSize * size; limitMemSize = limitMemSize + 1024; } /* use anonymous shm by default */ if(limitTableSize > 0) { apr_shm_create(&limitMem, limitMemSize, NULL, pool); } res = apr_shm_create(&clientMem, clientMemSize, NULL, pool); if(APR_STATUS_IS_ENOTIMPL(res)) { char *lfile = apr_psprintf(pool, "%s_cc_ml.mod_qos", qos_tmpnam(pool, srec)); file = apr_psprintf(pool, "%s_cc_m.mod_qos", qos_tmpnam(pool, srec)); #ifdef ap_http_scheme /* Apache 2.2 */ if(limitTableSize > 0) { apr_shm_remove(lfile, pool); } apr_shm_remove(file, pool); #endif if(limitTableSize > 0) { apr_shm_create(&limitMem, limitMemSize, lfile, pool); } res = apr_shm_create(&clientMem, clientMemSize, file, pool); } ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, srec, QOS_LOGD_PFX"create shared memory (client control)(%s): %d bytes", file, clientMemSize + limitMemSize); if(res != APR_SUCCESS) { char buf[MAX_STRING_LEN]; apr_strerror(res, buf, sizeof(buf)); ap_log_error(APLOG_MARK, APLOG_EMERG, 0, srec, QOS_LOG_PFX(002)"failed to create shared memory (client control)(%s): " "%s (%d bytes)", file, buf, clientMemSize); return NULL; } clientDataArray = apr_shm_baseaddr_get(clientMem); clientDataArray->m = clientMem; clientDataArray->generation_locked = -1; if(limitTableSize > 0) { apr_table_entry_t *te = (apr_table_entry_t *)apr_table_elts(limitTable)->elts; limitTableEntry = apr_shm_baseaddr_get(limitMem); clientDataArray->limitTable = apr_table_make(pool, limitTableSize+10); for(i = 0; i < limitTableSize; i++) { char *eventName = apr_pstrdup(pool, te[i].key); qos_s_entry_limit_conf_t *eventLimitConf = apr_pcalloc(pool, sizeof(qos_s_entry_limit_conf_t)); qos_s_entry_limit_conf_t *src = (qos_s_entry_limit_conf_t*)te[i].val; eventLimitConf->limit = src->limit; eventLimitConf->limitTime = src->limitTime; eventLimitConf->eventClearStr = apr_pstrcat(pool, eventName, QS_LIMIT_CLEAR, NULL); eventLimitConf->eventDecStr = apr_pstrcat(pool, eventName, QS_LIMIT_DEC, NULL); eventLimitConf->condStr = NULL; eventLimitConf->preg = NULL; if(src->condStr) { eventLimitConf->condStr = apr_pstrdup(pool, src->condStr); eventLimitConf->preg = ap_pregcomp(pool, src->condStr, AP_REG_EXTENDED); } apr_table_addn(clientDataArray->limitTable, eventName, (char *)eventLimitConf); } } else { clientDataArray->limitTable = NULL; } clientDataArray->lock_file = apr_psprintf(pool, "%s_ccl.mod_qos", qos_tmpnam(pool, srec)); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, srec, QOS_LOGD_PFX"create mutex (client control)(%s)", clientDataArray->lock_file); res = apr_global_mutex_create(&clientDataArray->lock, clientDataArray->lock_file, APR_LOCK_DEFAULT, pool); if(res != APR_SUCCESS) { char buf[MAX_STRING_LEN]; apr_strerror(res, buf, sizeof(buf)); ap_log_error(APLOG_MARK, APLOG_EMERG, 0, srec, QOS_LOG_PFX(004)"failed to create mutex (client control)(%s): %s", clientDataArray->lock_file, buf); apr_shm_destroy(clientDataArray->m); return NULL; } #ifdef AP_NEED_SET_MUTEX_PERMS qos_unixd_set_global_mutex_perms(clientDataArray->lock); #endif clientDataEntry = (qos_s_entry_t *)&clientDataArray[1]; clientDataArray->ipd = (qos_s_entry_t **)&clientDataEntry[size]; clientDataArray->timed = (qos_s_entry_t **)&clientDataArray->ipd[size]; clientDataArray->num = 0; clientDataArray->max = size; clientDataArray->msize = clientMemSize; clientDataArray->connections = 0; clientDataArray->html = 0; clientDataArray->cssjs = 0; clientDataArray->img = 0; clientDataArray->other = 0; clientDataArray->notmodified = 0; for(i = 0; i < size; i++) { clientDataArray->ipd[i] = clientDataEntry; clientDataArray->timed[i] = clientDataEntry; if(limitTableSize > 0) { clientDataEntry->limit = limitTableEntry; limitTableEntry += limitTableSize; } else { clientDataEntry->limit = NULL; } clientDataEntry++; } clientDataArray->t = time(NULL); for(i = 0; i < QOS_LOG_MSGCT; i++) { clientDataArray->eventTotal[i] = 0; clientDataArray->eventLast[i] = 0; } return clientDataArray; } /** * Destroys the client data store */ static void qos_cc_free(qos_s_t *s) { /* We don't need to destroy locks or shared memory manually as apr_global_mutex_create() and apr_shm_create() register the cleanup methods themself to the pool we used when allocating the locks/memory. if(s->lock) { apr_global_mutex_destroy(s->lock); } if(s->m) { apr_shm_destroy(s->m); } if(s->lm) { apr_shm_destroy(s->lm); } */ } /** * searches an entry * @param s Client store (locked) * @param pA IP to search * @param now Current time (update access to the entry) * @return client entry or NULL if not available */ static qos_s_entry_t **qos_cc_get0(qos_s_t *s, qos_s_entry_t *pA, time_t now) { qos_s_entry_t **pB; unsigned char *b = (void *)&pA->ip6[1]; int mod = b[7] % m_qos_cc_partition; int max = (s->max / m_qos_cc_partition); int start = mod * max; if(m_ip_type == QS_IP_V4) { pB = bsearch((const void *)&pA, (const void *)&s->ipd[start], max, sizeof(qos_s_entry_t *), qos_cc_compv4); } else { pB = bsearch((const void *)&pA, (const void *)&s->ipd[start], max, sizeof(qos_s_entry_t *), qos_cc_comp); } if(pB) { if(now != 0) { s->t = now; } (*pB)->time = s->t; } return pB; } /** * inserts a new entry to the client data store * @param s Client store (locked) * @param pA IP to insert * @param now Current time (last access) * @return inserted entry */ static qos_s_entry_t **qos_cc_set(qos_s_t *s, qos_s_entry_t *pA, time_t now) { qos_s_entry_t **pB; unsigned char *b = (void *)&pA->ip6[1]; int mod = b[7] % m_qos_cc_partition; int max = (s->max / m_qos_cc_partition); int start = mod * max; s->t = now; qsort(&s->timed[start], max, sizeof(qos_s_entry_t *), qos_cc_comp_time); if(s->num < s->max) { s->num++; } pB = &s->timed[start]; (*pB)->ip6[0] = pA->ip6[0]; (*pB)->ip6[1] = pA->ip6[1]; (*pB)->time = now; if(m_ip_type == QS_IP_V4) { qsort(&s->ipd[start], max, sizeof(qos_s_entry_t *), qos_cc_compv4); } else { qsort(&s->ipd[start], max, sizeof(qos_s_entry_t *), qos_cc_comp); } (*pB)->vip = 0; (*pB)->lowrate = 0; (*pB)->lowratestatus = 0; (*pB)->block = 0; (*pB)->blockMsg = 0; (*pB)->blockTime = 0; if(s->limitTable) { int i; for(i = 0; i < apr_table_elts(s->limitTable)->nelts; i++) { (*pB)->limit[i].limit = 0; (*pB)->limit[i].limitTime = 0; } } (*pB)->interval = now; (*pB)->req = 0; (*pB)->req_per_sec = 0; (*pB)->req_per_sec_block_rate = 0; (*pB)->event_req = 0; (*pB)->serialize = 0; (*pB)->serializeQueue = 0; (*pB)->html = 1; (*pB)->cssjs = 1; (*pB)->img = 1; (*pB)->other = 1; (*pB)->notmodified = 1; (*pB)->events = 0; return pB; } /** * searches or inserts (if not available) an entry from/to the client data store * @param s Client store (locked) * @param pA IP to insert * @param now Current time (last access) * @return inserted entry */ static qos_s_entry_t **qos_cc_getOrSet(qos_s_t *s, qos_s_entry_t *pA, time_t now) { qos_s_entry_t **clientEntry = clientEntry = qos_cc_get0(s, pA, now); if(!clientEntry) { clientEntry = qos_cc_set(s, pA, now != 0 ? now : time(NULL)); } return clientEntry; } static int qos_isnum(const char *x) { const char *p = x; if(x == NULL || x[0] == 0) { return 0; } while(p && p[0]) { if(!apr_isdigit(p[0])) { return 0; } p++; } return 1; } /* 000-255 */ int qos_dec32c(const char *x) { char buf[4]; strncpy(buf, x, 3); buf[3] = '\0'; return atoi(buf); } int qos_dec22c(const char *x) { char buf[4]; strncpy(buf, x, 2); buf[2] = '\0'; return atoi(buf); } /** * hex value for the char * @param x * @return hex value */ int qos_hex2c(const char *x) { int i, ch; ch = x[0]; if (isdigit(ch)) { i = ch - '0'; }else if (isupper(ch)) { i = ch - ('A' - 10); } else { i = ch - ('a' - 10); } i <<= 4; ch = x[1]; if (isdigit(ch)) { i += ch - '0'; } else if (isupper(ch)) { i += ch - ('A' - 10); } else { i += ch - ('a' - 10); } return i; } #define QOS_ISHEX(x) (((x >= '0') && (x <= '9')) || \ ((x >= 'a') && (x <= 'f')) || \ ((x >= 'A') && (x <= 'F'))) /** * url unescaping (%xx, \xHH, '+') * optional decoding: * - uni: MS IIS unicode %uXXXX * - ansi: ansi c esc (\n, \r, ...), not implemented * - char: charset conv, not implemented * - html: (amp/angelbr, &#xHH;, &#DDD;, &#DD;), not implemented ('&' is delimiter) */ static int qos_unescaping(char *x, int mode, int *error) { /* start with standard url decoding*/ int i, j, ch; if(x == 0) { return 0; } if(x[0] == '\0') { return 0; } for(i = 0, j = 0; x[i] != '\0'; i++, j++) { ch = x[i]; if(ch == '%') { if(QOS_ISHEX(x[i + 1]) && QOS_ISHEX(x[i + 2])) { /* url %xx */ ch = qos_hex2c(&x[i + 1]); i += 2; } else if((mode & QOS_DEC_MODE_FLAGS_UNI) && ((x[i + 1] == 'u') || (x[i + 1] == 'U')) && QOS_ISHEX(x[i + 2]) && QOS_ISHEX(x[i + 3]) && QOS_ISHEX(x[i + 4]) && QOS_ISHEX(x[i + 5])) { /* unicode %uXXXX */ ch = qos_hex2c(&x[i + 4]); if((ch > 0x00) && (ch < 0x5f) && ((x[i + 2] == 'f') || (x[i + 2] == 'F')) && ((x[i + 3] == 'f') || (x[i + 3] == 'F'))) { ch += 0x20; } i += 5; } else { (*error)++; } } else if((ch == '\\') && (mode & QOS_DEC_MODE_FLAGS_UNI) && ((x[i + 1] == 'u') || (x[i + 1] == 'U'))) { if(QOS_ISHEX(x[i + 2]) && QOS_ISHEX(x[i + 3]) && QOS_ISHEX(x[i + 4]) && QOS_ISHEX(x[i + 5])) { /* unicode \uXXXX */ ch = qos_hex2c(&x[i + 4]); if((ch > 0x00) && (ch < 0x5f) && ((x[i + 2] == 'f') || (x[i + 2] == 'F')) && ((x[i + 3] == 'f') || (x[i + 3] == 'F'))) { ch += 0x20; } i += 5; } else { (*error)++; } } else if(ch == '\\' && (x[i + 1] == 'x')) { if(QOS_ISHEX(x[i + 2]) && QOS_ISHEX(x[i + 3])) { /* url \xHH */ ch = qos_hex2c(&x[i + 2]); i += 3; } else { (*error)++; } } else if(ch == '+') { ch = ' '; } x[j] = ch; } x[j] = '\0'; return j; } /** * returns the request id from mod_unique_id (if available) */ static const char *qos_unique_id(request_rec *r, const char *eid) { const char *uid = apr_table_get(r->subprocess_env, "UNIQUE_ID"); if(eid) { apr_table_set(r->notes, "error-notes", eid); apr_table_set(r->subprocess_env, QS_ErrorNotes, eid); } if(uid == NULL) { /* generate simple id if mod_unique_id has not been not loaded */ qos_unique_id_t id; char *uidstr; int len; m_unique_id.unique_id_counter++; id.request_time = r->request_time; id.in_addr = m_unique_id.in_addr; #if APR_HAS_THREADS id.tid = apr_os_thread_current(); #endif id.conn = r->connection->id; id.unique_id_counter = m_unique_id.unique_id_counter; uidstr = (char *)apr_pcalloc(r->pool, apr_base64_encode_len(sizeof(qos_unique_id_t))); len = qos_encode64_binary(uidstr, (const char *)&id, sizeof(qos_unique_id_t)); uidstr[len-2] = (id.unique_id_counter%8)+50; uid = uidstr; apr_table_set(r->subprocess_env, "UNIQUE_ID", uid); } return uid; } static void qos_log_env(request_rec *r, const char *handler) { char *msg = ""; int i; apr_table_entry_t *e = (apr_table_entry_t *) apr_table_elts(r->subprocess_env)->elts; for (i = 0; i < apr_table_elts(r->subprocess_env)->nelts; ++i) { msg = apr_psprintf(r->pool, "%s=%s;%s", e[i].key, e[i].val, msg); } ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_NOTICE, 0, r, QOS_LOG_PFX(210)"ENV %s %s %s", handler, msg, qos_unique_id(r, NULL)); return; } static char *qos_ipv6_hash(request_rec *r, const char *var) { char *md = ap_md5_binary(r->pool, (unsigned char *)var, strlen(var)); char *hash = apr_pcalloc(r->pool, 64); char *d = hash; int c = 0; while(md[0]) { d[0] = md[0]; d++; c++; md++; if(c == 4 && md[0]) { c=0; d[0] = ':'; d++; } } d[0] = '\0'; return hash; } static const char *qos_forwardedfor_fromHeader(request_rec *r, const char *header) { const char *forwardedfor = apr_table_get(r->headers_in, header); if(forwardedfor == NULL && r->prev) { // internal redirect? forwardedfor = apr_table_get(r->prev->headers_in, header); } if(forwardedfor == NULL && r->main) { // internal redirect? forwardedfor = apr_table_get(r->main->headers_in, header); } return forwardedfor; } static const char *qos_forwardedfor_fromSSL(request_rec *r) { if(qos_ssl_var) { const char *dn = qos_ssl_var(r->pool, r->server, r->connection, r, "SSL_CLIENT_S_DN"); const char *issuer = qos_ssl_var(r->pool, r->server, r->connection, r, "SSL_CLIENT_I_DN"); char *header = apr_pstrcat(r->pool, dn, issuer, NULL); if(header && header[0]) { return header; } } return NULL; } static const char *qos_pseudoip(request_rec *r, const char *header) { const char *forwardedfor = NULL; if(strcmp("SSL_CLIENT_S_DN", header) == 0) { forwardedfor = qos_forwardedfor_fromSSL(r); } else { forwardedfor = qos_forwardedfor_fromHeader(r, header); } if(forwardedfor && forwardedfor[0]) { return qos_ipv6_hash(r, forwardedfor); } return NULL; } static const char *qos_forwardedfor(request_rec *r, const char *header) { const char *forwardedfor = NULL; if(header[0] == '#') { forwardedfor = qos_pseudoip(r, &header[1]); } else { forwardedfor = qos_forwardedfor_fromHeader(r, header); } return forwardedfor; } /** * Returns the client IP, either from the connection * of from the forwarded-for header if configured * * @param r Request to get the IP from the header * @param sconf * @param cconf (if available) to get the real IP from * @param caller Which caller of the method * @param ip6 The client's IP address to be used (pointer to array of unsigned long (2)) * @return The client's IP address as a string (for logging) */ static const char *qos_get_clientIP(request_rec *r, qos_srv_config *sconf, qs_conn_ctx *cconf, const char *caller, apr_uint64_t *ip6) { const char *forwardedForLogIP; if(sconf->qos_cc_forwardedfor) { const char *forwardedfor = qos_forwardedfor(r, sconf->qos_cc_forwardedfor); if(forwardedfor) { if(qos_ip_str2long(forwardedfor, ip6) == 0) { if(apr_table_get(r->notes, "QOS_LOG_PFX069") == NULL) { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, QOS_LOG_PFX(069)"no valid IP header found (@%s):" " invalid header value '%s'," " fallback to connection's IP %s, id=%s", caller, forwardedfor, QS_CONN_REMOTEIP(r->connection) == NULL ? "-" : QS_CONN_REMOTEIP(r->connection), qos_unique_id(r, "069")); apr_table_set(r->notes, "QOS_LOG_PFX069", "log once"); QS_INC_EVENT(sconf, 69); } } else { // done return forwardedfor; } } else { if(apr_table_get(r->notes, "QOS_LOG_PFX069") == NULL) { ap_log_rerror(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, 0, r, QOS_LOG_PFX(069)"no valid IP header found (@%s):" " header '%s' not available," " fallback to connection's IP %s, id=%s", caller, sconf->qos_cc_forwardedfor, QS_CONN_REMOTEIP(r->connection) == NULL ? "-" : QS_CONN_REMOTEIP(r->connection), qos_unique_id(r, "069")); apr_table_set(r->notes, "QOS_LOG_PFX069", "log once"); QS_INC_EVENT(sconf, 69); } } } // use the real IP if(cconf) { forwardedForLogIP = QS_CONN_REMOTEIP(cconf->mc); // works for real connections only (no HTTP/2) ip6[0] = cconf->ip6[0]; ip6[1] = cconf->ip6[1]; } else { // HTTP/2 forwardedForLogIP = QS_CONN_REMOTEIP(r->connection); qos_ip_str2long(forwardedForLogIP, ip6); } return forwardedForLogIP; } /** * returns the version number of mod_qos * @param p Pool to alloc version string from * @return Version string */ static char *qos_revision(apr_pool_t *p) { return apr_pstrdup(p, g_revision); } /** * returns the request context */ static qs_req_ctx *qos_rctx_config_get(request_rec *r) { qs_req_ctx *rctx = ap_get_module_config(r->request_config, &qos_module); if(rctx == NULL) { rctx = apr_pcalloc(r->pool, sizeof(qs_req_ctx)); rctx->entry = NULL; rctx->entry_cond = NULL; rctx->evmsg = NULL; rctx->is_vip = 0; rctx->event_entries = apr_table_make(r->pool, 1); rctx->maxpostcount = 0; rctx->cc_event_req_set = 0; rctx->cc_event_ip[0] = 0; rctx->cc_event_ip[1] = 0; rctx->cc_serialize_set = 0; rctx->cc_serialize_ip[0] = 0; rctx->cc_serialize_ip[1] = 0; rctx->srv_serialize_set = 0; rctx->body_window = NULL; rctx->response_delayed = 0; ap_set_module_config(r->request_config, &qos_module, rctx); } return rctx; } /** * Adds the defined mod_qos_ev id to the request context (creates * the req ctx if necessary). * * @param r * @param id ID to set, e.g. "L;" or "D;" */ static void qs_set_evmsg(request_rec *r, const char *id) { qs_req_ctx *rctx = qos_rctx_config_get(r); if(rctx->evmsg == NULL || (strstr(rctx->evmsg, id) == NULL)) { rctx->evmsg = apr_pstrcat(r->pool, id, rctx->evmsg, NULL); } } /** * Encrypts and base64 encodes the provided buffer * @param r * @param sconf Secret to use (sconf->key) * @param b Buffer to encrypt * @param l Length of the buffer * @return Encrypted string (or NULL on error) */ static char *qos_encrypt(request_rec *r, qos_srv_config *sconf, const unsigned char *b, int l) { #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_CIPHER_CTX cipher_ctx; EVP_CIPHER_CTX *cipher_ctx_p = &cipher_ctx; HMAC_CTX hmac; HMAC_CTX *hmac_p = &hmac; #else EVP_CIPHER_CTX *cipher_ctx_p; HMAC_CTX *hmac_p; #endif unsigned char hash[HMAC_MAX_MD_CBLOCK]; unsigned int hashLen = HMAC_MAX_MD_CBLOCK; int buf_len = 0; int len = 0; unsigned char *buf = apr_pcalloc(r->pool, + EVP_MAX_IV_LENGTH + QOS_HASH_LEN + l + EVP_CIPHER_block_size(EVP_des_ede3_cbc())); #if APR_HAS_RANDOM if(apr_generate_random_bytes(buf, EVP_MAX_IV_LENGTH) != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, QOS_LOG_PFX(080)"Can't generate random data, id=%s", qos_unique_id(r, NULL)); } #else if(!RAND_bytes(buf, EVP_MAX_IV_LENGTH)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, QOS_LOG_PFX(080)"Can't generate random data, id=%s", qos_unique_id(r, NULL)); } #endif /* checksum */ #if OPENSSL_VERSION_NUMBER < 0x10100000L HMAC_CTX_init(hmac_p); #else hmac_p = HMAC_CTX_new(); #endif #ifndef OPENSSL_NO_MD5 HMAC_Init_ex(hmac_p, sconf->rawKey, sconf->rawKeyLen, EVP_md5(), NULL); #else HMAC_Init_ex(hmac_p, sconf->rawKey, sconf->rawKeyLen, EVP_sha256(), NULL); #endif HMAC_Update(hmac_p, b, l); HMAC_Final(hmac_p, hash, &hashLen); #if OPENSSL_VERSION_NUMBER < 0x10100000L HMAC_CTX_cleanup(hmac_p); #else HMAC_CTX_free(hmac_p); #endif /* sym enc */ #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_CIPHER_CTX_init(cipher_ctx_p); #else cipher_ctx_p = EVP_CIPHER_CTX_new(); #endif EVP_EncryptInit(cipher_ctx_p, EVP_des_ede3_cbc(), sconf->key, (unsigned char *)buf); // skip iv, enc(hash + data) buf_len = EVP_MAX_IV_LENGTH; if(!EVP_EncryptUpdate(cipher_ctx_p, &buf[buf_len], &len, hash, QOS_HASH_LEN)) { goto failed; } buf_len+=len; if(!EVP_EncryptUpdate(cipher_ctx_p, &buf[buf_len], &len, b, l)) { goto failed; } buf_len+=len; if(!EVP_EncryptFinal(cipher_ctx_p, &buf[buf_len], &len)) { goto failed; } buf_len+=len; #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_CIPHER_CTX_cleanup(cipher_ctx_p); #else EVP_CIPHER_CTX_free(cipher_ctx_p); #endif /* b64 encode */ { char *data = (char *)apr_pcalloc(r->pool, 1 + apr_base64_encode_len(buf_len)); len = apr_base64_encode(data, (const char *)buf, buf_len); data[len] = '\0'; return data; } failed: #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_CIPHER_CTX_cleanup(cipher_ctx_p); #else EVP_CIPHER_CTX_free(cipher_ctx_p); #endif if(QS_ISDEBUG(r->server)) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, QOS_LOGD_PFX"qos_encrypt() encryption operation failed, id=%s", qos_unique_id(r, NULL)); } return NULL; } /** * Decryptes the base64 encoded string (see qos_encrypt()). * @param r * @param sconf To access the secret * @param ret_buf Pointer to the decypted data (result), NULL if decyption fails * @param value Base64 encded string to decrypt * @return Length of the decypted data (0 if decyption failed) */ static int qos_decrypt(request_rec *r, qos_srv_config* sconf, unsigned char **ret_buf, const char *value) { #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_CIPHER_CTX cipher_ctx; EVP_CIPHER_CTX *cipher_ctx_p = &cipher_ctx; #else EVP_CIPHER_CTX *cipher_ctx_p; #endif /* decode */ char *dec = (char *)apr_pcalloc(r->pool, 1 + apr_base64_decode_len(value)); int dec_len = apr_base64_decode(dec, value); *ret_buf = NULL; if(dec_len < (EVP_MAX_IV_LENGTH + QOS_HASH_LEN)) { if(QS_ISDEBUG(r->server)) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, QOS_LOGD_PFX"qos_decrypt() base64 decoding failed, id=%s", qos_unique_id(r, NULL)); } return 0; } else { /* decrypt */ #if OPENSSL_VERSION_NUMBER < 0x10100000L HMAC_CTX hmac; HMAC_CTX *hmac_p = &hmac; #else HMAC_CTX *hmac_p; #endif unsigned char hash[HMAC_MAX_MD_CBLOCK]; unsigned int hashLen = HMAC_MAX_MD_CBLOCK; int len = 0; int buf_len = 0; unsigned char *buf; dec_len -= EVP_MAX_IV_LENGTH; buf = apr_pcalloc(r->pool, dec_len); /* sym dec */ #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_CIPHER_CTX_init(cipher_ctx_p); #else cipher_ctx_p = EVP_CIPHER_CTX_new(); #endif EVP_DecryptInit(cipher_ctx_p, EVP_des_ede3_cbc(), sconf->key, (unsigned char *)dec); if(!EVP_DecryptUpdate(cipher_ctx_p, (unsigned char *)&buf[buf_len], &len, (const unsigned char *)&dec[EVP_MAX_IV_LENGTH], dec_len)) { goto failed; } buf_len+=len; if(!EVP_DecryptFinal(cipher_ctx_p, (unsigned char *)&buf[buf_len], &len)) { goto failed; } buf_len+=len; #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_CIPHER_CTX_cleanup(cipher_ctx_p); #else EVP_CIPHER_CTX_free(cipher_ctx_p); #endif // hash + data if(buf_len < (QOS_HASH_LEN + 1)) { if(QS_ISDEBUG(r->server)) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, QOS_LOGD_PFX"qos_decrypt() misshing hash, id=%s", qos_unique_id(r, NULL)); } return 0; } /* checksum */ buf_len -= QOS_HASH_LEN; #if OPENSSL_VERSION_NUMBER < 0x10100000L HMAC_CTX_init(hmac_p); #else hmac_p = HMAC_CTX_new(); #endif #ifndef OPENSSL_NO_MD5 HMAC_Init_ex(hmac_p, sconf->rawKey, sconf->rawKeyLen, EVP_md5(), NULL); #else HMAC_Init_ex(hmac_p, sconf->rawKey, sconf->rawKeyLen, EVP_sha256(), NULL); #endif HMAC_Update(hmac_p, &buf[QOS_HASH_LEN], buf_len); HMAC_Final(hmac_p, hash, &hashLen); #if OPENSSL_VERSION_NUMBER < 0x10100000L HMAC_CTX_cleanup(hmac_p); #else HMAC_CTX_free(hmac_p); #endif if(hashLen > QOS_HASH_LEN) { // we don't keep more than 16 bytes hashLen = QOS_HASH_LEN; } if(memcmp(hash, buf, hashLen) != 0) { if(QS_ISDEBUG(r->server)) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, QOS_LOGD_PFX"qos_decrypt() invalid hash, id=%s", qos_unique_id(r, NULL)); } return 0; } /* decrypted and valid */ *ret_buf = &buf[QOS_HASH_LEN]; return buf_len; } failed: #if OPENSSL_VERSION_NUMBER < 0x10100000L EVP_CIPHER_CTX_cleanup(cipher_ctx_p); #else EVP_CIPHER_CTX_free(cipher_ctx_p); #endif if(QS_ISDEBUG(r->server)) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, QOS_LOGD_PFX"qos_decrypt() decryption operation failed, id=%s", qos_unique_id(r, NULL)); } return 0; } /** * Adds the user tracking cookie to r->headers_out if QOS_USER_TRACKING_NEW env variable * has been set. * @param r * @param sconf * @param status (302 or other) */ static void qos_send_user_tracking_cookie(request_rec *r, qos_srv_config* sconf, int status) { const char *new_user = apr_table_get(r->subprocess_env, QOS_USER_TRACKING_NEW); if(new_user) { char *setCookieVal; apr_size_t retcode; char timeString[MAX_STRING_LEN]; apr_time_exp_t timeEx; int new_user_len = strlen(new_user); int len = 2 + new_user_len; unsigned char *value = apr_pcalloc(r->pool, len + 1); char *encryptedVal; char *domain = NULL; apr_time_exp_gmt(&timeEx, r->request_time); apr_strftime(timeString, &retcode, sizeof(timeString), "%m", &timeEx); #ifdef QS_INTERNAL_TEST { const char *m = apr_table_get(r->headers_in, "X-TEST-USER-TRACK-MONTH"); if(m) { strcpy(timeString, m); } } #endif memcpy(value, timeString, 2); memcpy(&value[2], new_user, new_user_len); value[len] = '\0'; encryptedVal = qos_encrypt(r, sconf, value, len + 1); if(sconf->user_tracking_cookie_domain != NULL) { domain = apr_pstrcat(r->pool, "; Domain=", sconf->user_tracking_cookie_domain, NULL); } /* set cookie valid for 300 days or for this session only */ setCookieVal = apr_psprintf(r->pool, "%s=%s; Path=/%s%s", sconf->user_tracking_cookie, encryptedVal, sconf->user_tracking_cookie_session < 1 ? "; Max-Age=25920000" : "", domain != NULL ? domain : ""); if(status != HTTP_MOVED_TEMPORARILY) { apr_table_add(r->headers_out, "Set-Cookie", setCookieVal); } else { apr_table_add(r->err_headers_out, "Set-Cookie", setCookieVal); } } return; } /** * Verifies and sets the user tracking cookie * - QOS_USER_TRACKING if the cookie was available * - QOS_USER_TRACKING_NEW if a new cookie needs to be set * - QOS_USER_TRACKING_RENEW if the cookie shall be renewed * * syntax: b64(enc()) * * shall be called after(!) mod_unique_id has created an id * * @param r * @param sconf * @param value Cookie received from the client, possibly null (see qos_get_remove_cookie()) */ static void qos_get_create_user_tracking(request_rec *r, qos_srv_config* sconf, const char *value) { const char *uid = qos_unique_id(r, NULL); const char *verified = NULL; const char *newUID = NULL; if(value != NULL) { int buf_len = 0; unsigned char *buf; buf_len = qos_decrypt(r, sconf, &buf, value); if(buf_len > 0) { verified = (char *)buf; } } if(verified == NULL) { newUID = uid; apr_table_set(r->subprocess_env, QOS_USER_TRACKING_NEW, newUID); qs_set_evmsg(r, "u;"); } else if(strlen(verified) > 2) { apr_size_t retcode; char timeString[MAX_STRING_LEN]; apr_time_exp_t timeEx; apr_time_exp_gmt(&timeEx, r->request_time); apr_strftime(timeString, &retcode, sizeof(timeString), "%m", &timeEx); if(strncmp(timeString, verified, 2) != 0) { /* renew, if not from this month */ apr_table_set(r->subprocess_env, QOS_USER_TRACKING_NEW, &verified[2]); apr_table_set(r->subprocess_env, QOS_USER_TRACKING_RENEW, "1"); } newUID = &verified[2]; } else { newUID = uid; apr_table_set(r->subprocess_env, QOS_USER_TRACKING_NEW, newUID); } apr_table_set(r->subprocess_env, QOS_USER_TRACKING, newUID); return; } /** * Adds new milestone cookie to the response headers if QOS_MILESTONE_COOKIE * has been set. * See qos_verify_milestone() about the syntax. */ static void qos_update_milestone(request_rec *r, qos_srv_config* sconf) { const char *new_ms = apr_table_get(r->subprocess_env, QOS_MILESTONE_COOKIE); if(new_ms) { apr_time_t now = apr_time_sec(r->request_time); int new_ms_len = strlen(new_ms); int len = sizeof(apr_time_t) + new_ms_len; unsigned char *value = apr_pcalloc(r->pool, len + 1); char *encryptedVal; apr_table_unset(r->subprocess_env, QOS_MILESTONE_COOKIE); memcpy(value, &now, sizeof(apr_time_t)); memcpy(&value[sizeof(apr_time_t)], new_ms, new_ms_len); value[len] = '\0'; encryptedVal = qos_encrypt(r, sconf, value, len); apr_table_add(r->headers_out, "Set-Cookie", apr_psprintf(r->pool, "%s=%s; Path=/;", QOS_MILESTONE_COOKIE, encryptedVal)); } return; } /** * Verifies the milestone. Evaluates rule and enforces it. Does also set the * QOS_MILESTONE_COOKIE variable if a new milestone has been reached. * * milestone cookie syntax: b64(enc(