xref: /lighttpd1.4/src/mod_maxminddb.c (revision 3a8fc4bc)
1 /*
2  * mod_maxminddb - MaxMind GeoIP2 support for lighttpd
3  *
4  * Copyright(c) 2019 Glenn Strauss gstrauss()gluelogic.com  All rights reserved
5  * License: BSD 3-clause (same as lighttpd)
6  */
7 /**
8  *
9  * Name:
10  *     mod_maxminddb.c
11  *
12  * Description:
13  *     MaxMind GeoIP2 module (plugin) for lighttpd.
14  *
15  *     GeoIP2 country db env's:
16  *         GEOIP_COUNTRY_CODE
17  *         GEOIP_COUNTRY_NAME
18  *
19  *     GeoIP2 city db env's:
20  *         GEOIP_COUNTRY_CODE
21  *         GEOIP_COUNTRY_NAME
22  *         GEOIP_CITY_NAME
23  *         GEOIP_CITY_LATITUDE
24  *         GEOIP_CITY_LONGITUDE
25  *
26  * Usage (configuration options):
27  *     maxminddb.db = <path to the geoip or geocity database>
28  *         GeoLite2 database filenames end in ".mmdb"
29  *     maxminddb.activate = <enable|disable> : default disabled
30  *     maxminddb.env = (
31  *         "GEOIP_COUNTRY_CODE"   => "country/iso_code",
32  *         "GEOIP_COUNTRY_NAME"   => "country/names/en",
33  *         "GEOIP_CITY_NAME"      => "city/names/en",
34  *         "GEOIP_CITY_LATITUDE"  => "location/latitude",
35  *         "GEOIP_CITY_LONGITUDE" => "location/longitude",
36  *     )
37  *
38  * Installation Instructions:
39  *     https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModGeoip
40  *
41  * References:
42  *   https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModGeoip
43  *   http://dev.maxmind.com/geoip/legacy/geolite/
44  *   http://dev.maxmind.com/geoip/geoip2/geolite2/
45  *   http://dev.maxmind.com/geoip/geoipupdate/
46  *
47  *   GeoLite2 database format
48  *   http://maxmind.github.io/MaxMind-DB/
49  *   https://github.com/maxmind/libmaxminddb
50  *
51  * Note: GeoLite2 databases are free IP geolocation databases comparable to,
52  *       but less accurate than, MaxMind’s GeoIP2 databases.
53  *       If you are a commercial entity, please consider a subscription to the
54  *       more accurate databases to support MaxMind.
55  *         http://dev.maxmind.com/geoip/geoip2/downloadable/
56  */
57 
58 #include "first.h"      /* first */
59 #include "sys-socket.h" /* AF_INET AF_INET6 */
60 #include <stdlib.h>
61 #include <string.h>
62 
63 #include "base.h"
64 #include "buffer.h"
65 #include "http_header.h"
66 #include "log.h"
67 #include "sock_addr.h"
68 
69 #include "plugin.h"
70 
71 #include <maxminddb.h>
72 
73 SETDEFAULTS_FUNC(mod_maxminddb_set_defaults);
74 INIT_FUNC(mod_maxminddb_init);
75 FREE_FUNC(mod_maxminddb_free);
76 REQUEST_FUNC(mod_maxminddb_request_env_handler);
77 CONNECTION_FUNC(mod_maxminddb_handle_con_close);
78 
79 __attribute_cold__
80 int mod_maxminddb_plugin_init(plugin *p);
mod_maxminddb_plugin_init(plugin * p)81 int mod_maxminddb_plugin_init(plugin *p) {
82     p->version                   = LIGHTTPD_VERSION_ID;
83     p->name                      = "maxminddb";
84 
85     p->set_defaults              = mod_maxminddb_set_defaults;
86     p->init                      = mod_maxminddb_init;
87     p->cleanup                   = mod_maxminddb_free;
88     p->handle_request_env        = mod_maxminddb_request_env_handler;
89     p->handle_connection_close   = mod_maxminddb_handle_con_close;
90 
91     return 0;
92 }
93 
94 typedef struct {
95     int activate;
96     const array *env;
97     const char ***cenv;
98     struct MMDB_s *mmdb;
99 } plugin_config;
100 
101 typedef struct {
102     PLUGIN_DATA;
103     plugin_config defaults;
104 } plugin_data;
105 
106 typedef struct {
107     const array *env;
108     const char ***cenv;
109 } plugin_config_env;
110 
111 typedef struct {
112   #ifdef HAVE_IPV6
113     struct sockaddr_in6 addr;
114   #else
115     struct sockaddr_in addr;
116   #endif
117     array *env;
118 } handler_ctx;
119 
INIT_FUNC(mod_maxminddb_init)120 INIT_FUNC(mod_maxminddb_init)
121 {
122     return ck_calloc(1, sizeof(plugin_data));
123 }
124 
125 
FREE_FUNC(mod_maxminddb_free)126 FREE_FUNC(mod_maxminddb_free)
127 {
128     plugin_data * const p = p_d;
129     if (NULL == p->cvlist) return;
130     /* (init i to 0 if global context; to 1 to skip empty global context) */
131     for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
132         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
133         for (; -1 != cpv->k_id; ++cpv) {
134             switch (cpv->k_id) {
135               case 1: /* maxminddb.db */
136                 if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) {
137                     struct MMDB_s *mmdb;
138                     *(struct MMDB_s **)&mmdb = cpv->v.v;
139                     MMDB_close(mmdb);
140                     free(mmdb);
141                 }
142                 break;
143               case 2: /* maxminddb.env */
144                 if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) {
145                     plugin_config_env * const pcenv = cpv->v.v;
146                     const array * const env = pcenv->env;
147                     char ***cenv;
148                     *(const char ****)&cenv = pcenv->cenv;
149                     for (uint32_t k = 0, cused = env->used; k < cused; ++k)
150                         free(cenv[k]);
151                     free(cenv);
152                 }
153                 break;
154               default:
155                 break;
156             }
157         }
158     }
159 }
160 
161 
162 static MMDB_s *
mod_maxminddb_open_db(server * srv,const buffer * db_name)163 mod_maxminddb_open_db (server *srv, const buffer *db_name)
164 {
165     if (db_name->used < sizeof(".mmdb")
166         || 0 != memcmp(db_name->ptr+db_name->used-sizeof(".mmdb"),
167                        CONST_STR_LEN(".mmdb"))) {
168         log_error(srv->errh, __FILE__, __LINE__,
169           "GeoIP database is of unsupported type %s)",
170           db_name->ptr);
171         return NULL;
172     }
173 
174     MMDB_s * const mmdb = (MMDB_s *)ck_calloc(1, sizeof(MMDB_s));
175     int rc = MMDB_open(db_name->ptr, MMDB_MODE_MMAP, mmdb);
176     if (MMDB_SUCCESS == rc)
177         return mmdb;
178 
179     if (MMDB_IO_ERROR == rc)
180         log_perror(srv->errh, __FILE__, __LINE__,
181           "failed to open GeoIP2 database (%s)",
182           db_name->ptr);
183     else
184         log_error(srv->errh, __FILE__, __LINE__,
185           "failed to open GeoIP2 database (%s): %s",
186           db_name->ptr, MMDB_strerror(rc));
187     free(mmdb);
188     return NULL;
189 }
190 
191 
192 static plugin_config_env *
mod_maxminddb_prep_cenv(server * srv,const array * const env)193 mod_maxminddb_prep_cenv (server *srv, const array * const env)
194 {
195     data_string ** const data = (data_string **)env->data;
196     char *** const cenv = ck_calloc(env->used, sizeof(char **));
197     for (uint32_t j = 0, used = env->used; j < used; ++j) {
198         if (data[j]->type != TYPE_STRING) {
199             log_error(srv->errh, __FILE__, __LINE__,
200               "maxminddb.env must be a list of strings");
201             for (uint32_t k = 0; k < j; ++k) free(cenv[k]);
202             free(cenv);
203             return NULL;
204         }
205         buffer *value = &data[j]->value;
206         if (buffer_is_blank(value)
207             || '/' == value->ptr[0]
208             || '/' == value->ptr[buffer_clen(value)-1]) {
209             log_error(srv->errh, __FILE__, __LINE__,
210               "maxminddb.env must be a list of non-empty "
211               "strings and must not begin or end with '/'");
212             for (uint32_t k = 0; k < j; ++k) free(cenv[k]);
213             free(cenv);
214             return NULL;
215         }
216         /* XXX: should strings be lowercased? */
217         unsigned int k = 2;
218         for (char *t = value->ptr; (t = strchr(t, '/')); ++t) ++k;
219         const char **keys =
220           (const char **)(cenv[j] = ck_calloc(k, sizeof(char *)));
221         k = 0;
222         keys[k] = value->ptr;
223         for (char *t = value->ptr; (t = strchr(t, '/')); ) {
224             *t = '\0';
225             keys[++k] = ++t;
226         }
227         keys[++k] = NULL;
228     }
229 
230     plugin_config_env * const pcenv = ck_malloc(sizeof(plugin_config_env));
231     pcenv->env = env;
232     pcenv->cenv = (const char ***)cenv;
233     return pcenv;
234 }
235 
236 
237 static void
mod_maxminddb_merge_config_cpv(plugin_config * const pconf,const config_plugin_value_t * const cpv)238 mod_maxminddb_merge_config_cpv(plugin_config * const pconf,
239                                const config_plugin_value_t * const cpv)
240 {
241     switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
242       case 0: /* maxminddb.activate */
243         pconf->activate = (int)cpv->v.u;
244         break;
245       case 1: /* maxminddb.db */
246         if (cpv->vtype != T_CONFIG_LOCAL) break;
247         pconf->mmdb = cpv->v.v;
248         break;
249       case 2: /* maxminddb.env */
250         if (cpv->vtype == T_CONFIG_LOCAL) {
251             plugin_config_env * const pcenv = cpv->v.v;
252             pconf->env = pcenv->env;
253             pconf->cenv = pcenv->cenv;
254         }
255         break;
256       default:/* should not happen */
257         return;
258     }
259 }
260 
261 
262 static void
mod_maxminddb_merge_config(plugin_config * const pconf,const config_plugin_value_t * cpv)263 mod_maxminddb_merge_config (plugin_config * const pconf,
264                             const config_plugin_value_t *cpv)
265 {
266     do {
267         mod_maxminddb_merge_config_cpv(pconf, cpv);
268     } while ((++cpv)->k_id != -1);
269 }
270 
271 
272 static void
mod_maxminddb_patch_config(request_st * const r,const plugin_data * const p,plugin_config * const pconf)273 mod_maxminddb_patch_config (request_st * const r,
274                             const plugin_data * const p,
275                             plugin_config * const pconf)
276 {
277     *pconf = p->defaults; /* copy small struct instead of memcpy() */
278     /*memcpy(pconf, &p->defaults, sizeof(plugin_config));*/
279     for (int i = 1, used = p->nconfig; i < used; ++i) {
280         if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
281             mod_maxminddb_merge_config(pconf, p->cvlist + p->cvlist[i].v.u2[0]);
282     }
283 }
284 
285 
SETDEFAULTS_FUNC(mod_maxminddb_set_defaults)286 SETDEFAULTS_FUNC(mod_maxminddb_set_defaults)
287 {
288     static const config_plugin_keys_t cpk[] = {
289       { CONST_STR_LEN("maxminddb.activate"),
290         T_CONFIG_BOOL,
291         T_CONFIG_SCOPE_CONNECTION }
292      ,{ CONST_STR_LEN("maxminddb.db"),
293         T_CONFIG_STRING,
294         T_CONFIG_SCOPE_CONNECTION }
295      ,{ CONST_STR_LEN("maxminddb.env"),
296         T_CONFIG_ARRAY_KVSTRING,
297         T_CONFIG_SCOPE_CONNECTION }
298      ,{ NULL, 0,
299         T_CONFIG_UNSET,
300         T_CONFIG_SCOPE_UNSET }
301     };
302 
303     plugin_data * const p = p_d;
304     if (!config_plugin_values_init(srv, p, cpk, "mod_maxminddb"))
305         return HANDLER_ERROR;
306 
307     /* process and validate config directives
308      * (init i to 0 if global context; to 1 to skip empty global context) */
309     for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
310         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
311         for (; -1 != cpv->k_id; ++cpv) {
312             switch (cpv->k_id) {
313               case 0: /* maxminddb.activate */
314                 break;
315               case 1: /* maxminddb.db */
316                 if (!buffer_is_blank(cpv->v.b)) {
317                     cpv->v.v = mod_maxminddb_open_db(srv, cpv->v.b);
318                     if (NULL == cpv->v.v) return HANDLER_ERROR;
319                     cpv->vtype = T_CONFIG_LOCAL;
320                 }
321                 break;
322               case 2: /* maxminddb.env */
323                 if (cpv->v.a->used) {
324                     cpv->v.v = mod_maxminddb_prep_cenv(srv, cpv->v.a);
325                     if (NULL == cpv->v.v) return HANDLER_ERROR;
326                     cpv->vtype = T_CONFIG_LOCAL;
327                 }
328                 break;
329               default:/* should not happen */
330                 break;
331             }
332         }
333     }
334 
335     /* initialize p->defaults from global config context */
336     if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
337         const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
338         if (-1 != cpv->k_id)
339             mod_maxminddb_merge_config(&p->defaults, cpv);
340     }
341 
342     return HANDLER_GO_ON;
343 }
344 
345 
346 static void
geoip2_env_set(request_st * const r,array * const env,const buffer * const kb,MMDB_entry_data_s * const data)347 geoip2_env_set (request_st * const r, array * const env,
348                 const buffer * const kb, MMDB_entry_data_s * const data)
349 {
350     /* GeoIP2 database interfaces return pointers directly into database,
351      * and these are valid until the database is closed.
352      * However, note that the strings *are not* '\0'-terminated */
353     char buf[35];
354     if (!data->has_data || 0 == data->offset) return;
355     const char *v = buf;
356     size_t vlen;
357     switch (data->type) {
358       case MMDB_DATA_TYPE_UTF8_STRING:
359         v = data->utf8_string;
360         vlen = data->data_size;
361         break;
362       case MMDB_DATA_TYPE_BOOLEAN:
363         v = data->boolean ? "1" : "0";
364         vlen = 1;
365         break;
366       case MMDB_DATA_TYPE_BYTES:
367         v = (const char *)data->bytes;
368         vlen = data->data_size;
369         break;
370       case MMDB_DATA_TYPE_DOUBLE:
371         vlen = snprintf(buf, sizeof(buf), "%.5f", data->double_value);
372         break;
373       case MMDB_DATA_TYPE_FLOAT:
374         vlen = snprintf(buf, sizeof(buf), "%.5f", data->float_value);
375         break;
376       case MMDB_DATA_TYPE_INT32:
377         vlen = li_itostrn(buf, sizeof(buf), data->int32);
378         break;
379       case MMDB_DATA_TYPE_UINT32:
380         vlen = li_utostrn(buf, sizeof(buf), data->uint32);
381         break;
382       case MMDB_DATA_TYPE_UINT16:
383         vlen = li_utostrn(buf, sizeof(buf), data->uint16);
384         break;
385       case MMDB_DATA_TYPE_UINT64:
386         /* truncated value on 32-bit unless uintmax_t is 64-bit (long long) */
387         vlen = li_utostrn(buf, sizeof(buf), data->uint64);
388         break;
389       case MMDB_DATA_TYPE_UINT128:
390         buf[0] = '0';
391         buf[1] = 'x';
392        #if MMDB_UINT128_IS_BYTE_ARRAY
393         li_tohex_uc(buf+2, sizeof(buf)-2, (char *)data->uint128, 16);
394        #else
395         li_tohex_uc(buf+2, sizeof(buf)-2, (char *)&data->uint128, 16);
396        #endif
397         vlen = 34;
398         break;
399       default: /*(ignore unknown data type)*/
400         return;
401     }
402 
403     http_header_env_set(r, BUF_PTR_LEN(kb), v, vlen);
404     if (env)
405         array_set_key_value(env, BUF_PTR_LEN(kb), v, vlen);
406 }
407 
408 
409 static void
mod_maxminddb_geoip2(request_st * const r,array * const env,const struct sockaddr * const dst_addr,plugin_config * const pconf)410 mod_maxminddb_geoip2 (request_st * const r, array * const env,
411                       const struct sockaddr * const dst_addr,
412                       plugin_config * const pconf)
413 {
414     MMDB_lookup_result_s res;
415     MMDB_entry_data_s data;
416     int rc;
417 
418     res = MMDB_lookup_sockaddr(pconf->mmdb, dst_addr, &rc);
419     if (MMDB_SUCCESS != rc || !res.found_entry) return;
420     MMDB_entry_s * const entry = &res.entry;
421 
422     const data_string ** const names = (const data_string **)pconf->env->data;
423     const char *** const cenv = pconf->cenv;
424     for (size_t i = 0, used = pconf->env->used; i < used; ++i) {
425         if (MMDB_SUCCESS == MMDB_aget_value(entry, &data, cenv[i])
426             && data.has_data) {
427             geoip2_env_set(r, env, &names[i]->key, &data);
428         }
429     }
430 }
431 
432 
REQUEST_FUNC(mod_maxminddb_request_env_handler)433 REQUEST_FUNC(mod_maxminddb_request_env_handler)
434 {
435     plugin_config pconf;
436     plugin_data *p = p_d;
437     mod_maxminddb_patch_config(r, p, &pconf);
438     /* check mod_maxminddb activated, env fields requested, and db is open */
439     if (!pconf.activate || NULL == pconf.env || NULL == pconf.mmdb)
440         return HANDLER_GO_ON;
441 
442     const sock_addr * const dst_addr = r->dst_addr;
443 
444   #if 0
445     /* future: if mod_extforward is (future) extended for HTTP/2 and
446      * load balancers configured to reuse an HTTP/2 connection for more
447      * than one client, then might add a configuration option to bypass
448      * allocating and caching the db lookup results in env, even for the
449      * initial request env */
450     if (!pconf.cache) { /*(not implemented)*/
451         const int sa_family = sock_addr_get_family(dst_addr);
452         if (sa_family == AF_INET && sa_family == AF_INET6)
453             mod_maxminddb_geoip2(r, NULL,
454                                  (const struct sockaddr *)dst_addr, &pconf);
455         return HANDLER_GO_ON;
456     }
457   #endif
458 
459     handler_ctx ** const hctx = (handler_ctx **)&r->con->plugin_ctx[p->id];
460 
461     if (*hctx && sock_addr_is_addr_eq((sock_addr *)&(*hctx)->addr, dst_addr)) {
462         const array * const env = (*hctx)->env;
463         for (uint32_t i = 0; i < env->used; ++i) {
464             /* note: replaces values which may have been set by mod_openssl
465              *(when mod_extforward listed after mod_openssl in server.modules)*/
466             const data_string * const ds = (data_string *)env->data[i];
467             http_header_env_set(r, BUF_PTR_LEN(&ds->key),
468                                    BUF_PTR_LEN(&ds->value));
469         }
470         return HANDLER_GO_ON;
471     }
472 
473     array *env = NULL;
474     if (*hctx && r->http_version <= HTTP_VERSION_1_1) {
475         env = (*hctx)->env;
476         /*array_reset_data_strings(env);*/ /* reuse string allocations */
477         env->used = 0;
478     }
479     else if ((*hctx) == NULL) {
480         (*hctx) = ck_malloc(sizeof(handler_ctx));
481         (*hctx)->env = env = array_init(pconf.env->used);
482     }
483 
484     if (env) { /* then (env == (*hctx)->env) else (env == NULL) */
485         const int sa_family = sock_addr_get_family(dst_addr);
486         if (sa_family != AF_INET && sa_family != AF_INET6)
487             return HANDLER_GO_ON;
488         if (sa_family == AF_INET)
489             memcpy(&(*hctx)->addr, dst_addr, sizeof(dst_addr->ipv4));
490       #ifdef HAVE_IPV6
491         else
492             memcpy(&(*hctx)->addr, dst_addr, sizeof(dst_addr->ipv6));
493       #endif
494     }
495 
496     mod_maxminddb_geoip2(r, env, (const struct sockaddr *)dst_addr, &pconf);
497 
498     return HANDLER_GO_ON;
499 }
500 
501 
CONNECTION_FUNC(mod_maxminddb_handle_con_close)502 CONNECTION_FUNC(mod_maxminddb_handle_con_close)
503 {
504     handler_ctx **hctx =
505       (handler_ctx **)&con->plugin_ctx[((plugin_data *)p_d)->id];
506     if (NULL != *hctx) {
507         array_free((*hctx)->env);
508         free(*hctx);
509         *hctx = NULL;
510     }
511 
512     return HANDLER_GO_ON;
513 }
514