xref: /lighttpd1.4/src/mod_maxminddb.c (revision 463c60ce)
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);
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 INIT_FUNC(mod_maxminddb_init)
112 {
113     return calloc(1, sizeof(plugin_data));
114 }
115 
116 
117 FREE_FUNC(mod_maxminddb_free)
118 {
119     plugin_data * const p = p_d;
120     if (NULL == p->cvlist) return;
121     /* (init i to 0 if global context; to 1 to skip empty global context) */
122     for (int i = !p->cvlist[0].v.u2[1], used = p->nconfig; i < used; ++i) {
123         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
124         for (; -1 != cpv->k_id; ++cpv) {
125             switch (cpv->k_id) {
126               case 1: /* maxminddb.db */
127                 if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) {
128                     struct MMDB_s *mmdb;
129                     *(struct MMDB_s **)&mmdb = cpv->v.v;
130                     MMDB_close(mmdb);
131                     free(mmdb);
132                 }
133                 break;
134               case 2: /* maxminddb.env */
135                 if (cpv->vtype == T_CONFIG_LOCAL && NULL != cpv->v.v) {
136                     plugin_config_env * const pcenv = cpv->v.v;
137                     const array * const env = pcenv->env;
138                     char ***cenv;
139                     *(const char ****)&cenv = pcenv->cenv;
140                     for (uint32_t k = 0, cused = env->used; k < cused; ++k)
141                         free(cenv[k]);
142                     free(cenv);
143                 }
144                 break;
145               default:
146                 break;
147             }
148         }
149     }
150 }
151 
152 
153 static MMDB_s *
154 mod_maxminddb_open_db (server *srv, const buffer *db_name)
155 {
156     if (db_name->used < sizeof(".mmdb")
157         || 0 != memcmp(db_name->ptr+db_name->used-sizeof(".mmdb"),
158                        CONST_STR_LEN(".mmdb"))) {
159         log_error(srv->errh, __FILE__, __LINE__,
160           "GeoIP database is of unsupported type %s)",
161           db_name->ptr);
162         return NULL;
163     }
164 
165     MMDB_s * const mmdb = (MMDB_s *)calloc(1, sizeof(MMDB_s));
166     int rc = MMDB_open(db_name->ptr, MMDB_MODE_MMAP, mmdb);
167     if (MMDB_SUCCESS == rc)
168         return mmdb;
169 
170     if (MMDB_IO_ERROR == rc)
171         log_perror(srv->errh, __FILE__, __LINE__,
172           "failed to open GeoIP2 database (%s)",
173           db_name->ptr);
174     else
175         log_error(srv->errh, __FILE__, __LINE__,
176           "failed to open GeoIP2 database (%s): %s",
177           db_name->ptr, MMDB_strerror(rc));
178     free(mmdb);
179     return NULL;
180 }
181 
182 
183 static plugin_config_env *
184 mod_maxminddb_prep_cenv (server *srv, const array * const env)
185 {
186     data_string ** const data = (data_string **)env->data;
187     char *** const cenv = calloc(env->used, sizeof(char **));
188     force_assert(cenv);
189     for (uint32_t j = 0, used = env->used; j < used; ++j) {
190         if (data[j]->type != TYPE_STRING) {
191             log_error(srv->errh, __FILE__, __LINE__,
192               "maxminddb.env must be a list of strings");
193             for (uint32_t k = 0; k < j; ++k) free(cenv[k]);
194             free(cenv);
195             return NULL;
196         }
197         buffer *value = &data[j]->value;
198         if (buffer_is_blank(value)
199             || '/' == value->ptr[0]
200             || '/' == value->ptr[buffer_clen(value)-1]) {
201             log_error(srv->errh, __FILE__, __LINE__,
202               "maxminddb.env must be a list of non-empty "
203               "strings and must not begin or end with '/'");
204             for (uint32_t k = 0; k < j; ++k) free(cenv[k]);
205             free(cenv);
206             return NULL;
207         }
208         /* XXX: should strings be lowercased? */
209         unsigned int k = 2;
210         for (char *t = value->ptr; (t = strchr(t, '/')); ++t) ++k;
211         const char **keys = (const char **)(cenv[j] = calloc(k,sizeof(char *)));
212         force_assert(keys);
213         k = 0;
214         keys[k] = value->ptr;
215         for (char *t = value->ptr; (t = strchr(t, '/')); ) {
216             *t = '\0';
217             keys[++k] = ++t;
218         }
219         keys[++k] = NULL;
220     }
221 
222     plugin_config_env * const pcenv = malloc(sizeof(plugin_config_env));
223     force_assert(pcenv);
224     pcenv->env = env;
225     pcenv->cenv = (const char ***)cenv;
226     return pcenv;
227 }
228 
229 
230 static void
231 mod_maxminddb_merge_config_cpv(plugin_config * const pconf,
232                                const config_plugin_value_t * const cpv)
233 {
234     switch (cpv->k_id) { /* index into static config_plugin_keys_t cpk[] */
235       case 0: /* maxminddb.activate */
236         pconf->activate = (int)cpv->v.u;
237         break;
238       case 1: /* maxminddb.db */
239         if (cpv->vtype != T_CONFIG_LOCAL) break;
240         pconf->mmdb = cpv->v.v;
241         break;
242       case 2: /* maxminddb.env */
243         if (cpv->vtype == T_CONFIG_LOCAL) {
244             plugin_config_env * const pcenv = cpv->v.v;
245             pconf->env = pcenv->env;
246             pconf->cenv = pcenv->cenv;
247         }
248         break;
249       default:/* should not happen */
250         return;
251     }
252 }
253 
254 
255 static void
256 mod_maxminddb_merge_config (plugin_config * const pconf,
257                             const config_plugin_value_t *cpv)
258 {
259     do {
260         mod_maxminddb_merge_config_cpv(pconf, cpv);
261     } while ((++cpv)->k_id != -1);
262 }
263 
264 
265 static void
266 mod_maxmind_patch_config (request_st * const r,
267                           const plugin_data * const p,
268                           plugin_config * const pconf)
269 {
270     *pconf = p->defaults; /* copy small struct instead of memcpy() */
271     /*memcpy(pconf, &p->defaults, sizeof(plugin_config));*/
272     for (int i = 1, used = p->nconfig; i < used; ++i) {
273         if (config_check_cond(r, (uint32_t)p->cvlist[i].k_id))
274             mod_maxminddb_merge_config(pconf, p->cvlist + p->cvlist[i].v.u2[0]);
275     }
276 }
277 
278 
279 SETDEFAULTS_FUNC(mod_maxminddb_set_defaults)
280 {
281     static const config_plugin_keys_t cpk[] = {
282       { CONST_STR_LEN("maxminddb.activate"),
283         T_CONFIG_BOOL,
284         T_CONFIG_SCOPE_CONNECTION }
285      ,{ CONST_STR_LEN("maxminddb.db"),
286         T_CONFIG_STRING,
287         T_CONFIG_SCOPE_CONNECTION }
288      ,{ CONST_STR_LEN("maxminddb.env"),
289         T_CONFIG_ARRAY_KVSTRING,
290         T_CONFIG_SCOPE_CONNECTION }
291      ,{ NULL, 0,
292         T_CONFIG_UNSET,
293         T_CONFIG_SCOPE_UNSET }
294     };
295 
296     plugin_data * const p = p_d;
297     if (!config_plugin_values_init(srv, p, cpk, "mod_maxminddb"))
298         return HANDLER_ERROR;
299 
300     /* process and validate config directives
301      * (init i to 0 if global context; to 1 to skip empty global context) */
302     for (int i = !p->cvlist[0].v.u2[1]; i < p->nconfig; ++i) {
303         config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0];
304         for (; -1 != cpv->k_id; ++cpv) {
305             switch (cpv->k_id) {
306               case 0: /* maxminddb.activate */
307                 break;
308               case 1: /* maxminddb.db */
309                 if (!buffer_is_blank(cpv->v.b)) {
310                     cpv->v.v = mod_maxminddb_open_db(srv, cpv->v.b);
311                     if (NULL == cpv->v.v) return HANDLER_ERROR;
312                     cpv->vtype = T_CONFIG_LOCAL;
313                 }
314                 break;
315               case 2: /* maxminddb.env */
316                 if (cpv->v.a->used) {
317                     cpv->v.v = mod_maxminddb_prep_cenv(srv, cpv->v.a);
318                     if (NULL == cpv->v.v) return HANDLER_ERROR;
319                     cpv->vtype = T_CONFIG_LOCAL;
320                 }
321                 break;
322               default:/* should not happen */
323                 break;
324             }
325         }
326     }
327 
328     /* initialize p->defaults from global config context */
329     if (p->nconfig > 0 && p->cvlist->v.u2[1]) {
330         const config_plugin_value_t *cpv = p->cvlist + p->cvlist->v.u2[0];
331         if (-1 != cpv->k_id)
332             mod_maxminddb_merge_config(&p->defaults, cpv);
333     }
334 
335     return HANDLER_GO_ON;
336 }
337 
338 
339 static void
340 geoip2_env_set (array * const env, const char * const k,
341                 const size_t klen, MMDB_entry_data_s * const data)
342 {
343     /* GeoIP2 database interfaces return pointers directly into database,
344      * and these are valid until the database is closed.
345      * However, note that the strings *are not* '\0'-terminated */
346     char buf[35];
347     if (!data->has_data || 0 == data->offset) return;
348     const char *v = buf;
349     size_t vlen;
350     switch (data->type) {
351       case MMDB_DATA_TYPE_UTF8_STRING:
352         v = data->utf8_string;
353         vlen = data->data_size;
354         break;
355       case MMDB_DATA_TYPE_BOOLEAN:
356         v = data->boolean ? "1" : "0";
357         vlen = 1;
358         break;
359       case MMDB_DATA_TYPE_BYTES:
360         v = (const char *)data->bytes;
361         vlen = data->data_size;
362         break;
363       case MMDB_DATA_TYPE_DOUBLE:
364         vlen = snprintf(buf, sizeof(buf), "%.5f", data->double_value);
365         break;
366       case MMDB_DATA_TYPE_FLOAT:
367         vlen = snprintf(buf, sizeof(buf), "%.5f", data->float_value);
368         break;
369       case MMDB_DATA_TYPE_INT32:
370         vlen = li_itostrn(buf, sizeof(buf), data->int32);
371         break;
372       case MMDB_DATA_TYPE_UINT32:
373         vlen = li_utostrn(buf, sizeof(buf), data->uint32);
374         break;
375       case MMDB_DATA_TYPE_UINT16:
376         vlen = li_utostrn(buf, sizeof(buf), data->uint16);
377         break;
378       case MMDB_DATA_TYPE_UINT64:
379         /* truncated value on 32-bit unless uintmax_t is 64-bit (long long) */
380         vlen = li_utostrn(buf, sizeof(buf), data->uint64);
381         break;
382       case MMDB_DATA_TYPE_UINT128:
383         buf[0] = '0';
384         buf[1] = 'x';
385        #if MMDB_UINT128_IS_BYTE_ARRAY
386         li_tohex_uc(buf+2, sizeof(buf)-2, (char *)data->uint128, 16);
387        #else
388         li_tohex_uc(buf+2, sizeof(buf)-2, (char *)&data->uint128, 16);
389        #endif
390         vlen = 34;
391         break;
392       default: /*(ignore unknown data type)*/
393         return;
394     }
395 
396     array_set_key_value(env, k, klen, v, vlen);
397 }
398 
399 
400 static void
401 mod_maxmind_geoip2 (array * const env, const struct sockaddr * const dst_addr,
402                     plugin_config * const pconf)
403 {
404     MMDB_lookup_result_s res;
405     MMDB_entry_data_s data;
406     int rc;
407 
408     res = MMDB_lookup_sockaddr(pconf->mmdb, dst_addr, &rc);
409     if (MMDB_SUCCESS != rc || !res.found_entry) return;
410     MMDB_entry_s * const entry = &res.entry;
411 
412     const data_string ** const names = (const data_string **)pconf->env->data;
413     const char *** const cenv = pconf->cenv;
414     for (size_t i = 0, used = pconf->env->used; i < used; ++i) {
415         if (MMDB_SUCCESS == MMDB_aget_value(entry, &data, cenv[i])
416             && data.has_data) {
417             geoip2_env_set(env, BUF_PTR_LEN(&names[i]->key), &data);
418         }
419     }
420 }
421 
422 
423 REQUEST_FUNC(mod_maxminddb_request_env_handler)
424 {
425     connection * const con = r->con;
426     const sock_addr * const dst_addr = &con->dst_addr;
427     const int sa_family = sock_addr_get_family(dst_addr);
428     if (sa_family != AF_INET && sa_family != AF_INET6) return HANDLER_GO_ON;
429 
430     plugin_config pconf;
431     plugin_data *p = p_d;
432     mod_maxmind_patch_config(r, p, &pconf);
433     /* check that mod_maxmind is activated and env fields were requested */
434     if (!pconf.activate || NULL == pconf.env) return HANDLER_GO_ON;
435 
436     array *env = con->plugin_ctx[p->id];
437     if (NULL == env) {
438         env = con->plugin_ctx[p->id] = array_init(pconf.env->used);
439         if (pconf.mmdb)
440             mod_maxmind_geoip2(env, (const struct sockaddr *)dst_addr, &pconf);
441     }
442 
443     for (uint32_t i = 0; i < env->used; ++i) {
444         /* note: replaces values which may have been set by mod_openssl
445          * (when mod_extforward is listed after mod_openssl in server.modules)*/
446         data_string *ds = (data_string *)env->data[i];
447         http_header_env_set(r, BUF_PTR_LEN(&ds->key), BUF_PTR_LEN(&ds->value));
448     }
449 
450     return HANDLER_GO_ON;
451 }
452 
453 
454 CONNECTION_FUNC(mod_maxminddb_handle_con_close)
455 {
456     plugin_data *p = p_d;
457     array *env = con->plugin_ctx[p->id];
458     if (NULL != env) {
459         array_free(env);
460         con->plugin_ctx[p->id] = NULL;
461     }
462 
463     return HANDLER_GO_ON;
464 }
465