From 1e876c61adfbd109cfec66cb6722a1f3bd5e5cf3 Mon Sep 17 00:00:00 2001 From: Hans Zandbelt Date: Mon, 18 Dec 2023 08:43:03 +0100 Subject: [PATCH] 2.4.15rc10: metrics refactoring and extension Signed-off-by: Hans Zandbelt --- ChangeLog | 4 + auth_openidc.conf | 27 +- configure.ac | 2 +- src/authz.c | 11 +- src/config.c | 1 + src/const.h | 1 + src/metrics.c | 598 +++++++++++++++++++++++++++-------------- src/metrics.h | 68 +++-- src/mod_auth_openidc.c | 57 ++-- src/mod_auth_openidc.h | 1 + src/oauth.c | 4 +- src/parse.c | 8 +- src/proto.c | 2 +- src/util.c | 2 +- 14 files changed, 521 insertions(+), 265 deletions(-) diff --git a/ChangeLog b/ChangeLog index dd227507..d54c37b6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +12/18/2023 +- metrics refactoring and extension +- bump to 2.4.15rc10 + 12/15/2023 - add (and fix) more metrics, including provider requests, authorization and cache - bump to 2.4.15rc9 diff --git a/auth_openidc.conf b/auth_openidc.conf index 349af6a2..a69b8a78 100644 --- a/auth_openidc.conf +++ b/auth_openidc.conf @@ -1013,20 +1013,25 @@ # Specify metrics that you wish to collect and keep in shared memory for retrieval. # Supported metrics classes are: -# authtype: the authentication handler type split out per AuthType: openid-connect, oauth20, auth-openidc -# authn: authentication request generation and response processing -# authz: authorization errors -# requests: requests to the provider endpoints (metadata retrieval, token request, refresh requests and userinfo requests) -# session: existing session handling -# cache: cache read/write/errors -# redirect_uri: requests to the redirect_uri -# content: requests to the content handler, split out per types of request (info, metrics, jwks, etc.) +# authtype Request counter, overall and per AuthType: openid-connect, oauth20 and auth-openidc. +# authn Authentication request creation and response processing. +# authz Authorization errors per OIDCUnAuthzAction (per Require statement, not overall). +# require.claim Match/failure count of Require claim directives (per Require statement, not overall). +# requests Requests to the provider endpoints: metadata retrieval, token request, refresh requests and userinfo requests. +# session Existing session processing. +# cache Cache read/write timings and errors. +# redirect_uri Requests to the Redirect URI, per type. +# content Requests to the content handler, per type of request: info, metrics, jwks, etc. # When not defined no metrics will be recorded. -#OIDCMetricsData [ authtype | authn | authz | requests | session | cache | redirect_uri | content ]+ +#OIDCMetricsData [ authtype | authn | authz | require.claim | requests | session | cache | redirect_uri | content ]+ # Specify the path where metrics are published and can be consumed. -# The "format=" parameter can be passed to specify the format of the data. -# The default is "prometheus", "json" is also supported. +# The format parameter can be passed to specify the format in which the collected data is returned. +# format=prometheus Prometheus text-based exporter +# format=json (non-standard) JSON with descriptions and names +# format=status short text based status message "OK" plus optional counter (&vhost=&counter=) +# format=internal internal terse JSON for debugging purposes +# The default is "prometheus". # Protect protect this path (e.g. Require host localhost) or serve it on an internal co-located vhost/port. # When not defined, no metrics will be published on the enclosing vhost. #OIDCMetricsPublish diff --git a/configure.ac b/configure.ac index 13e9d496..6b47a705 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -AC_INIT([mod_auth_openidc],[2.4.15rc9],[hans.zandbelt@openidc.com]) +AC_INIT([mod_auth_openidc],[2.4.15rc10],[hans.zandbelt@openidc.com]) AC_SUBST(NAMEVER, AC_PACKAGE_TARNAME()-AC_PACKAGE_VERSION()) diff --git a/src/authz.c b/src/authz.c index 4bc8bffc..e7679827 100644 --- a/src/authz.c +++ b/src/authz.c @@ -44,6 +44,8 @@ */ #include "mod_auth_openidc.h" + +#include "metrics.h" #include "pcre_subst.h" static apr_byte_t oidc_authz_match_value(request_rec *r, const char *spec_c, const json_t *val, const char *key) { @@ -308,10 +310,10 @@ int oidc_authz_worker22(request_rec *r, json_t *claims, const require_line *cons token = ap_getword_white(r->pool, &requirement); /* see if we've got anything meant for us */ - if (apr_strnatcasecmp(token, OIDC_REQUIRE_CLAIM_NAME) == 0) { + if (_oidc_strnatcasecmp(token, OIDC_REQUIRE_CLAIM_NAME) == 0) { match_claim_fn = oidc_authz_match_claim; #ifdef USE_LIBJQ - } else if (apr_strnatcasecmp(token, OIDC_REQUIRE_CLAIMS_EXPR_NAME) == 0) { + } else if (_oidc_strnatcasecmp(token, OIDC_REQUIRE_CLAIMS_EXPR_NAME) == 0) { match_claim_fn = oidc_authz_match_claims_expr; #endif } else { @@ -381,6 +383,7 @@ int oidc_authz_worker22(request_rec *r, json_t *claims, const require_line *cons authz_status oidc_authz_worker24(request_rec *r, json_t *claims, const char *require_args, const void *parsed_require_args, oidc_authz_match_claim_fn_type match_claim_fn) { + oidc_cfg *cfg = ap_get_module_config(r->server->module_config, &auth_openidc_module); int count_oauth_claims = 0; const char *t, *w, *err = NULL; const ap_expr_info_t *expr = parsed_require_args; @@ -413,6 +416,8 @@ authz_status oidc_authz_worker24(request_rec *r, json_t *claims, const char *req /* see if we can match any of out input claims against this Require'd value */ if (match_claim_fn(r, w, claims) == TRUE) { + OIDC_METRICS_COUNTER_INC_SPEC(r, cfg, OM_AUTHZ_MATCH_REQUIRE_CLAIM, require_args); + oidc_debug(r, "require claim/expr '%s' matched", w); return AUTHZ_GRANTED; } @@ -423,6 +428,8 @@ authz_status oidc_authz_worker24(request_rec *r, json_t *claims, const char *req oidc_warn(r, "'require claim/expr' missing specification(s) in configuration, denying"); } + OIDC_METRICS_COUNTER_INC_SPEC(r, cfg, OM_AUTHZ_ERROR_REQUIRE_CLAIM, require_args); + oidc_debug(r, "could not match require claim expression '%s'", require_args); oidc_authz_error_add(r, require_args); diff --git a/src/config.c b/src/config.c index dcd731b9..dff52282 100644 --- a/src/config.c +++ b/src/config.c @@ -2760,6 +2760,7 @@ void oidc_register_hooks(apr_pool_t *pool) { oidc_pre_config_init(); ap_hook_post_config(oidc_post_config, NULL, NULL, APR_HOOK_LAST); ap_hook_child_init(oidc_child_init, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_fixups(oidc_fixups, NULL, NULL, APR_HOOK_MIDDLE); static const char *const proxySucc[] = {"mod_proxy.c", NULL}; ap_hook_handler(oidc_content_handler, NULL, proxySucc, APR_HOOK_FIRST); ap_hook_insert_filter(oidc_filter_in_insert_filter, NULL, NULL, APR_HOOK_MIDDLE); diff --git a/src/const.h b/src/const.h index a71799f4..8d54122b 100644 --- a/src/const.h +++ b/src/const.h @@ -69,6 +69,7 @@ #define _oidc_strlen(s) (s ? strlen(s) : 0) #define _oidc_strcmp(a, b) ((a && b) ? apr_strnatcmp(a, b) : -1) +#define _oidc_strnatcasecmp(a, b) ((a && b) ? apr_strnatcasecmp(a, b) : -1) #define _oidc_strncmp(a, b, size) ((a && b) ? strncmp(a, b, size) : -1) #define _oidc_str_to_int(s) (s ? (int)strtol(s, NULL, 10) : 0) diff --git a/src/metrics.c b/src/metrics.c index e0fd431f..cd09f833 100644 --- a/src/metrics.c +++ b/src/metrics.c @@ -46,142 +46,112 @@ #include "metrics.h" #include -#define OM_CLASS_AUTH_TYPE "authtype" -#define OM_CLASS_AUTHN "authn" -#define OM_CLASS_AUTHZ "authz" -#define OM_CLASS_REQUESTS "requests" -#define OM_CLASS_SESSION "session" -#define OM_CLASS_CACHE "cache" -#define OM_CLASS_CONTENT "content" -#define OM_CLASS_REDIRECT_URI "redirect_uri" +// NB: formatting matters for docs script from here until clang-format on + +// KEEP THIS: start-of-classes + +#define OM_CLASS_AUTH_TYPE "authtype" // Request counter, overall and per AuthType: openid-connect, oauth20 and auth-openidc. +#define OM_CLASS_AUTHN "authn" // Authentication request creation and response processing. +#define OM_CLASS_AUTHZ "authz" // Authorization errors per OIDCUnAuthzAction (per Require statement, not overall). +#define OM_CLASS_REQUIRE_CLAIM "require.claim" // Match/failure count of Require claim directives (per Require statement, not overall). +#define OM_CLASS_REQUESTS "requests" // Requests to the provider endpoints: metadata retrieval, token request, refresh requests and userinfo requests. +#define OM_CLASS_SESSION "session" // Existing session processing. +#define OM_CLASS_CACHE "cache" // Cache read/write timings and errors. +#define OM_CLASS_REDIRECT_URI "redirect_uri" // Requests to the Redirect URI, per type. +#define OM_CLASS_CONTENT "content" // Requests to the content handler, per type of request: info, metrics, jwks, etc. + +// KEEP THIS: end-of-classes + +// NB: order must match the oidc_metrics_timing_type_t enum type in metrics.h const oidc_metrics_timing_info_t _oidc_metrics_timings_info[] = { - { OM_AUTHN_REQUEST, OM_CLASS_AUTHN, "request", - "authentication requests" }, - { OM_AUTHN_RESPONSE, OM_CLASS_AUTHN, "response", - "authentication responses" }, - { OM_SESSION_VALID, OM_CLASS_SESSION, "valid", - "successfully validated existing sessions" }, - - { OM_PROVIDER_METADATA, OM_CLASS_REQUESTS, "metadata", - "provider discovery document requests" }, - { OM_PROVIDER_TOKEN, OM_CLASS_REQUESTS, "token", - "provider token requests" }, - { OM_PROVIDER_REFRESH, OM_CLASS_REQUESTS, "refresh", - "provider refresh token requests" }, - { OM_PROVIDER_USERINFO, OM_CLASS_REQUESTS, "userinfo", - "provider userinfo requests" }, - - { OM_CACHE_READ, OM_CLASS_CACHE, "read", - "cache read requests" }, - { OM_CACHE_WRITE, OM_CLASS_CACHE, "write", - "cache write requests" }, + // KEEP THIS: start-of-timers + + { OM_CLASS_AUTH_TYPE, "handler", "the overall authz+authz processing time" }, + + { OM_CLASS_AUTHN, "request", "authentication requests" }, + { OM_CLASS_AUTHN, "response", "authentication responses" }, + + { OM_CLASS_SESSION, "valid", "successfully validated existing sessions" }, + + { OM_CLASS_REQUESTS, "metadata", "provider discovery document requests" }, + { OM_CLASS_REQUESTS, "token", "provider token requests" }, + { OM_CLASS_REQUESTS, "refresh", "provider refresh token requests" }, + { OM_CLASS_REQUESTS, "userinfo", "provider userinfo requests" }, + + { OM_CLASS_CACHE, "read", "cache read requests" }, + { OM_CLASS_CACHE, "write", "cache write requests" }, + + // KEEP THIS: end-of-timers }; +// NB: order must match the oidc_metrics_counter_type_t enum type in metrics.h + const oidc_metrics_counter_info_t _oidc_metrics_counters_info[] = { - { OM_AUTHTYPE_OPENID_CONNECT, OM_CLASS_AUTH_TYPE, "handler", "openid-connect", - "incoming requests" }, - { OM_AUTHTYPE_OAUTH20, OM_CLASS_AUTH_TYPE, "handler", "oauth20", - "incoming requests" }, - { OM_AUTHTYPE_AUTH_OPENIDC, OM_CLASS_AUTH_TYPE, "handler", "auth-openidc", - "incoming requests" }, - { OM_AUTHTYPE_DECLINED, OM_CLASS_AUTH_TYPE, "handler", "declined", - "incoming requests" }, - - { OM_AUTHN_REQUEST_ERROR_URL, OM_CLASS_AUTHN, "request.error", "url", - "errors matching the incoming request URL against the configuration" }, - - { OM_AUTHN_RESPONSE_ERROR_STATE_MISMATCH, OM_CLASS_AUTHN, "response.error", "state-mismatch", - "state mismatch errors in authentication responses" }, - { OM_AUTHN_RESPONSE_ERROR_STATE_EXPIRED, OM_CLASS_AUTHN, "response.error", "state-expired", - "state expired errors in authentication responses" }, - { OM_AUTHN_RESPONSE_ERROR_PROVIDER, OM_CLASS_AUTHN, "response.error", "provider", - "errors returned by the provider in authentication responses" }, - { OM_AUTHN_RESPONSE_ERROR_PROTOCOL, OM_CLASS_AUTHN, "response.error", "protocol", - "errors handling authentication responses" }, - { OM_AUTHN_RESPONSE_ERROR_REMOTE_USER, OM_CLASS_AUTHN, "response.error", "remote-user", - "errors identifying the remote user based on provided claims" }, - - { OM_AUTHZ_ERROR_GENERAL, OM_CLASS_AUTHZ, "error", "general", - "authorization errors" }, - { OM_AUTHZ_ACTION_AUTH, OM_CLASS_AUTHZ, "action", "auth", - "step-up authentication requests" }, - { OM_AUTHZ_ACTION_401, OM_CLASS_AUTHZ, "action", "401", - "401 authorization errors" }, - { OM_AUTHZ_ACTION_403, OM_CLASS_AUTHZ, "action", "403", - "403 authorization errors" }, - { OM_AUTHZ_ACTION_302, OM_CLASS_AUTHZ, "action", "302", - "302 authorization errors" }, - { OM_AUTHZ_ERROR_OAUTH20, OM_CLASS_AUTHZ, "error", "oauth20", - "AuthType oauth20 authorization errors 401" }, - - { OM_PROVIDER_METADATA_ERROR, OM_CLASS_REQUESTS, "provider.metadata", "error" - "errors retrieving provider discovery document" }, - { OM_PROVIDER_TOKEN_ERROR, OM_CLASS_REQUESTS, "provider.token", "error", - "errors making a token request to the provider" }, - { OM_PROVIDER_REFRESH_ERROR, OM_CLASS_REQUESTS, "provider.refresh", "error", - "errors refreshing the access token" }, - { OM_PROVIDER_USERINFO_ERROR, OM_CLASS_REQUESTS, "provider.userinfo", "error", - "errors calling the provider userinfo endpoint" }, - - { OM_SESSION_ERROR_COOKIE_DOMAIN, OM_CLASS_SESSION, "error", "cookie-domain", - "cookie domain validation errors for existing sessions" }, - { OM_SESSION_ERROR_EXPIRED, OM_CLASS_SESSION, "error", "expired", - "sessions that exceeded the maximum duration" }, - { OM_SESSION_ERROR_REFRESH_ACCESS_TOKEN, OM_CLASS_SESSION, "error", "refresh-access-token", - "errors refreshing the access token before expiry in existing sessions" }, - { OM_SESSION_ERROR_REFRESH_USERINFO, OM_CLASS_SESSION, "error", "refresh-user-info", - "errors refreshing claims from the userinfo endpoint in existing sessions" }, - { OM_SESSION_ERROR_GENERAL, OM_CLASS_SESSION, "error", "general", - "existing sessions that failed validation" }, - - { OM_CACHE_ERROR, OM_CLASS_CACHE, "cache", "error", - "cache read/write errors" }, - - { OM_REDIRECT_URI_AUTHN_RESPONSE_REDIRECT, OM_CLASS_REDIRECT_URI, "authn.response", "redirect", - "authentication responses received in a redirect", }, - { OM_REDIRECT_URI_AUTHN_RESPONSE_POST, OM_CLASS_REDIRECT_URI, "authn.response", "post", - "authentication responses received in a HTTP POST", }, - { OM_REDIRECT_URI_AUTHN_RESPONSE_IMPLICIT, OM_CLASS_REDIRECT_URI, "authn.response", "implicit", - "(presumed) implicit authentication responses to the redirect URI", }, - { OM_REDIRECT_URI_DISCOVERY_RESPONSE, OM_CLASS_REDIRECT_URI, "discovery", "response", - "discovery responses to the redirect URI", }, - { OM_REDIRECT_URI_REQUEST_LOGOUT, OM_CLASS_REDIRECT_URI, "request", "logout", - "logout requests to the redirect URI", }, - { OM_REDIRECT_URI_REQUEST_JWKS, OM_CLASS_REDIRECT_URI, "request", "jwks", - "JWKs retrieval requests to the redirect URI", }, - { OM_REDIRECT_URI_REQUEST_SESSION, OM_CLASS_REDIRECT_URI, "request", "session", - "session management requests to the redirect URI", }, - { OM_REDIRECT_URI_REQUEST_REFRESH, OM_CLASS_REDIRECT_URI, "request", "refresh", - "refresh access token requests to the redirect URI", }, - { OM_REDIRECT_URI_REQUEST_REQUEST_URI, OM_CLASS_REDIRECT_URI, "request", "request_uri", - "Request URI calls to the redirect URI", }, - { OM_REDIRECT_URI_REQUEST_REMOVE_AT_CACHE, OM_CLASS_REDIRECT_URI, "request", "remove_at_cache", - "access token cache removal requests to the redirect URI", }, - { OM_REDIRECT_URI_REQUEST_REVOKE_SESSION, OM_CLASS_REDIRECT_URI, "request", "session", - "revoke session requests to the redirect URI", }, - { OM_REDIRECT_URI_REQUEST_INFO, OM_CLASS_REDIRECT_URI, "request", "info", - "info hook requests to the redirect URI", }, - { OM_REDIRECT_URI_ERROR_PROVIDER, OM_CLASS_REDIRECT_URI, "error", "provider", - "provider authentication response errors received at the redirect URI", }, - { OM_REDIRECT_URI_ERROR_INVALID, OM_CLASS_REDIRECT_URI, "error", "invalid", - "invalid requests to the redirect URI", }, - - { OM_CONTENT_REQUEST_DECLINED, OM_CLASS_CONTENT, "request", "declined", - "requests declined by the content handler" }, - { OM_CONTENT_REQUEST_INFO, OM_CLASS_CONTENT, "request", "info", - "info hook requests to the content handler" }, - { OM_CONTENT_REQUEST_JWKS, OM_CLASS_CONTENT, "request", "jwks", - "JWKs requests to the content handler" }, - { OM_CONTENT_REQUEST_DISCOVERY, OM_CLASS_CONTENT, "request", "discovery", - "discovery requests to the content handler" }, - { OM_CONTENT_REQUEST_POST_PRESERVE, OM_CLASS_CONTENT, "request", "post-preserve", - "HTTP POST preservation requests to the content handler" }, - { OM_CONTENT_REQUEST_UNKNOWN, OM_CLASS_CONTENT, "request", "unknown", - "unknown requests to the content handler" }, + // KEEP THIS: start-of-counters + + { OM_CLASS_AUTH_TYPE, "handler", "mod_auth_openidc", "requests handled by mod_auth_openidc" }, + { OM_CLASS_AUTH_TYPE, "handler", "openid-connect", "requests handled by AuthType openid-connect" }, + { OM_CLASS_AUTH_TYPE, "handler", "oauth20", "requests handled by AuthType oauth20" }, + { OM_CLASS_AUTH_TYPE, "handler", "auth-openidc", "requests handled by AuthType auth-openidc" }, + { OM_CLASS_AUTH_TYPE, "handler", "declined", "requests not handled by mod_auth_openidc"}, + + { OM_CLASS_AUTHN, "request.error", "url", "errors matching the incoming request URL against the configuration" }, + + { OM_CLASS_AUTHN, "response.error", "state-mismatch", "state mismatch errors in authentication responses" }, + { OM_CLASS_AUTHN, "response.error", "state-expired", "state expired errors in authentication responses" }, + { OM_CLASS_AUTHN, "response.error", "provider", "errors returned by the provider in authentication responses" }, + { OM_CLASS_AUTHN, "response.error", "protocol", "protocol errors handling authentication responses" }, + { OM_CLASS_AUTHN, "response.error", "remote-user", "errors identifying the remote user based on provided claims" }, + + { OM_CLASS_AUTHZ, "action", "auth", "step-up authentication requests" }, + { OM_CLASS_AUTHZ, "action", "401", "401 authorization errors" }, + { OM_CLASS_AUTHZ, "action", "403", "403 authorization errors" }, + { OM_CLASS_AUTHZ, "action", "302", "302 authorization errors" }, + { OM_CLASS_AUTHZ, "error", "oauth20", "AuthType oauth20 (401) authorization errors" }, + + { OM_CLASS_REQUIRE_CLAIM, "match", "require", "(per-) Require claim authorization matches" }, + { OM_CLASS_REQUIRE_CLAIM, "error", "require", "(per-) Require claim authorization errors" }, + + { OM_CLASS_REQUESTS, "provider.metadata", "error", "errors retrieving a provider discovery document" }, + { OM_CLASS_REQUESTS, "provider.token", "error", "errors making a token request to a provider" }, + { OM_CLASS_REQUESTS, "provider.refresh", "error", "errors refreshing the access token at the token endpoint" }, + { OM_CLASS_REQUESTS, "provider.userinfo", "error", "errors calling a provider userinfo endpoint" }, + + { OM_CLASS_SESSION, "error", "cookie-domain", "cookie domain validation errors for existing sessions" }, + { OM_CLASS_SESSION, "error", "expired", "sessions that exceeded the maximum duration" }, + { OM_CLASS_SESSION, "error", "refresh-access-token", "errors refreshing the access token before expiry in existing sessions" }, + { OM_CLASS_SESSION, "error", "refresh-user-info", "errors refreshing claims from the userinfo endpoint in existing sessions" }, + { OM_CLASS_SESSION, "error", "general", "existing sessions that failed validation" }, + + { OM_CLASS_CACHE, "cache", "error", "cache read/write errors" }, + + { OM_CLASS_REDIRECT_URI, "authn.response", "redirect", "authentication responses received in a redirect", }, + { OM_CLASS_REDIRECT_URI, "authn.response", "post", "authentication responses received in a HTTP POST", }, + { OM_CLASS_REDIRECT_URI, "authn.response", "implicit", "(presumed) implicit authentication responses to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "discovery", "response", "discovery responses to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request", "logout", "logout requests to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request", "jwks", "JWKs retrieval requests to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request", "session", "session management requests to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request", "refresh", "refresh access token requests to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request", "request_uri", "Request URI calls to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request", "remove_at_cache", "access token cache removal requests to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request", "session", "revoke session requests to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "request", "info", "info hook requests to the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "error", "provider", "provider authentication response errors received at the redirect URI", }, + { OM_CLASS_REDIRECT_URI, "error", "invalid", "invalid requests to the redirect URI", }, + + { OM_CLASS_CONTENT, "request", "declined", "requests declined by the content handler" }, + { OM_CLASS_CONTENT, "request", "info", "info hook requests to the content handler" }, + { OM_CLASS_CONTENT, "request", "jwks", "JWKs requests to the content handler" }, + { OM_CLASS_CONTENT, "request", "discovery", "discovery requests to the content handler" }, + { OM_CLASS_CONTENT, "request", "post-preserve", "HTTP POST preservation requests to the content handler" }, + { OM_CLASS_CONTENT, "request", "unknown", "unknown requests to the content handler" }, + + // KEEP THIS: end-of-counters }; @@ -207,14 +177,14 @@ static oidc_metrics_t _oidc_metrics = {NULL, NULL}; // mutex to protect the local metrics hash table static oidc_cache_mutex_t *_oidc_metrics_process_mutex = NULL; -// shared memory write interval in seconds -#define OIDC_CACHE_METRICS_STORAGE_INTERVAL 5 +// default shared memory write interval in seconds +#define OIDC_METRICS_CACHE_STORAGE_INTERVAL_DEFAULT 5000 // maximum length of the string representation of the global JSON metrics data in shared memory // 1024 sample size (compact, long keys, large json_int values, no description), timing + counter // 256 number of individual metrics collected // 4 number of vhosts supported -#define OIDC_CACHE_METRICS_JSON_MAX 1024 * 256 * 4 +#define OIDC_METRICS_CACHE_JSON_MAX_DEFAULT 1024 * 256 * 4 typedef struct oidc_metrics_bucket_t { const char *name; @@ -239,15 +209,16 @@ static oidc_metrics_bucket_t _oidc_metric_buckets[] = { { "inf", "bucket{le=\"+Inf\"}", 0 } }; -#define OIDC_METRICS_BUCKET_NUM sizeof(_oidc_metric_buckets) / sizeof(oidc_metrics_bucket_t) - // clang-format on -// NB: shared between JSON and Prometheus formatting +#define OIDC_METRICS_BUCKET_NUM sizeof(_oidc_metric_buckets) / sizeof(oidc_metrics_bucket_t) + +// NB: matters for Prometheus formatting #define OIDC_METRICS_SUM "sum" #define OIDC_METRICS_COUNT "count" #define OIDC_METRICS_TYPE "type" +#define OIDC_METRICS_SPEC "spec" #define OIDC_METRICS_NAME "name" #define OIDC_METRICS_LABEL_NAME "lname" @@ -257,38 +228,93 @@ static oidc_metrics_bucket_t _oidc_metric_buckets[] = { #define OIDC_METRICS_TIMINGS "timings" #define OIDC_METRICS_COUNTERS "counters" +/* + * convert a Jansson number to a string: JSON_INTEGER_FORMAT does not work with apr_psprintf !? + */ +static inline char *_json_int2str(apr_pool_t *pool, json_int_t n) { + char s[255]; + sprintf(s, "%" JSON_INTEGER_FORMAT, n); + return apr_pstrdup(pool, s); +} + +#if JSON_INTEGER_IS_LONG_LONG +#define OIDC_METRICS_INT_MAX LLONG_MAX +#else +#define OIDC_METRICS_INT_MAX LONG_MAX +#endif + +/* + * check Jansson specific integer/long number overrun + */ +static inline int _is_no_overflow(server_rec *s, json_int_t cur, json_int_t add) { + if ((OIDC_METRICS_INT_MAX - add) < cur) { + oidc_swarn(s, + "cannot update metrics since the size (%s) of the integer value would be larger than the " + "JSON/libjansson maximum " + "(%s)", + _json_int2str(s->process->pool, add), _json_int2str(s->process->pool, OIDC_METRICS_INT_MAX)); + return 0; + } + return 1; +} + // single counter container typedef struct oidc_metrics_counter_t { oidc_metrics_counter_type_t type; json_int_t count; + char *spec; } oidc_metrics_counter_t; // single timing stats container typedef struct oidc_metrics_timing_t { oidc_metrics_timing_type_t type; - json_int_t *buckets; + json_int_t buckets[OIDC_METRICS_BUCKET_NUM]; apr_time_t sum; json_int_t count; } oidc_metrics_timing_t; +/* + * collection thread + */ + /* * retrieve the (JSON) serialized (global) metrics data from shared memory */ -static void oidc_metrics_storage_get(server_rec *s, char **value) { - char *p = apr_shm_baseaddr_get(_oidc_metrics_cache); - if (*p) - *value = apr_pstrdup(s->process->pool, p); +static inline char *oidc_metrics_storage_get(server_rec *s) { + char *p = (char *)apr_shm_baseaddr_get(_oidc_metrics_cache); + return (*p) ? apr_pstrdup(s->process->pool, p) : NULL; +} + +/* + * retrieve environment variable integer with default setting + */ +static inline int oidc_metrics_get_env_int(const char *name, int dval) { + int v; + const char *env = getenv(name); + return (((env) && (sscanf(env, "%d", &v) == 1)) ? v : dval); +} + +#define OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR "OIDC_METRICS_CACHE_JSON_MAX" + +/* + * get the size of the to-be-allocated shared memory segment + */ +static inline int oidc_metrics_shm_size(server_rec *s) { + return oidc_metrics_get_env_int(OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR, OIDC_METRICS_CACHE_JSON_MAX_DEFAULT); } /* * store the serialized (global) metrics data in shared memory */ -static void oidc_metrics_storage_set(server_rec *s, const char *value) { +static inline void oidc_metrics_storage_set(server_rec *s, const char *value) { char *p = apr_shm_baseaddr_get(_oidc_metrics_cache); if (value) { int n = strlen(value) + 1; - if (n > OIDC_CACHE_METRICS_JSON_MAX) - oidc_serror(s, "json value too large"); + if (n > oidc_metrics_shm_size(s)) + oidc_serror(s, + "json value too large: set or increase system environment variable %s to a value " + "larger than %d", + OIDC_METRICS_CACHE_JSON_MAX_ENV_VAR, oidc_metrics_shm_size(s)); else _oidc_memcpy(p, value, n); } else { @@ -315,7 +341,7 @@ static json_t *oidc_metrics_timings_new(server_rec *s, const oidc_metrics_timing */ static void oidc_metrics_timings_update(server_rec *s, const json_t *entry, const oidc_metrics_timing_t *timing) { json_t *j_member = NULL; - json_int_t n = 0; + json_int_t n = 0, v = 0; int i = 0; for (i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) { @@ -325,16 +351,12 @@ static void oidc_metrics_timings_update(server_rec *s, const json_t *entry, cons j_member = json_object_get(entry, OIDC_METRICS_SUM); n = json_integer_value(j_member); - // check Jansson specific integer/long overrun - if ((LONG_MAX - apr_time_as_msec(timing->sum)) < n) { - oidc_serror(s, - "cannot update metrics since the size (%ld" - ") of the integer value would be larger than the JSON/libjansson maximum " - "(%ld)", - (long int)n, LONG_MAX); + + v = apr_time_as_msec(timing->sum); + if (_is_no_overflow(s, n, v) == 0) return; - } - json_integer_set(j_member, n + apr_time_as_msec(timing->sum)); + + json_integer_set(j_member, n + v); j_member = json_object_get(entry, OIDC_METRICS_COUNT); n = json_integer_value(j_member); @@ -365,13 +387,14 @@ static void oidc_metrics_store(server_rec *s) { apr_hash_t *server_hash = NULL; oidc_metrics_timing_t *timing = NULL; oidc_metrics_counter_t *counter = NULL; + json_int_t v = 0; json_error_t json_error; /* lock the shared memory for other processes */ oidc_cache_mutex_lock(s->process->pool, s, _oidc_metrics_global_mutex); /* get the global stringified JSON metrics */ - oidc_metrics_storage_get(s, &s_json); + s_json = oidc_metrics_storage_get(s); /* parse the metrics string to JSON */ if (s_json != NULL) @@ -393,11 +416,15 @@ static void oidc_metrics_store(server_rec *s) { j_value = json_object_get(j_counters, key); if (j_value != NULL) { j_member = json_object_get(j_value, OIDC_METRICS_COUNT); - json_integer_set(j_member, json_integer_value(j_member) + counter->count); + v = json_integer_value(j_member); + if (_is_no_overflow(s, v, counter->count)) + json_integer_set(j_member, v + counter->count); } else { j_member = json_object(); json_object_set_new(j_member, OIDC_METRICS_COUNT, json_integer(counter->count)); json_object_set_new(j_member, OIDC_METRICS_TYPE, json_integer(counter->type)); + if (counter->spec) + json_object_set_new(j_member, OIDC_METRICS_SPEC, json_string(counter->spec)); json_object_set_new(j_counters, key, j_member); } } @@ -438,16 +465,32 @@ static void oidc_metrics_store(server_rec *s) { oidc_cache_mutex_unlock(s->process->pool, s, _oidc_metrics_global_mutex); } +#define OIDC_METRICS_CACHE_STORAGE_INTERVAL_ENV_VAR "OIDC_METRICS_CACHE_STORAGE_INTERVAL" + +static inline apr_interval_time_t oidc_metrics_interval(server_rec *s) { + return apr_time_from_msec(oidc_metrics_get_env_int(OIDC_METRICS_CACHE_STORAGE_INTERVAL_ENV_VAR, + OIDC_METRICS_CACHE_STORAGE_INTERVAL_DEFAULT)); +} + +unsigned int oidc_metric_random_int(unsigned int mod) { + unsigned int v; + apr_generate_random_bytes((unsigned char *)&v, sizeof(v)); + return v % mod; +} + /* * thread that periodically writes the local data into the shared memory */ static void *oidc_metrics_thread_run(apr_thread_t *thread, void *data) { server_rec *s = (server_rec *)data; + /* sleep for a short random time <1s so child processes write-lock on a different frequency */ + apr_sleep(apr_time_from_msec(oidc_metric_random_int(1000))); + /* see if we are asked to exit */ while (_oidc_metrics_thread_exit == FALSE) { - apr_sleep(apr_time_from_sec(OIDC_CACHE_METRICS_STORAGE_INTERVAL)); + apr_sleep(oidc_metrics_interval(s)); // NB: no exit here because we need to write our local metrics into the cache before exiting /* lock the mutex that protects the locally cached metrics */ @@ -469,6 +512,10 @@ static void *oidc_metrics_thread_run(apr_thread_t *thread, void *data) { return NULL; } +/* + * server config handlers + */ + /* * NB: global, yet called for each vhost that has metrics enabled! */ @@ -479,7 +526,7 @@ apr_byte_t oidc_metrics_cache_post_config(server_rec *s) { return TRUE; /* create the shared memory segment that holds the stringified JSON formatted metrics data */ - if (apr_shm_create(&_oidc_metrics_cache, OIDC_CACHE_METRICS_JSON_MAX, NULL, s->process->pconf) != APR_SUCCESS) + if (apr_shm_create(&_oidc_metrics_cache, oidc_metrics_shm_size(s), NULL, s->process->pconf) != APR_SUCCESS) return FALSE; if (_oidc_metrics_cache == NULL) return FALSE; @@ -573,10 +620,14 @@ apr_status_t oidc_metrics_cache_cleanup(server_rec *s) { return rv; } +/* + * sampling + */ + /* * obtain the local metrics hashtable for the current vhost */ -static apr_hash_t *oidc_metrics_server_hash(request_rec *r, apr_hash_t *table) { +static inline apr_hash_t *oidc_metrics_server_hash(request_rec *r, apr_hash_t *table) { apr_hash_t *server_hash = NULL; char *name = "_default_"; @@ -599,7 +650,7 @@ static apr_hash_t *oidc_metrics_server_hash(request_rec *r, apr_hash_t *table) { * retrieve or create a local hashtable for the specified key * NB: assumes local hashtable has been locked */ -static void *oidc_metrics_get(request_rec *r, const char *key, apr_hash_t *table, size_t size) { +static inline void *oidc_metrics_get(request_rec *r, const char *key, apr_hash_t *table, size_t size) { void *result = NULL; apr_hash_t *server_hash = oidc_metrics_server_hash(r, table); @@ -618,7 +669,7 @@ static void *oidc_metrics_get(request_rec *r, const char *key, apr_hash_t *table /* * add/increase a counter metric in the locally cached data */ -void oidc_metrics_counter_inc(request_rec *r, oidc_metrics_counter_type_t type) { +void oidc_metrics_counter_inc(request_rec *r, oidc_metrics_counter_type_t type, const char *spec) { oidc_metrics_counter_t *counter = NULL; char *key = NULL; @@ -637,11 +688,16 @@ void oidc_metrics_counter_inc(request_rec *r, oidc_metrics_counter_type_t type) _oidc_metrics_counters_info[type].label_value); } } + if ((spec != NULL) && (_oidc_strcmp(spec, "") != 0)) + key = apr_psprintf(r->server->process->pool, "%s.%s", key, spec); counter = (oidc_metrics_counter_t *)oidc_metrics_get(r, key, _oidc_metrics.counters, sizeof(oidc_metrics_counter_t)); + counter->spec = spec ? apr_pstrdup(r->server->process->pool, spec) : NULL; counter->type = type; - counter->count++; + + if (_is_no_overflow(r->server, counter->count, 1)) + counter->count++; /* unlock the local metrics cache hashtable */ oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_process_mutex); @@ -655,7 +711,7 @@ void oidc_metrics_timing_add(request_rec *r, oidc_metrics_timing_type_t type, ap int i = 0; const char *key = apr_psprintf(r->pool, "%s.%s", _oidc_metrics_timings_info[type].name, - _oidc_metrics_timings_info[type].subkey); + _oidc_metrics_timings_info[type].spec); /* TODO: how can this happen? */ if (elapsed < 0) { @@ -668,19 +724,27 @@ void oidc_metrics_timing_add(request_rec *r, oidc_metrics_timing_type_t type, ap /* obtain or create the entry for the specified key */ timing = oidc_metrics_get(r, key, _oidc_metrics.timings, sizeof(oidc_metrics_timing_t)); - if (timing->buckets == NULL) - timing->buckets = apr_pcalloc(r->server->process->pool, sizeof(json_int_t) * OIDC_METRICS_BUCKET_NUM); timing->type = type; - for (i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) - if ((elapsed < _oidc_metric_buckets[i].threshold) || (_oidc_metric_buckets[i].threshold == 0)) - timing->buckets[i]++; - timing->sum += elapsed; - timing->count++; + + if (_is_no_overflow(r->server, timing->sum, elapsed)) { + for (i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) + if ((elapsed < _oidc_metric_buckets[i].threshold) || (_oidc_metric_buckets[i].threshold == 0)) + timing->buckets[i]++; + timing->sum += elapsed; + timing->count++; + } /* unlock the local metrics cache hashtable */ oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_process_mutex); } +/* + * representation handlers + */ + +/* + * JSON with extended descriptions/names + */ static int oidc_metrics_handle_json(request_rec *r, char *s_json) { json_t *json = NULL, *j_server = NULL, *j_timings, *j_counters, *j_timing = NULL, *j_counter = NULL; @@ -689,10 +753,12 @@ static int oidc_metrics_handle_json(request_rec *r, char *s_json) { json_error_t json_error; /* parse the metrics string to JSON */ - if (s_json != NULL) - json = json_loads(s_json, 0, &json_error); + if (s_json == NULL) + s_json = "{}"; + + json = json_loads(s_json, 0, &json_error); if (json == NULL) - return HTTP_NOT_FOUND; + goto end; void *iter1 = json_object_iter(json); while (iter1) { @@ -739,20 +805,73 @@ static int oidc_metrics_handle_json(request_rec *r, char *s_json) { iter1 = json_object_iter_next(json, iter1); } - if (json) { - char *str = json_dumps(json, JSON_COMPACT | JSON_PRESERVE_ORDER); - s_json = apr_pstrdup(r->pool, str); - free(str); - json_decref(json); - } else { - s_json = "{}"; - } + char *str = json_dumps(json, JSON_COMPACT | JSON_PRESERVE_ORDER); + s_json = apr_pstrdup(r->pool, str); + free(str); + + json_decref(json); + +end: /* return the data to the caller */ return oidc_util_http_send(r, s_json, _oidc_strlen(s_json), OIDC_CONTENT_TYPE_JSON, OK); } -static const char *oidc_metrics_name_to_label(request_rec *r, const char *json_name) { +/* + * dump the internal shared memory segment + */ +static int oidc_metrics_handle_internal(request_rec *r, char *s_json) { + if (s_json == NULL) + return HTTP_NOT_FOUND; + return oidc_util_http_send(r, s_json, _oidc_strlen(s_json), OIDC_CONTENT_TYPE_JSON, OK); +} + +#define OIDC_METRICS_VHOST_PARAM "vhost" +#define OIDC_METRICS_COUNTER_PARAM "counter" + +/* + * return status updates + */ +static int oidc_metrics_handle_status(request_rec *r, char *s_json) { + char *msg = "OK\n"; + char *metric = NULL, *vhost = NULL; + json_t *json = NULL, *j_server = NULL, *j_counters = NULL, *j_counter = NULL, *j_member = NULL; + json_error_t json_error; + + oidc_util_get_request_parameter(r, OIDC_METRICS_VHOST_PARAM, &vhost); + oidc_util_get_request_parameter(r, OIDC_METRICS_COUNTER_PARAM, &metric); + + if (vhost == NULL) + vhost = "localhost"; + + if ((metric) && (vhost)) { + + json = json_loads(s_json, 0, &json_error); + if (json == NULL) + goto end; + j_server = json_object_get(json, vhost); + if (j_server == NULL) + goto end; + j_counters = json_object_get(j_server, OIDC_METRICS_COUNTERS); + if (j_counters == NULL) + goto end; + j_counter = json_object_get(j_counters, metric); + if (j_counter == NULL) + goto end; + j_member = json_object_get(j_counter, OIDC_METRICS_COUNT); + + msg = apr_psprintf(r->pool, "OK: %s\n", _json_int2str(r->pool, json_integer_value(j_member))); + } + +end: + + if (json) + json_decref(json); + + return oidc_util_http_send(r, msg, _oidc_strlen(msg), "text/plain", OK); +} + +static const char *oidc_metrics_bucket_label(request_rec *r, const char *json_name) { const char *name = json_name; int i = 0; for (i = 0; i < OIDC_METRICS_BUCKET_NUM; i++) { @@ -768,16 +887,16 @@ static const char *oidc_prometheus_normalize(request_rec *r, const char *v1, con char *label = apr_psprintf(r->pool, "%s%s%s", v1 ? v1 : "", v2 ? "_" : "", v2 ? v2 : ""); int i = 0; for (i = 0; i < strlen(label); i++) - if ((label[i] == '-') || (label[i] == '.')) + if (isalnum(label[i]) == 0) label[i] = '_'; return label; } -static int oidc_metrics_handle_prometheus(request_rec *r, const char *s_json) { +static int oidc_metrics_handle_prometheus(request_rec *r, char *s_json) { json_t *json = NULL, *j_server = NULL, *j_timings, *j_counters, *j_timing = NULL, *j_member = NULL, - *j_counter = NULL; + *j_counter = NULL, *j_spec = NULL; const char *s_server = NULL, *s_key = NULL, *s_label = NULL, *s_bucket = NULL; - char *s_text = ""; + char *s_text = "", *s_desc = NULL; json_error_t json_error; json_int_t type = 0; @@ -801,7 +920,11 @@ static int oidc_metrics_handle_prometheus(request_rec *r, const char *s_json) { type = json_integer_value(json_object_get(j_counter, OIDC_METRICS_TYPE)); s_label = oidc_prometheus_normalize(r, s_server, s_key); - s_text = apr_psprintf(r->pool, "%s# HELP %s The number of %s.\n", s_text, s_label, + j_spec = json_object_get(j_counter, OIDC_METRICS_SPEC); + s_desc = "The number of"; + if (j_spec) + s_desc = apr_psprintf(r->pool, "%s [%s]", s_desc, json_string_value(j_spec)); + s_text = apr_psprintf(r->pool, "%s# HELP %s %s %s.\n", s_text, s_label, s_desc, _oidc_metrics_counters_info[type].desc); s_text = apr_psprintf(r->pool, "%s# TYPE %s counter\n", s_text, s_label); s_text = apr_psprintf( @@ -814,7 +937,8 @@ static int oidc_metrics_handle_prometheus(request_rec *r, const char *s_json) { _oidc_metrics_counters_info[type].label_value); } j_member = json_object_get(j_counter, OIDC_METRICS_COUNT); - s_text = apr_psprintf(r->pool, "%s %ld\n", s_text, (long int)json_integer_value(j_member)); + s_text = apr_psprintf(r->pool, "%s %s\n", s_text, + _json_int2str(r->pool, json_integer_value(j_member))); s_text = apr_psprintf(r->pool, "%s\n", s_text); iter2 = json_object_iter_next(j_counters, iter2); @@ -839,9 +963,9 @@ static int oidc_metrics_handle_prometheus(request_rec *r, const char *s_json) { while (iter3) { s_bucket = json_object_iter_key(iter3); j_member = json_object_iter_value(iter3); - s_text = apr_psprintf(r->pool, "%s%s_%s %ld\n", s_text, s_label, - oidc_metrics_name_to_label(r, s_bucket), - (long int)json_integer_value(j_member)); + s_text = apr_psprintf(r->pool, "%s%s_%s %s\n", s_text, s_label, + oidc_metrics_bucket_label(r, s_bucket), + _json_int2str(r->pool, json_integer_value(j_member))); iter3 = json_object_iter_next(j_timing, iter3); } s_text = apr_psprintf(r->pool, "%s\n", s_text); @@ -858,34 +982,106 @@ static int oidc_metrics_handle_prometheus(request_rec *r, const char *s_json) { return oidc_util_http_send(r, s_text, _oidc_strlen(s_text), "text/plain; version=0.0.4", OK); } +/* + * definitions for handler callbacks + */ + +typedef int (*oidc_metrics_handler_function_t)(request_rec *, char *); + +typedef struct oidc_metrics_handler_t { + const char *format; + oidc_metrics_handler_function_t callback; + int reset; +} oidc_metrics_content_handler_t; + +const oidc_metrics_content_handler_t _oidc_metrics_handlers[] = { + // first is default + {"prometheus", oidc_metrics_handle_prometheus, 1}, + {"json", oidc_metrics_handle_json, 1}, + {"internal", oidc_metrics_handle_internal, 0}, + {"status", oidc_metrics_handle_status, 0}, +}; + +#define OIDC_CONTENT_HANDLER_MAX sizeof(_oidc_metrics_handlers) / sizeof(oidc_metrics_content_handler_t) + +#define OIDC_METRICS_RESET_PARAM "reset" + +/* + * see if we are going to reset the cache after this + */ +static int oidc_metric_reset(request_rec *r, int dvalue) { + char *s_reset = NULL; + char svalue[16]; + int value = 0; + + oidc_util_get_request_parameter(r, OIDC_METRICS_RESET_PARAM, &s_reset); + + if (s_reset == NULL) + return dvalue; + + sscanf(s_reset, "%s", svalue); + if (_oidc_strcmp(svalue, "true") == 0) + value = 1; + else if (_oidc_strcmp(svalue, "false") == 0) + value = 0; + + return value; +} + #define OIDC_METRICS_FORMAT_PARAM "format" -#define OIDC_METRICS_FORMAT_JSON "json" -#define OIDC_METRICS_FORMAT_PROMETHEUS "prometheus" + +/* + * find the format handler + */ +const oidc_metrics_content_handler_t *oidc_metrics_find_handler(request_rec *r) { + const oidc_metrics_content_handler_t *handler = NULL; + char *s_format = NULL; + int i = 0; + + /* get the specified format */ + oidc_util_get_request_parameter(r, OIDC_METRICS_FORMAT_PARAM, &s_format); + + if (s_format == NULL) + return &_oidc_metrics_handlers[0]; + + for (i = 0; i < OIDC_CONTENT_HANDLER_MAX; i++) { + if (_oidc_strcmp(s_format, _oidc_metrics_handlers[i].format) == 0) { + handler = &_oidc_metrics_handlers[i]; + break; + } + } + + if (handler == NULL) + oidc_warn(r, "could not find a metrics handler for format: %s", s_format); + + return handler; +} /* * return the metrics to the caller and flush the storage */ int oidc_metrics_handle_request(request_rec *r) { - char *s_format = NULL; char *s_json = NULL; - char *s_content_type = NULL; + const oidc_metrics_content_handler_t *handler = NULL; + + /* get the content handler for the format */ + handler = oidc_metrics_find_handler(r); + if (handler == NULL) + return HTTP_NOT_FOUND; /* lock the global shared memory */ oidc_cache_mutex_lock(r->pool, r->server, _oidc_metrics_global_mutex); /* retrieve the JSON formatted metrics as a string */ - oidc_metrics_storage_get(r->server, &s_json); + s_json = oidc_metrics_storage_get(r->server); /* now that the metrics have been consumed, clear the shared memory segment */ - oidc_metrics_storage_set(r->server, NULL); + if (oidc_metric_reset(r, handler->reset)) + oidc_metrics_storage_set(r->server, NULL); /* unlock the global shared memory */ oidc_cache_mutex_unlock(r->pool, r->server, _oidc_metrics_global_mutex); - /* handle specified format */ - oidc_util_get_request_parameter(r, OIDC_METRICS_FORMAT_PARAM, &s_format); - if (_oidc_strcmp(s_format, OIDC_METRICS_FORMAT_JSON) == 0) - return oidc_metrics_handle_json(r, s_json); - - return oidc_metrics_handle_prometheus(r, s_json); + /* handle the specified format */ + return handler->callback(r, s_json); } diff --git a/src/metrics.h b/src/metrics.h index 51a294fc..ab832da8 100644 --- a/src/metrics.h +++ b/src/metrics.h @@ -19,7 +19,6 @@ /*************************************************************************** * Copyright (C) 2017-2023 ZmartZone Holding BV - * Copyright (C) 2013-2017 Ping Identity Corporation * All rights reserved. * * DISCLAIMER OF WARRANTIES: @@ -49,22 +48,29 @@ apr_status_t oidc_metrics_cache_child_init(apr_pool_t *p, server_rec *s); apr_status_t oidc_metrics_cache_cleanup(server_rec *s); int oidc_metrics_handle_request(request_rec *r); +// NB: order must match what is defined in metrics.c in array _oidc_metrics_timings_info typedef enum { - OM_AUTHN_REQUEST = 0, + + OM_MOD_AUTH_OPENIDC = 0, + + OM_AUTHN_REQUEST, OM_AUTHN_RESPONSE, + OM_SESSION_VALID, + OM_PROVIDER_METADATA, OM_PROVIDER_TOKEN, OM_PROVIDER_REFRESH, OM_PROVIDER_USERINFO, + OM_CACHE_READ, OM_CACHE_WRITE, + } oidc_metrics_timing_type_t; typedef struct oidc_metrics_timing_info_t { - oidc_metrics_timing_type_t type; char *name; - char *subkey; + char *spec; char *desc; } oidc_metrics_timing_info_t; @@ -72,20 +78,45 @@ extern const oidc_metrics_timing_info_t _oidc_metrics_timings_info[]; void oidc_metrics_timing_add(request_rec *r, oidc_metrics_timing_type_t type, apr_time_t elapsed); +#define OIDC_METRICS_TIMING_VAR apr_time_t _oidc_metrics_tstart; + #define OIDC_METRICS_TIMING_START(r, cfg) \ - apr_time_t _oidc_metrics_tstart; \ - if (cfg->metrics_hook_data != NULL) \ - _oidc_metrics_tstart = apr_time_now(); + OIDC_METRICS_TIMING_VAR \ + if (cfg->metrics_hook_data != NULL) { \ + _oidc_metrics_tstart = apr_time_now(); \ + } #define OIDC_METRICS_TIMING_ADD(r, cfg, type) \ - if (cfg->metrics_hook_data != NULL) \ + if (cfg->metrics_hook_data != NULL) { \ if (apr_hash_get(cfg->metrics_hook_data, _oidc_metrics_timings_info[type].name, \ - APR_HASH_KEY_STRING) != NULL) \ - oidc_metrics_timing_add(r, type, apr_time_now() - _oidc_metrics_tstart); - + APR_HASH_KEY_STRING) != NULL) { \ + oidc_metrics_timing_add(r, type, apr_time_now() - _oidc_metrics_tstart); \ + } \ + } +#define OIDC_METRICS_REQUEST_STATE_TIMER_KEY "oidc-metrics-request-timer" + +#define OIDC_METRICS_TIMING_REQUEST_START(r, cfg) \ + if (cfg->metrics_hook_data != NULL) { \ + oidc_request_state_set(r, OIDC_METRICS_REQUEST_STATE_TIMER_KEY, \ + apr_psprintf(r->pool, "%" APR_TIME_T_FMT, apr_time_now())); \ + } + +#define OIDC_METRICS_TIMING_REQUEST_ADD(r, cfg, type) \ + OIDC_METRICS_TIMING_VAR \ + const char *v = NULL; \ + if (cfg->metrics_hook_data != NULL) { \ + v = oidc_request_state_get(r, OIDC_METRICS_REQUEST_STATE_TIMER_KEY); \ + if (v) { \ + sscanf(v, "%" APR_TIME_T_FMT, &_oidc_metrics_tstart); \ + OIDC_METRICS_TIMING_ADD(r, cfg, type); \ + } \ + } + +// NB: order must match what is defined in metrics.c in array _oidc_metrics_counters_info typedef enum { - OM_AUTHTYPE_OPENID_CONNECT = 0, + OM_AUTHTYPE_MOD_AUTH_OPENIDC = 0, + OM_AUTHTYPE_OPENID_CONNECT, OM_AUTHTYPE_OAUTH20, OM_AUTHTYPE_AUTH_OPENIDC, OM_AUTHTYPE_DECLINED, @@ -98,13 +129,15 @@ typedef enum { OM_AUTHN_RESPONSE_ERROR_PROTOCOL, OM_AUTHN_RESPONSE_ERROR_REMOTE_USER, - OM_AUTHZ_ERROR_GENERAL, OM_AUTHZ_ACTION_AUTH, OM_AUTHZ_ACTION_401, OM_AUTHZ_ACTION_403, OM_AUTHZ_ACTION_302, OM_AUTHZ_ERROR_OAUTH20, + OM_AUTHZ_MATCH_REQUIRE_CLAIM, + OM_AUTHZ_ERROR_REQUIRE_CLAIM, + OM_PROVIDER_METADATA_ERROR, OM_PROVIDER_TOKEN_ERROR, OM_PROVIDER_REFRESH_ERROR, @@ -143,7 +176,6 @@ typedef enum { } oidc_metrics_counter_type_t; typedef struct oidc_metrics_counter_info_t { - oidc_metrics_counter_type_t type; char *name; char *label_name; char *label_value; @@ -152,12 +184,14 @@ typedef struct oidc_metrics_counter_info_t { extern const oidc_metrics_counter_info_t _oidc_metrics_counters_info[]; -void oidc_metrics_counter_inc(request_rec *r, oidc_metrics_counter_type_t type); +void oidc_metrics_counter_inc(request_rec *r, oidc_metrics_counter_type_t type, const char *spec); -#define OIDC_METRICS_COUNTER_INC(r, cfg, type) \ +#define OIDC_METRICS_COUNTER_INC_SPEC(r, cfg, type, spec) \ if (cfg->metrics_hook_data != NULL) \ if (apr_hash_get(cfg->metrics_hook_data, _oidc_metrics_counters_info[type].name, \ APR_HASH_KEY_STRING) != NULL) \ - oidc_metrics_counter_inc(r, type); + oidc_metrics_counter_inc(r, type, spec); + +#define OIDC_METRICS_COUNTER_INC(r, cfg, type) OIDC_METRICS_COUNTER_INC_SPEC(r, cfg, type, NULL); #endif /* MOD_AUTH_OPENIDC_METRICS_H_ */ diff --git a/src/mod_auth_openidc.c b/src/mod_auth_openidc.c index 5804dee9..91f35819 100644 --- a/src/mod_auth_openidc.c +++ b/src/mod_auth_openidc.c @@ -838,15 +838,15 @@ static void oidc_log_session_expires(request_rec *r, const char *msg, apr_time_t apr_byte_t oidc_is_auth_capable_request(request_rec *r) { if ((oidc_util_hdr_in_x_requested_with_get(r) != NULL) && - (apr_strnatcasecmp(oidc_util_hdr_in_x_requested_with_get(r), OIDC_HTTP_HDR_VAL_XML_HTTP_REQUEST) == 0)) + (_oidc_strnatcasecmp(oidc_util_hdr_in_x_requested_with_get(r), OIDC_HTTP_HDR_VAL_XML_HTTP_REQUEST) == 0)) return FALSE; if ((oidc_util_hdr_in_sec_fetch_mode_get(r) != NULL) && - (apr_strnatcasecmp(oidc_util_hdr_in_sec_fetch_mode_get(r), OIDC_HTTP_HDR_VAL_NAVIGATE) != 0)) + (_oidc_strnatcasecmp(oidc_util_hdr_in_sec_fetch_mode_get(r), OIDC_HTTP_HDR_VAL_NAVIGATE) != 0)) return FALSE; if ((oidc_util_hdr_in_sec_fetch_dest_get(r) != NULL) && - (apr_strnatcasecmp(oidc_util_hdr_in_sec_fetch_dest_get(r), OIDC_HTTP_HDR_VAL_DOCUMENT) != 0)) + (_oidc_strnatcasecmp(oidc_util_hdr_in_sec_fetch_dest_get(r), OIDC_HTTP_HDR_VAL_DOCUMENT) != 0)) return FALSE; if ((oidc_util_hdr_in_accept_contains(r, OIDC_CONTENT_TYPE_TEXT_HTML) == FALSE) && @@ -2454,7 +2454,7 @@ static int oidc_authenticate_user(request_rec *r, oidc_cfg *c, oidc_provider_t * int rc; - OIDC_METRICS_TIMING_START(r, c) + OIDC_METRICS_TIMING_START(r, c); oidc_debug(r, "enter"); @@ -3935,7 +3935,7 @@ int oidc_handle_redirect_uri_request(request_rec *r, oidc_cfg *c, oidc_session_t apr_byte_t needs_save = FALSE; int rc = OK; - OIDC_METRICS_TIMING_START(r, c) + OIDC_METRICS_TIMING_START(r, c); if (oidc_proto_is_redirect_authorization_response(r, c)) { @@ -4253,6 +4253,15 @@ static int oidc_check_mixed_userid_oauth(request_rec *r, oidc_cfg *c) { return oidc_check_userid_openidc(r, c); } +int oidc_fixups(request_rec *r) { + oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); + if (oidc_enabled(r) == TRUE) { + OIDC_METRICS_TIMING_REQUEST_ADD(r, c, OM_MOD_AUTH_OPENIDC); + return OK; + } + return DECLINED; +} + /* * generic Apache authentication hook for this module: dispatches to OpenID Connect or OAuth 2.0 specific routines */ @@ -4260,41 +4269,39 @@ int oidc_check_user_id(request_rec *r) { oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); int rv = DECLINED; - const char *current_auth = NULL; + + OIDC_METRICS_TIMING_REQUEST_START(r, c); /* log some stuff about the incoming HTTP request */ oidc_debug(r, "incoming request: \"%s?%s\", ap_is_initial_req(r)=%d", r->parsed_uri.path, r->args, ap_is_initial_req(r)); - /* see if any authentication has been defined at all */ - current_auth = ap_auth_type(r); - - if (current_auth == NULL) { - + if (oidc_enabled(r) == FALSE) { OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHTYPE_DECLINED); + return DECLINED; + } - /* see if we've configured OpenID Connect user authentication for this request */ - } else if (strcasecmp(current_auth, OIDC_AUTH_TYPE_OPENID_CONNECT) == 0) { + OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHTYPE_MOD_AUTH_OPENIDC); + + /* see if we've configured OpenID Connect user authentication for this request */ + if (_oidc_strnatcasecmp(ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_CONNECT) == 0) { OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHTYPE_OPENID_CONNECT); - r->ap_auth_type = (char *)current_auth; + r->ap_auth_type = apr_pstrdup(r->pool, ap_auth_type(r)); rv = oidc_check_userid_openidc(r, c); /* see if we've configured OAuth 2.0 access control for this request */ - } else if (strcasecmp(current_auth, OIDC_AUTH_TYPE_OPENID_OAUTH20) == 0) { + } else if (_oidc_strnatcasecmp(ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_OAUTH20) == 0) { OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHTYPE_OAUTH20); - r->ap_auth_type = (char *)current_auth; + r->ap_auth_type = apr_pstrdup(r->pool, ap_auth_type(r)); rv = oidc_oauth_check_userid(r, c, NULL); /* see if we've configured "mixed mode" for this request */ - } else if (strcasecmp(current_auth, OIDC_AUTH_TYPE_OPENID_BOTH) == 0) { + } else if (_oidc_strnatcasecmp(ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_BOTH) == 0) { OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHTYPE_AUTH_OPENIDC); rv = oidc_check_mixed_userid_oauth(r, c); - } else { - - OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHTYPE_DECLINED); } return rv; @@ -4331,7 +4338,7 @@ static authz_status oidc_handle_unauthorized_user24(request_rec *r) { oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); - if (apr_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_OAUTH20) == 0) { + if (_oidc_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_OAUTH20) == 0) { OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHZ_ERROR_OAUTH20); oidc_debug(r, "setting environment variable %s to \"%s\" for usage in mod_headers", OIDC_OAUTH_BEARER_SCOPE_ERROR, OIDC_OAUTH_BEARER_SCOPE_ERROR_VALUE); @@ -4452,7 +4459,7 @@ static int oidc_handle_unauthorized_user22(request_rec *r) { oidc_cfg *c = ap_get_module_config(r->server->module_config, &auth_openidc_module); - if (apr_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_OAUTH20) == 0) { + if (_oidc_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_OAUTH20) == 0) { OIDC_COUNTER_INC(r, cfg, OM_AUTHZ_ERROR_OAUTH20); oidc_oauth_return_www_authenticate(r, "insufficient_scope", "Different scope(s) or other claims required"); @@ -4551,13 +4558,13 @@ apr_byte_t oidc_enabled(request_rec *r) { if (ap_auth_type(r) == NULL) return FALSE; - if (apr_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_CONNECT) == 0) + if (_oidc_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_CONNECT) == 0) return TRUE; - if (apr_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_OAUTH20) == 0) + if (_oidc_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_OAUTH20) == 0) return TRUE; - if (apr_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_BOTH) == 0) + if (_oidc_strnatcasecmp((const char *)ap_auth_type(r), OIDC_AUTH_TYPE_OPENID_BOTH) == 0) return TRUE; return FALSE; diff --git a/src/mod_auth_openidc.h b/src/mod_auth_openidc.h index 71212d1e..47333e09 100644 --- a/src/mod_auth_openidc.h +++ b/src/mod_auth_openidc.h @@ -527,6 +527,7 @@ typedef struct oidc_cfg { } oidc_cfg; void oidc_pre_config_init(); +int oidc_fixups(request_rec *r); int oidc_check_user_id(request_rec *r); #if HAVE_APACHE_24 authz_status oidc_authz_checker_claim(request_rec *r, const char *require_args, const void *parsed_require_args); diff --git a/src/oauth.c b/src/oauth.c index b6645bac..16f6decf 100644 --- a/src/oauth.c +++ b/src/oauth.c @@ -195,7 +195,7 @@ apr_byte_t oidc_oauth_get_bearer_token(request_rec *r, const char **access_token apr_byte_t known_scheme = 0; /* look for the Bearer keyword */ - if ((apr_strnatcasecmp(ap_getword(r->pool, &auth_line, OIDC_CHAR_SPACE), OIDC_PROTO_BEARER) == + if ((_oidc_strnatcasecmp(ap_getword(r->pool, &auth_line, OIDC_CHAR_SPACE), OIDC_PROTO_BEARER) == 0) && accept_header) { @@ -440,7 +440,7 @@ static apr_byte_t oidc_oauth_resolve_access_token(request_rec *r, oidc_cfg *c, c return FALSE; } } else if (json_is_string(active)) { - if (apr_strnatcasecmp(json_string_value(active), "true") != 0) { + if (_oidc_strnatcasecmp(json_string_value(active), "true") != 0) { oidc_debug(r, "\"%s\" string object with value that is not equal to \"true\" " "found in response JSON object: %s", diff --git a/src/parse.c b/src/parse.c index 4a219918..d5841a58 100644 --- a/src/parse.c +++ b/src/parse.c @@ -347,13 +347,13 @@ const char *oidc_parse_cache_shm_entry_size_max(apr_pool_t *pool, const char *ar * parse a boolean value from a provided string */ const char *oidc_parse_boolean(apr_pool_t *pool, const char *arg, int *bool_value) { - if ((apr_strnatcasecmp(arg, "true") == 0) || (apr_strnatcasecmp(arg, "on") == 0) || - (apr_strnatcasecmp(arg, "yes") == 0) || (apr_strnatcasecmp(arg, "1") == 0)) { + if ((_oidc_strnatcasecmp(arg, "true") == 0) || (_oidc_strnatcasecmp(arg, "on") == 0) || + (_oidc_strnatcasecmp(arg, "yes") == 0) || (_oidc_strnatcasecmp(arg, "1") == 0)) { *bool_value = TRUE; return NULL; } - if ((apr_strnatcasecmp(arg, "false") == 0) || (apr_strnatcasecmp(arg, "off") == 0) || - (apr_strnatcasecmp(arg, "no") == 0) || (apr_strnatcasecmp(arg, "0") == 0)) { + if ((_oidc_strnatcasecmp(arg, "false") == 0) || (_oidc_strnatcasecmp(arg, "off") == 0) || + (_oidc_strnatcasecmp(arg, "no") == 0) || (_oidc_strnatcasecmp(arg, "0") == 0)) { *bool_value = FALSE; return NULL; } diff --git a/src/proto.c b/src/proto.c index 360c15d6..4f25ada2 100644 --- a/src/proto.c +++ b/src/proto.c @@ -1538,7 +1538,7 @@ apr_byte_t oidc_proto_parse_idtoken(request_rec *r, oidc_cfg *cfg, oidc_provider */ static apr_byte_t oidc_proto_validate_token_type(request_rec *r, oidc_provider_t *provider, const char *token_type) { /* we only support bearer/Bearer */ - if ((token_type != NULL) && (apr_strnatcasecmp(token_type, OIDC_PROTO_BEARER) != 0) && + if ((token_type != NULL) && (_oidc_strnatcasecmp(token_type, OIDC_PROTO_BEARER) != 0) && (provider->userinfo_endpoint_url != NULL)) { oidc_error(r, "token_type is \"%s\" and UserInfo endpoint (%s) for issuer \"%s\" is set: can only deal " diff --git a/src/util.c b/src/util.c index 54ad3789..314f68e2 100644 --- a/src/util.c +++ b/src/util.c @@ -1297,7 +1297,7 @@ const char *oidc_util_set_cookie_append_value(request_rec *r) { } apr_byte_t oidc_util_request_is_secure(request_rec *r, const oidc_cfg *c) { - return (apr_strnatcasecmp("https", oidc_get_current_url_scheme(r, c->x_forwarded_headers)) == 0); + return (_oidc_strnatcasecmp("https", oidc_get_current_url_scheme(r, c->x_forwarded_headers)) == 0); } /*