diff options
author | Glenn Strauss <gstrauss@gluelogic.com> | 2019-05-20 00:53:20 -0400 |
---|---|---|
committer | Glenn Strauss <gstrauss@gluelogic.com> | 2019-05-26 10:21:57 -0400 |
commit | 4ac239c40193378ae09f28355530a3a04858065e (patch) | |
tree | a122691eb51b4001653f2ad92637ab7f17671e5e /src | |
parent | ef0a2117331ef9d2428f27fe208a5aafa3a4df72 (diff) | |
download | lighttpd-git-4ac239c40193378ae09f28355530a3a04858065e.tar.gz |
[mod_maxminddb] MaxMind GeoIP2 support
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 10 | ||||
-rw-r--r-- | src/Makefile.am | 11 | ||||
-rw-r--r-- | src/SConscript | 3 | ||||
-rw-r--r-- | src/meson.build | 6 | ||||
-rw-r--r-- | src/mod_maxminddb.c | 405 |
5 files changed, 435 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6026520f..7a38aa0b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ option(WITH_MEMCACHED "memcached storage for mod_trigger_b4_dl [default: off]") option(WITH_LIBEV "libev support for fdevent handlers [default: off]") option(WITH_LIBUNWIND "with libunwind to print backtraces in asserts [default: off]") option(WITH_GEOIP "with GeoIP-support mod_geoip [default: off]") +option(WITH_MAXMINDDB "with MaxMind GeoIP2-support mod_maxminddb [default: off]") option(WITH_SASL "with SASL-support for mod_authn_sasl [default: off]") if(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_C_COMPILER_ID MATCHES "Clang") @@ -586,6 +587,10 @@ if(WITH_GEOIP) check_library_exists(geoip GeoIP_country_name_by_addr "" HAVE_GEOIP) endif() +if(WITH_MAXMINDDB) + check_library_exists(maxminddb MMDB_open "" HAVE_MAXMINDDB) +endif() + if(NOT BUILD_STATIC) check_include_files(dlfcn.h HAVE_DLFCN_H) else() @@ -882,6 +887,11 @@ if(WITH_GEOIP) target_link_libraries(mod_geoip GeoIP) endif() +if(WITH_MAXMINDDB) + add_and_install_library(mod_maxminddb mod_maxminddb.c) + target_link_libraries(mod_maxminddb maxminddb) +endif() + if(HAVE_MYSQL_H AND HAVE_MYSQL) add_and_install_library(mod_mysql_vhost "mod_mysql_vhost.c") target_link_libraries(mod_mysql_vhost mysqlclient) diff --git a/src/Makefile.am b/src/Makefile.am index 2dccaabb..ac50f9c8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -135,6 +135,13 @@ mod_geoip_la_LDFLAGS = $(common_module_ldflags) mod_geoip_la_LIBADD = $(common_libadd) $(GEOIP_LIB) endif +if BUILD_WITH_MAXMINDDB +lib_LTLIBRARIES += mod_maxminddb.la +mod_maxminddb_la_SOURCES = mod_maxminddb.c +mod_maxminddb_la_LDFLAGS = $(common_module_ldflags) +mod_maxminddb_la_LIBADD = $(common_libadd) $(MAXMINDDB_LIB) +endif + lib_LTLIBRARIES += mod_evasive.la mod_evasive_la_SOURCES = mod_evasive.c mod_evasive_la_LDFLAGS = $(common_module_ldflags) @@ -486,6 +493,10 @@ if BUILD_WITH_GEOIP lighttpd_SOURCES += mod_geoip.c lighttpd_LDADD += $(GEOIP_LIB) endif +if BUILD_WITH_MAXMINDDB +lighttpd_SOURCES += mod_maxminddb.c +lighttpd_LDADD += $(MAXMINDDB_LIB) +endif if BUILD_WITH_LUA lighttpd_SOURCES += mod_cml.c mod_cml_lua.c mod_cml_funcs.c \ mod_magnet.c mod_magnet_cache.c diff --git a/src/SConscript b/src/SConscript index 003a377f..d181ce5a 100644 --- a/src/SConscript +++ b/src/SConscript @@ -136,6 +136,9 @@ modules = { if env['with_geoip']: modules['mod_geoip'] = { 'src' : [ 'mod_geoip.c' ], 'lib' : [ env['LIBGEOIP'] ] } +if env['with_maxminddb']: + modules['mod_maxminddb'] = { 'src' : [ 'mod_maxminddb.c' ], 'lib' : [ env['LIBMAXMINDDB'] ] } + if env['with_krb5']: modules['mod_authn_gssapi'] = { 'src' : [ 'mod_authn_gssapi.c' ], 'lib' : [ env['LIBKRB5'], env['LIBGSSAPI_KRB5'] ] } diff --git a/src/meson.build b/src/meson.build index 6a2e53d3..5c0fc607 100644 --- a/src/meson.build +++ b/src/meson.build @@ -910,6 +910,12 @@ if get_option('with_lua') ] endif +if get_option('with_maxminddb') + modules += [ + [ 'mod_maxminddb', [ 'mod_maxminddb.c' ], libmaxminddb ], + ] +endif + if get_option('with_geoip') modules += [ [ 'mod_geoip', [ 'mod_geoip.c' ], libgeoip ], diff --git a/src/mod_maxminddb.c b/src/mod_maxminddb.c new file mode 100644 index 00000000..43134440 --- /dev/null +++ b/src/mod_maxminddb.c @@ -0,0 +1,405 @@ +/** + * + * Name: + * mod_maxminddb.c + * + * Description: + * MaxMind GeoIP2 module (plugin) for lighttpd. + * + * GeoIP2 country db env's: + * GEOIP_COUNTRY_CODE + * GEOIP_COUNTRY_NAME + * + * GeoIP2 city db env's: + * GEOIP_COUNTRY_CODE + * GEOIP_COUNTRY_NAME + * GEOIP_CITY_NAME + * GEOIP_CITY_LATITUDE + * GEOIP_CITY_LONGITUDE + * + * Usage (configuration options): + * maxminddb.db = <path to the geoip or geocity database> + * GeoLite2 database filenames end in ".mmdb" + * maxminddb.activate = <enable|disable> : default disabled + * maxminddb.env = ( + * "GEOIP_COUNTRY_CODE" => "country/iso_code", + * "GEOIP_COUNTRY_NAME" => "country/names/en", + * "GEOIP_CITY_NAME" => "city/names/en", + * "GEOIP_CITY_LATITUDE" => "location/latitude", + * "GEOIP_CITY_LONGITUDE" => "location/longitude", + * ) + * + * Installation Instructions: + * https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModGeoip + * + * References: + * https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModGeoip + * http://dev.maxmind.com/geoip/legacy/geolite/ + * http://dev.maxmind.com/geoip/geoip2/geolite2/ + * http://dev.maxmind.com/geoip/geoipupdate/ + * + * GeoLite2 database format + * http://maxmind.github.io/MaxMind-DB/ + * https://github.com/maxmind/libmaxminddb + * + * Note: GeoLite2 databases are free IP geolocation databases comparable to, + * but less accurate than, MaxMind’s GeoIP2 databases. + * If you are a commercial entity, please consider a subscription to the + * more accurate databases to support MaxMind. + * http://dev.maxmind.com/geoip/geoip2/downloadable/ + */ + +#include "first.h" /* first */ +#include "sys-socket.h" /* AF_INET AF_INET6 */ +#include <stdlib.h> +#include <string.h> + +#include "base.h" +#include "buffer.h" +#include "http_header.h" +#include "log.h" +#include "sock_addr.h" + +#include "plugin.h" + +#include <maxminddb.h> + +SETDEFAULTS_FUNC(mod_maxmind_set_defaults); +INIT_FUNC(mod_maxmind_init); +FREE_FUNC(mod_maxmind_free); +CONNECTION_FUNC(mod_maxmind_request_env_handler); +CONNECTION_FUNC(mod_maxmind_handle_con_close); + +int mod_maxminddb_plugin_init(plugin *p); +int mod_maxminddb_plugin_init(plugin *p) { + p->version = LIGHTTPD_VERSION_ID; + p->name = buffer_init_string("maxminddb"); + + p->set_defaults = mod_maxmind_set_defaults; + p->init = mod_maxmind_init; + p->cleanup = mod_maxmind_free; + p->handle_request_env = mod_maxmind_request_env_handler; + p->handle_connection_close = mod_maxmind_handle_con_close; + + p->data = NULL; + + return 0; +} + +typedef struct { + int activate; + array *env; + const char ***cenv; + struct MMDB_s *mmdb; + buffer *db_name; +} plugin_config; + +typedef struct { + PLUGIN_DATA; + int nconfig; + plugin_config **config_storage; +} plugin_data; + + +INIT_FUNC(mod_maxmind_init) +{ + return calloc(1, sizeof(plugin_data)); +} + + +FREE_FUNC(mod_maxmind_free) +{ + plugin_data *p = (plugin_data *)p_d; + if (!p) return HANDLER_GO_ON; + + if (p->config_storage) { + for (int i = 0; i < p->nconfig; ++i) { + plugin_config * const s = p->config_storage[i]; + if (!s) continue; + buffer_free(s->db_name); + if (s->mmdb) { MMDB_close(s->mmdb); free(s->mmdb); } + for (size_t k = 0, used = s->env->used; k < used; ++k) + free(s->cenv[k]); + free(s->cenv); + array_free(s->env); + } + free(p->config_storage); + } + + free(p); + + UNUSED(srv); + return HANDLER_GO_ON; +} + + +SETDEFAULTS_FUNC(mod_maxmind_set_defaults) +{ + static config_values_t cv[] = { + { "maxminddb.activate", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, + { "maxminddb.db", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, + { "maxminddb.env", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, + + { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET } + }; + + plugin_data * const p = (plugin_data *)p_d; + const size_t n_context = p->nconfig = srv->config_context->used; + p->config_storage = calloc(p->nconfig, sizeof(plugin_config *)); + force_assert(p->config_storage); + + for (size_t i = 0; i < n_context; ++i) { + plugin_config * const s = calloc(1, sizeof(plugin_config)); + force_assert(s); + p->config_storage[i] = s; + s->db_name = buffer_init(); + s->env = array_init(); + + cv[0].destination = &s->activate; + cv[1].destination = s->db_name; + cv[2].destination = s->env; + + array * const ca = ((data_config *)srv->config_context->data[i])->value; + if (0 != config_insert_values_global(srv, ca, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) { + return HANDLER_ERROR; + } + + if (!buffer_is_empty(s->db_name)) { + + if (s->db_name->used >= sizeof(".mmdb") + && 0 == memcmp(s->db_name->ptr+s->db_name->used-sizeof(".mmdb"), + CONST_STR_LEN(".mmdb"))) { + MMDB_s * const mmdb = (MMDB_s *)calloc(1, sizeof(MMDB_s)); + int rc = MMDB_open(s->db_name->ptr, MMDB_MODE_MMAP, mmdb); + if (MMDB_SUCCESS != rc) { + if (MMDB_IO_ERROR == rc) + log_perror(srv->errh, __FILE__, __LINE__, + "failed to open GeoIP2 database (%.*s)", + BUFFER_INTLEN_PTR(s->db_name)); + else + log_error(srv->errh, __FILE__, __LINE__, + "failed to open GeoIP2 database (%.*s): %s", + BUFFER_INTLEN_PTR(s->db_name), + MMDB_strerror(rc)); + free(mmdb); + return HANDLER_ERROR; + } + s->mmdb = mmdb; + } + else { + log_error(srv->errh, __FILE__, __LINE__, + "GeoIP database is of unsupported type %.*s)", + BUFFER_INTLEN_PTR(s->db_name)); + return HANDLER_ERROR; + } + } + + if (s->env->used) { + data_string **data = (data_string **)s->env->data; + s->cenv = calloc(s->env->used, sizeof(char **)); + force_assert(s->cenv); + for (size_t j = 0, used = s->env->used; j < used; ++j) { + if (data[j]->type != TYPE_STRING) { + log_error(srv->errh, __FILE__, __LINE__, + "maxminddb.env must be a list of strings"); + return HANDLER_ERROR; + } + buffer *value = data[j]->value; + if (buffer_string_is_empty(value) + || '/' == value->ptr[0] + || '/' == value->ptr[buffer_string_length(value)-1]) { + log_error(srv->errh, __FILE__, __LINE__, + "maxminddb.env must be a list of non-empty " + "strings and must not begin or end with '/'"); + return HANDLER_ERROR; + } + /* XXX: should strings be lowercased? */ + unsigned int k = 2; + for (char *t = value->ptr; (t = strchr(t, '/')); ++t) ++k; + const char **keys = s->cenv[j] = calloc(k, sizeof(char *)); + force_assert(keys); + k = 0; + keys[k] = value->ptr; + for (char *t = value->ptr; (t = strchr(t, '/')); ) { + *t = '\0'; + keys[++k] = ++t; + } + keys[++k] = NULL; + } + } + } + + return HANDLER_GO_ON; +} + + +static void +geoip2_env_set (array * const env, const char *k, size_t klen, + MMDB_entry_data_s *data) +{ + /* GeoIP2 database interfaces return pointers directly into database, + * and these are valid until the database is closed. + * However, note that the strings *are not* '\0'-terminated */ + char buf[35]; + if (!data->has_data || 0 == data->offset) return; + switch (data->type) { + case MMDB_DATA_TYPE_UTF8_STRING: + array_set_key_value(env, k, klen, data->utf8_string, data->data_size); + return; + case MMDB_DATA_TYPE_BOOLEAN: + array_set_key_value(env, k, klen, data->boolean ? "1" : "0", 1); + return; + case MMDB_DATA_TYPE_BYTES: + array_set_key_value(env, k, klen, + (const char *) data->bytes, data->data_size); + return; + case MMDB_DATA_TYPE_DOUBLE: + array_set_key_value(env, k, klen, + buf, snprintf(buf, sizeof(buf), "%.5f", + data->double_value)); + return; + case MMDB_DATA_TYPE_FLOAT: + array_set_key_value(env, k, klen, + buf, snprintf(buf, sizeof(buf), "%.5f", + data->float_value)); + return; + case MMDB_DATA_TYPE_INT32: + li_itostrn(buf, sizeof(buf), data->int32); + break; + case MMDB_DATA_TYPE_UINT32: + li_utostrn(buf, sizeof(buf), data->uint32); + break; + case MMDB_DATA_TYPE_UINT16: + li_utostrn(buf, sizeof(buf), data->uint16); + break; + case MMDB_DATA_TYPE_UINT64: + /* truncated value on 32-bit unless uintmax_t is 64-bit (long long) */ + li_utostrn(buf, sizeof(buf), data->uint64); + break; + case MMDB_DATA_TYPE_UINT128: + buf[0] = '0'; + buf[1] = 'x'; + #if MMDB_UINT128_IS_BYTE_ARRAY + li_tohex_uc(buf+2, sizeof(buf)-2, (char *)data->uint128, 16); + #else + li_tohex_uc(buf+2, sizeof(buf)-2, (char *)&data->uint128, 16); + #endif + array_set_key_value(env, k, klen, buf, 34); + return; + default: /*(ignore unknown data type)*/ + return; + } + + array_set_key_value(env, k, klen, buf, strlen(buf)); /*(numerical types)*/ +} + + +static void +mod_maxmind_geoip2 (array * const env, sock_addr *dst_addr, + plugin_config *pconf) +{ + MMDB_lookup_result_s res; + MMDB_entry_data_s data; + int rc; + + res = MMDB_lookup_sockaddr(pconf->mmdb, (struct sockaddr *)dst_addr, &rc); + if (MMDB_SUCCESS != rc || !res.found_entry) return; + MMDB_entry_s * const entry = &res.entry; + + const data_string ** const names = (const data_string **)pconf->env->data; + const char *** const cenv = pconf->cenv; + for (size_t i = 0, used = pconf->env->used; i < used; ++i) { + if (MMDB_SUCCESS == MMDB_aget_value(entry, &data, cenv[i]) + && data.has_data) { + geoip2_env_set(env, CONST_BUF_LEN(names[i]->key), &data); + } + } +} + + +static void +mod_maxmind_patch_connection (server * const srv, + connection * const con, + const plugin_data * const p, + plugin_config * const pconf) +{ + const plugin_config *s = p->config_storage[0]; + memcpy(pconf, s, sizeof(*s)); + if (1 == p->nconfig) + return; + + data_config ** const context_data = + (data_config **)srv->config_context->data; + + s = p->config_storage[1]; /* base config (global context) copied above */ + for (size_t i = 1; i < srv->config_context->used; ++i) { + data_config *dc = context_data[i]; + if (!config_check_cond(srv, con, dc)) + continue; /* condition did not match */ + + s = p->config_storage[i]; + + /* merge config */ + #define PATCH(x) pconf->x = s->x; + for (size_t j = 0; j < dc->value->used; ++j) { + data_unset *du = dc->value->data[j]; + + if (buffer_is_equal_string(du->key, CONST_STR_LEN("maxminddb.activate"))) { + PATCH(activate); + } + else if (buffer_is_equal_string(du->key, CONST_STR_LEN("maxminddb.db"))) { + /*PATCH(db_name);*//*(not used)*/ + PATCH(mmdb); + } + else if (buffer_is_equal_string(du->key, CONST_STR_LEN("maxminddb.env"))) { + PATCH(env); + PATCH(cenv); + } + } + #undef PATCH + } +} + + +CONNECTION_FUNC(mod_maxmind_request_env_handler) +{ + const int sa_family = con->dst_addr.plain.sa_family; + if (sa_family != AF_INET && sa_family != AF_INET6) return HANDLER_GO_ON; + + plugin_config pconf; + plugin_data *p = p_d; + mod_maxmind_patch_connection(srv, con, p, &pconf); + /* check that mod_maxmind is activated and env fields were requested */ + if (!pconf.activate || 0 == pconf.env->used) return HANDLER_GO_ON; + + array *env = con->plugin_ctx[p->id]; + if (NULL == env) { + env = con->plugin_ctx[p->id] = array_init(); + if (pconf.mmdb) + mod_maxmind_geoip2(env, &con->dst_addr, &pconf); + } + + for (size_t i = 0; i < env->used; ++i) { + /* note: replaces values which may have been set by mod_openssl + * (when mod_extforward is listed after mod_openssl in server.modules)*/ + data_string *ds = (data_string *)env->data[i]; + http_header_env_set(con, + CONST_BUF_LEN(ds->key), CONST_BUF_LEN(ds->value)); + } + + return HANDLER_GO_ON; +} + + +CONNECTION_FUNC(mod_maxmind_handle_con_close) +{ + plugin_data *p = p_d; + array *env = con->plugin_ctx[p->id]; + UNUSED(srv); + if (NULL != env) { + array_free(env); + con->plugin_ctx[p->id] = NULL; + } + + return HANDLER_GO_ON; +} |