lighttpd1.4/src/mod_maxminddb.c

465 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* mod_maxminddb - MaxMind GeoIP2 support for lighttpd
*
* Copyright(c) 2019 Glenn Strauss gstrauss()gluelogic.com All rights reserved
* License: BSD 3-clause (same as lighttpd)
*/
/**
*
* 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, MaxMinds 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_maxminddb_set_defaults);
INIT_FUNC(mod_maxminddb_init);
FREE_FUNC(mod_maxminddb_free);
REQUEST_FUNC(mod_maxminddb_request_env_handler);
CONNECTION_FUNC(mod_maxminddb_handle_con_close);
int mod_maxminddb_plugin_init(plugin *p);
int mod_maxminddb_plugin_init(plugin *p) {
p->version = LIGHTTPD_VERSION_ID;
p->name = "maxminddb";
p->set_defaults = mod_maxminddb_set_defaults;
p->init = mod_maxminddb_init;
p->cleanup = mod_maxminddb_free;
p->handle_request_env = mod_maxminddb_request_env_handler;
p->handle_connection_close = mod_maxminddb_handle_con_close;
return 0;
}
typedef struct {
int activate;
const array *env;
const char ***cenv;
struct MMDB_s *mmdb;
} plugin_config;
typedef struct {
PLUGIN_DATA;
plugin_config defaults;
} plugin_data;
typedef struct {
const array *env;
const char ***cenv;
} plugin_config_env;
INIT_FUNC(mod_maxminddb_init)
{
return calloc(1, sizeof(plugin_data));
}
FREE_FUNC(mod_maxminddb_free)
{
plugin_data * const p = p_d;
if (NULL == p->cvlist) return;
/* (init i to 0 if global context; to 1 to skip empty global context) */
for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
for (; -1 != cpv->k_id; ++cpv) {
switch (cpv->k_id) {
case 1: /* maxminddb.db */
if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) {
struct MMDB_s *mmdb;
*(struct MMDB_s **)&mmdb = cpv->v.v;
MMDB_close(mmdb);
free(mmdb);
}
break;
case 2: /* maxminddb.env */
if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) {
plugin_config_env * const pcenv = cpv->v.v;
const array * const env = pcenv->env;
char ***cenv;
*(const char ****)&cenv = pcenv->cenv;
for (uint32_t k = 0, cused = env->used; k < cused; ++k)
free(cenv[k]);
free(cenv);
}
break;
default:
break;
}
}
}
}
static MMDB_s *
mod_maxminddb_open_db (server *srv, const buffer *db_name)
{
if (db_name->used < sizeof(".mmdb")
|| 0 != memcmp(db_name->ptr+db_name->used-sizeof(".mmdb"),
CONST_STR_LEN(".mmdb"))) {
log_error(srv->errh, __FILE__, __LINE__,
"GeoIP database is of unsupported type %.*s)",
BUFFER_INTLEN_PTR(db_name));
return NULL;
}
MMDB_s * const mmdb = (MMDB_s *)calloc(1, sizeof(MMDB_s));
int rc = MMDB_open(db_name->ptr, MMDB_MODE_MMAP, mmdb);
if (MMDB_SUCCESS == rc)
return mmdb;
if (MMDB_IO_ERROR == rc)
log_perror(srv->errh, __FILE__, __LINE__,
"failed to open GeoIP2 database (%.*s)",
BUFFER_INTLEN_PTR(db_name));
else
log_error(srv->errh, __FILE__, __LINE__,
"failed to open GeoIP2 database (%.*s): %s",
BUFFER_INTLEN_PTR(db_name), MMDB_strerror(rc));
free(mmdb);
return NULL;
}
static plugin_config_env *
mod_maxminddb_prep_cenv (server *srv, const array * const env)
{
data_string ** const data = (data_string **)env->data;
char *** const cenv = calloc(env->used, sizeof(char **));
force_assert(cenv);
for (uint32_t j = 0, used = 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");
for (uint32_t k = 0; k < j; ++k) free(cenv[k]);
free(cenv);
return NULL;
}
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 '/'");
for (uint32_t k = 0; k < j; ++k) free(cenv[k]);
free(cenv);
return NULL;
}
/* XXX: should strings be lowercased? */
unsigned int k = 2;
for (char *t = value->ptr; (t = strchr(t, '/')); ++t) ++k;
const char **keys = (const char **)(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;
}
plugin_config_env * const pcenv = malloc(sizeof(plugin_config_env));
force_assert(pcenv);
pcenv->env = env;
pcenv->cenv = (const char ***)cenv;
return pcenv;
}
static void
mod_maxminddb_merge_config_cpv(plugin_config * const pconf,
const config_plugin_value_t * const cpv)
{
switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
case 0: /* maxminddb.activate */
pconf->activate = (int)cpv->v.u;
break;
case 1: /* maxminddb.db */
if (cpv->vtype != T_CONFIG_LOCAL) break;
pconf->mmdb = cpv->v.v;
break;
case 2: /* maxminddb.env */
if (cpv->vtype == T_CONFIG_LOCAL) {
plugin_config_env * const pcenv = cpv->v.v;
pconf->env = pcenv->env;
pconf->cenv = pcenv->cenv;
}
break;
default:/* should not happen */
return;
}
}
static void
mod_maxminddb_merge_config (plugin_config * const pconf,
const config_plugin_value_t *cpv)
{
do {
mod_maxminddb_merge_config_cpv(pconf, cpv);
} while ((++cpv)->k_id != -1);
}
static void
mod_maxmind_patch_config (request_st * const r,
const plugin_data * const p,
plugin_config * const pconf)
{
*pconf = p->defaults; /* copy small struct instead of memcpy() */
/*memcpy(pconf, &p->defaults, sizeof(plugin_config));*/
for (int i = 1, used = p->nconfig; i < used; ++i) {
if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
mod_maxminddb_merge_config(pconf, p->cvlist + p->cvlist[i].v.u2[0]);
}
}
SETDEFAULTS_FUNC(mod_maxminddb_set_defaults)
{
static const config_plugin_keys_t cpk[] = {
{ CONST_STR_LEN("maxminddb.activate"),
T_CONFIG_BOOL,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("maxminddb.db"),
T_CONFIG_STRING,
T_CONFIG_SCOPE_CONNECTION }
,{ CONST_STR_LEN("maxminddb.env"),
T_CONFIG_ARRAY_VLIST,
T_CONFIG_SCOPE_CONNECTION }
,{ NULL, 0,
T_CONFIG_UNSET,
T_CONFIG_SCOPE_UNSET }
};
plugin_data * const p = p_d;
if (!config_plugin_values_init(srv, p, cpk, "mod_maxminddb"))
return HANDLER_ERROR;
/* process and validate config directives
* (init i to 0 if global context; to 1 to skip empty global context) */
for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
for (; -1 != cpv->k_id; ++cpv) {
switch (cpv->k_id) {
case 0: /* maxminddb.activate */
break;
case 1: /* maxminddb.db */
if (!buffer_is_empty(cpv->v.b)) {
cpv->v.v = mod_maxminddb_open_db(srv, cpv->v.b);
if (NULL == cpv->v.v) return HANDLER_ERROR;
cpv->vtype = T_CONFIG_LOCAL;
}
break;
case 2: /* maxminddb.env */
if (cpv->v.a->used) {
cpv->v.v = mod_maxminddb_prep_cenv(srv, cpv->v.a);
if (NULL == cpv->v.v) return HANDLER_ERROR;
cpv->vtype = T_CONFIG_LOCAL;
}
break;
default:/* should not happen */
break;
}
}
}
/* initialize p->defaults from global config context */
if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
if (-1 != cpv->k_id)
mod_maxminddb_merge_config(&p->defaults, cpv);
}
return HANDLER_GO_ON;
}
static void
geoip2_env_set (array * const env, const char * const k,
const size_t klen, MMDB_entry_data_s * const 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;
const char *v = buf;
size_t vlen;
switch (data->type) {
case MMDB_DATA_TYPE_UTF8_STRING:
v = data->utf8_string;
vlen = data->data_size;
break;
case MMDB_DATA_TYPE_BOOLEAN:
v = data->boolean ? "1" : "0";
vlen = 1;
break;
case MMDB_DATA_TYPE_BYTES:
v = (const char *)data->bytes;
vlen = data->data_size;
break;
case MMDB_DATA_TYPE_DOUBLE:
vlen = snprintf(buf, sizeof(buf), "%.5f", data->double_value);
break;
case MMDB_DATA_TYPE_FLOAT:
vlen = snprintf(buf, sizeof(buf), "%.5f", data->float_value);
break;
case MMDB_DATA_TYPE_INT32:
vlen = li_itostrn(buf, sizeof(buf), data->int32);
break;
case MMDB_DATA_TYPE_UINT32:
vlen = li_utostrn(buf, sizeof(buf), data->uint32);
break;
case MMDB_DATA_TYPE_UINT16:
vlen = 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) */
vlen = 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
vlen = 34;
break;
default: /*(ignore unknown data type)*/
return;
}
array_set_key_value(env, k, klen, v, vlen);
}
static void
mod_maxmind_geoip2 (array * const env, const struct sockaddr * const dst_addr,
plugin_config * const pconf)
{
MMDB_lookup_result_s res;
MMDB_entry_data_s data;
int rc;
res = MMDB_lookup_sockaddr(pconf->mmdb, 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);
}
}
}
REQUEST_FUNC(mod_maxminddb_request_env_handler)
{
const sock_addr * const dst_addr = &r->con->dst_addr;
const int sa_family = 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_config(r, p, &pconf);
/* check that mod_maxmind is activated and env fields were requested */
if (!pconf.activate || NULL == pconf.env) return HANDLER_GO_ON;
array *env = r->plugin_ctx[p->id];
if (NULL == env) {
env = r->plugin_ctx[p->id] = array_init(pconf.env->used);
if (pconf.mmdb)
mod_maxmind_geoip2(env, (const struct sockaddr *)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(r,
CONST_BUF_LEN(&ds->key), CONST_BUF_LEN(&ds->value));
}
return HANDLER_GO_ON;
}
CONNECTION_FUNC(mod_maxminddb_handle_con_close)
{
request_st * const r = &con->request;
plugin_data *p = p_d;
array *env = r->plugin_ctx[p->id];
if (NULL != env) {
array_free(env);
r->plugin_ctx[p->id] = NULL;
}
return HANDLER_GO_ON;
}