diff --git a/admin/listclients.xsl b/admin/listclients.xsl index 9bdd0d68..2d0af605 100644 --- a/admin/listclients.xsl +++ b/admin/listclients.xsl @@ -25,6 +25,7 @@ Role Sec. connected User Agent + Location Action @@ -36,6 +37,10 @@ + + +  On OSM + Kick Move diff --git a/configure.ac b/configure.ac index cca2f51e..553bb0b0 100644 --- a/configure.ac +++ b/configure.ac @@ -243,6 +243,14 @@ PKG_HAVE_WITH_MODULES([OPENSSL], [openssl >= 1.1.0], [ LIBS="${LIBS} ${OPENSSL_LIBS}" ]) +dnl +dnl libmaxminddb +dnl +PKG_HAVE_WITH_MODULES([MAXMINDDB], [libmaxminddb >= 1.3.2], [ + CFLAGS="${CFLAGS} ${MAXMINDDB_CFLAGS}" + LIBS="${LIBS} ${MAXMINDDB_LIBS}" +]) + dnl dnl librhash - first try pkgconfig and then basic search dnl since the function is defined in rhash.h we need to check for that first, diff --git a/src/Makefile.am b/src/Makefile.am index 82f4f40b..d7548649 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -34,6 +34,7 @@ noinst_HEADERS = \ prng.h \ matchfile.h \ tls.h \ + geoip.h \ refobject.h \ module.h \ reportxml.h \ @@ -88,6 +89,7 @@ icecast_SOURCES = \ prng.c \ matchfile.c \ tls.c \ + geoip.c \ refobject.c \ module.c \ reportxml.c \ diff --git a/src/admin.c b/src/admin.c index 07a973c8..825d3bd5 100644 --- a/src/admin.c +++ b/src/admin.c @@ -150,6 +150,12 @@ #define DEFAULT_HTML_REQUEST "" #define BUILDM3U_RAW_REQUEST "buildm3u" +typedef struct { + size_t listeners; + size_t tls; + size_t ipv6; +} country_t; + typedef struct { const char *prefix; size_t length; @@ -577,6 +583,8 @@ xmlDocPtr admin_build_sourcelist(const char *mount, client_t *client, admin_form snprintf(buf, sizeof(buf), "%"PRIu64, source->dumpfile_written); xmlNewTextChild(srcnode, NULL, XMLSTR("dumpfile_written"), XMLSTR(buf)); + + admin_add_geoip_to_mount(source, srcnode, client->mode); } node = avl_get_next(node); } @@ -922,6 +930,30 @@ static inline xmlNodePtr __add_listener(client_t *client, xmlNewTextChild(node, NULL, XMLSTR("protocol"), XMLSTR(client_protocol_to_string(client->protocol))); + if (client->con) { + connection_t *con = client->con; + + if (*con->geoip.iso_3166_1_alpha_2 || con->geoip.have_latitude || con->geoip.have_longitude) { + xmlNodePtr geoip = xmlNewChild(node, NULL, XMLSTR("geoip"), NULL); + + if (*con->geoip.iso_3166_1_alpha_2) { + xmlNodePtr country = xmlNewChild(geoip, NULL, XMLSTR("country"), NULL); + xmlSetProp(country, XMLSTR("iso-alpha-2"), XMLSTR(con->geoip.iso_3166_1_alpha_2)); + } + if (con->geoip.have_latitude || con->geoip.have_longitude) { + xmlNodePtr location = xmlNewChild(geoip, NULL, XMLSTR("location"), NULL); + if (con->geoip.have_latitude) { + snprintf(buf, sizeof(buf), "%f", con->geoip.latitude); + xmlSetProp(location, XMLSTR("latitude"), XMLSTR(buf)); + } + if (con->geoip.have_longitude) { + snprintf(buf, sizeof(buf), "%f", con->geoip.longitude); + xmlSetProp(location, XMLSTR("longitude"), XMLSTR(buf)); + } + } + } + } + do { xmlNodePtr history = xmlNewChild(node, NULL, XMLSTR("history"), NULL); size_t i; @@ -950,6 +982,80 @@ void admin_add_listeners_to_mount(source_t *source, avl_tree_unlock(source->client_tree); } +static void admin_add_geoip_to_mount__country(source_t *source, + xmlNodePtr parent, + operation_mode mode, + const char *code, + country_t *country) +{ + if (country->listeners) { + xmlNodePtr node = xmlNewChild(parent, NULL, XMLSTR("country"), NULL); + char buf[22]; + + if (code) + xmlSetProp(node, XMLSTR("iso-alpha-2"), XMLSTR(code)); + + snprintf(buf, sizeof(buf), "%llu", (long long unsigned)country->listeners); + xmlNewTextChild(node, NULL, XMLSTR("listeners"), XMLSTR(buf)); + + snprintf(buf, sizeof(buf), "%llu", (long long unsigned)country->tls); + xmlNewTextChild(node, NULL, XMLSTR("tls"), XMLSTR(buf)); + + snprintf(buf, sizeof(buf), "%llu", (long long unsigned)country->ipv6); + xmlNewTextChild(node, NULL, XMLSTR("ipv6"), XMLSTR(buf)); + } +} + +void admin_add_geoip_to_mount(source_t *source, + xmlNodePtr parent, + operation_mode mode) +{ + avl_node *client_node; + xmlNodePtr geoip = xmlNewChild(parent, NULL, XMLSTR("geoip"), NULL); + country_t countries[26][26]; + country_t default_country; + + memset(countries, 0, sizeof(countries)); + memset(&default_country, 0, sizeof(default_country)); + + avl_tree_rlock(source->client_tree); + client_node = avl_get_first(source->client_tree); + while(client_node) { + client_t *client = client_node->key; + connection_t *con = client->con; + country_t *country = &default_country; + + if (con && *con->geoip.iso_3166_1_alpha_2) { + const char *iso = client->con->geoip.iso_3166_1_alpha_2; + + if ((iso[0] >= 'a' && iso[0] <= 'z') && (iso[1] >= 'a' && iso[1] <= 'z')) { + country = &(countries[iso[0] - 'a'][iso[1] - 'a']); + } + } + country->listeners++; + + if (con) { + if (con->tls) + country->tls++; + + if (con->ip && strchr(con->ip, ':')) + country->ipv6++; + } + + client_node = avl_get_next(client_node); + } + avl_tree_unlock(source->client_tree); + + for (size_t idx_a = 0; idx_a < 26; idx_a++) { + for (size_t idx_b = 0; idx_b < 26; idx_b++) { + const char code[3] = {idx_a + 'a', idx_b + 'a', 0}; + admin_add_geoip_to_mount__country(source, geoip, mode, code, &(countries[idx_a][idx_b])); + } + } + + admin_add_geoip_to_mount__country(source, geoip, mode, NULL, &default_country); +} + static void command_show_listeners(client_t *client, source_t *source, admin_format_t response) @@ -970,6 +1076,7 @@ static void command_show_listeners(client_t *client, xmlNewTextChild(srcnode, NULL, XMLSTR(client->mode == OMODE_LEGACY ? "Listeners" : "listeners"), XMLSTR(buf)); admin_add_listeners_to_mount(source, srcnode, client->mode); + admin_add_geoip_to_mount(source, srcnode, client->mode); admin_send_response(doc, client, response, LISTCLIENTS_HTML_REQUEST); diff --git a/src/admin.h b/src/admin.h index 698b00eb..37a7a9d0 100644 --- a/src/admin.h +++ b/src/admin.h @@ -61,6 +61,11 @@ void admin_add_listeners_to_mount(source_t *source, xmlNodePtr parent, operation_mode mode); +void admin_add_geoip_to_mount(source_t *source, + xmlNodePtr parent, + operation_mode mode); + + xmlNodePtr admin_add_role_to_authentication(auth_t *auth, xmlNodePtr parent); admin_command_id_t admin_get_command(const char *command); diff --git a/src/cfgfile.c b/src/cfgfile.c index 42880d17..62bde152 100644 --- a/src/cfgfile.c +++ b/src/cfgfile.c @@ -55,6 +55,7 @@ #include "slave.h" #include "xslt.h" #include "prng.h" +#include "geoip.h" #define CATMODULE "CONFIG" #define RANGE_PORT 1, 65535 @@ -914,6 +915,7 @@ void config_clear(ice_config_t *c) if (c->log_dir) xmlFree(c->log_dir); if (c->webroot_dir) xmlFree(c->webroot_dir); if (c->adminroot_dir) xmlFree(c->adminroot_dir); + if (c->geoipdbfile) xmlFree(c->geoipdbfile); if (c->null_device) xmlFree(c->null_device); if (c->pidfile) xmlFree(c->pidfile); if (c->banfile) xmlFree(c->banfile); @@ -1004,6 +1006,8 @@ void config_reread_config(void) restart_logging(config); prng_configure(config); main_config_reload(config); + igloo_ro_unref(&global.geoip_db); + global.geoip_db = geoip_db_new(config->geoipdbfile); connection_reread_config(config); yp_recheck_config(config); fserve_recheck_mime_types(config); @@ -2719,6 +2723,14 @@ static void _parse_paths(xmlDocPtr doc, } } xmlFree(temp); + } else if (xmlStrcmp(node->name, XMLSTR("geoipdb")) == 0) { + if (!(temp = (char *)xmlNodeListGetString(doc, node->xmlChildrenNode, 1))) { + ICECAST_LOG_WARN(" setting must not be empty."); + continue; + } + if (configuration->geoipdbfile) + xmlFree(configuration->geoipdbfile); + configuration->geoipdbfile = (char *)temp; } else if (xmlStrcmp(node->name, XMLSTR("resource")) == 0 || xmlStrcmp(node->name, XMLSTR("alias")) == 0) { _parse_resource(doc, node, configuration); } else { diff --git a/src/cfgfile.h b/src/cfgfile.h index 8a151f78..257dd1eb 100644 --- a/src/cfgfile.h +++ b/src/cfgfile.h @@ -288,6 +288,7 @@ struct ice_config_tag { char *allowfile; char *webroot_dir; char *adminroot_dir; + char *geoipdbfile; prng_seed_config_t *prng_seed; resource_t *resources; reportxml_database_t *reportxml_db; diff --git a/src/client.c b/src/client.c index fcaf8c03..60e93d3b 100644 --- a/src/client.c +++ b/src/client.c @@ -57,6 +57,7 @@ #include "acl.h" #include "listensocket.h" #include "fastevent.h" +#include "geoip.h" /* for ADMIN_COMMAND_ERROR, and ADMIN_ICESTATS_LEGACY_EXTENSION_APPLICATION */ #include "admin.h" @@ -189,6 +190,8 @@ int client_create(client_t **c_ptr, connection_t *con, http_parser_t *parser) fastevent_emit(FASTEVENT_TYPE_CLIENT_CREATE, FASTEVENT_FLAG_MODIFICATION_ALLOWED, FASTEVENT_DATATYPE_CLIENT, client); + geoip_lookup_client(global.geoip_db, client); + return ret; } diff --git a/src/connection.h b/src/connection.h index 3261d1ee..254160cf 100644 --- a/src/connection.h +++ b/src/connection.h @@ -16,6 +16,7 @@ #include #include +#include #include "tls.h" @@ -64,6 +65,14 @@ struct connection_tag { /* IP Address of the client as seen by the server */ char *ip; + + struct { + double latitude; + double longitude; + bool have_latitude; + bool have_longitude; + char iso_3166_1_alpha_2[3]; /* 2 bytes plus \0 */ + } geoip; }; void connection_initialize(void); diff --git a/src/geoip.c b/src/geoip.c new file mode 100644 index 00000000..6d197d5f --- /dev/null +++ b/src/geoip.c @@ -0,0 +1,128 @@ +/* Icecast + * + * This program is distributed under the GNU General Public License, version 2. + * A copy of this license is included with this source. + * + * Copyright 2023 , Philipp Schafft , + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "icecasttypes.h" + +#ifdef HAVE_MAXMINDDB +#include +#include +#include +#endif + +#include +#include + +#include "geoip.h" +#include "global.h" +#include "client.h" +#include "connection.h" +#include "util_string.h" +#include "logging.h" +#define CATMODULE "geoip" + +struct geoip_db_tag { + igloo_ro_full_t __parent; + +#ifdef HAVE_MAXMINDDB + MMDB_s mmdb; +#endif +}; + +static void geoip_db_free(igloo_ro_t self) +{ +#ifdef HAVE_MAXMINDDB + geoip_db_t *db = igloo_ro_to_type(self, geoip_db_t); + + MMDB_close(&(db->mmdb)); +#endif +} + +igloo_RO_PUBLIC_TYPE(geoip_db_t, igloo_ro_full_t, + igloo_RO_TYPEDECL_FREE(geoip_db_free) + ); + +#ifdef HAVE_MAXMINDDB +geoip_db_t * geoip_db_new(const char *filename) +{ + geoip_db_t *ret; + MMDB_s mmdb; + int status; + + status = MMDB_open(filename, MMDB_MODE_MMAP, &mmdb); + if (status != MMDB_SUCCESS) { + if (status == MMDB_IO_ERROR) { + ICECAST_LOG_ERROR("Cannot open geoip database: %s: %s", MMDB_strerror(status), strerror(errno)); + } else { + ICECAST_LOG_ERROR("Cannot open geoip database: %s", MMDB_strerror(status)); + } + return NULL; + } + + if (igloo_ro_new_raw(&ret, geoip_db_t, igloo_instance) != igloo_ERROR_NONE) + return NULL; + + ret->mmdb = mmdb; + + ICECAST_LOG_INFO("Loaded geoip database: %s", filename); + + return ret; +} + +void geoip_lookup_client(geoip_db_t *self, client_t * client) +{ + int gai_error, mmdb_error; + MMDB_lookup_result_s result; + connection_t *con; + + if (!self || !client) + return; + + if (!client->con && !client->con->ip) + return; + + con = client->con; + + result = MMDB_lookup_string(&(self->mmdb), client->con->ip, &gai_error, &mmdb_error); + + if (gai_error || mmdb_error != MMDB_SUCCESS) + return; + + if (result.found_entry) { + MMDB_entry_data_s entry_data; + int status; + + status = MMDB_get_value(&result.entry, &entry_data, "country", "iso_code", (const char*)NULL); + if (status == MMDB_SUCCESS && entry_data.has_data) { + if (entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) { + if (entry_data.data_size < sizeof(con->geoip.iso_3166_1_alpha_2)) { + memcpy(con->geoip.iso_3166_1_alpha_2, entry_data.utf8_string, entry_data.data_size); + con->geoip.iso_3166_1_alpha_2[entry_data.data_size] = 0; + util_strtolower(con->geoip.iso_3166_1_alpha_2); + ICECAST_LOG_DINFO("FOUND: <%zu> <%H>", (size_t)entry_data.data_size, con->geoip.iso_3166_1_alpha_2); + } + } + } + + status = MMDB_get_value(&result.entry, &entry_data, "location", "latitude", (const char*)NULL); + if (status == MMDB_SUCCESS && entry_data.has_data && entry_data.type == MMDB_DATA_TYPE_DOUBLE) { + con->geoip.latitude = entry_data.double_value; + con->geoip.have_latitude = true; + } + + status = MMDB_get_value(&result.entry, &entry_data, "location", "longitude", (const char*)NULL); + if (status == MMDB_SUCCESS && entry_data.has_data && entry_data.type == MMDB_DATA_TYPE_DOUBLE) { + con->geoip.longitude = entry_data.double_value; + con->geoip.have_longitude = true; + } + } +} +#endif diff --git a/src/geoip.h b/src/geoip.h new file mode 100644 index 00000000..083935e1 --- /dev/null +++ b/src/geoip.h @@ -0,0 +1,25 @@ +/* Icecast + * + * This program is distributed under the GNU General Public License, version 2. + * A copy of this license is included with this source. + * + * Copyright 2023-2023, Philipp Schafft , + */ + +#ifndef __GEOIP_H__ +#define __GEOIP_H__ + +#include "icecasttypes.h" +#include "client.h" + +igloo_RO_FORWARD_TYPE(geoip_db_t); + +#ifdef HAVE_MAXMINDDB +geoip_db_t * geoip_db_new(const char *filename); +void geoip_lookup_client(geoip_db_t *self, client_t * client); +#else +#define geoip_db_new(filename) NULL +#define geoip_lookup_client(self,client) +#endif + +#endif /* __GEOIP_H__ */ diff --git a/src/global.c b/src/global.c index de8be3f7..e07f8414 100644 --- a/src/global.c +++ b/src/global.c @@ -51,6 +51,7 @@ void global_shutdown(void) { thread_mutex_destroy(&_global_mutex); igloo_ro_unref(&global.modulecontainer); + igloo_ro_unref(&global.geoip_db); avl_tree_free(global.source_tree, NULL); igloo_sp_unref(&_instance_uuid, igloo_instance); } diff --git a/src/global.h b/src/global.h index ffb5336b..6d9d60a9 100644 --- a/src/global.h +++ b/src/global.h @@ -53,6 +53,7 @@ typedef struct ice_global_tag relay_t *master_relays; module_container_t *modulecontainer; + geoip_db_t *geoip_db; /* state */ diff --git a/src/icecasttypes.h b/src/icecasttypes.h index 87bd5400..dc30b8d8 100644 --- a/src/icecasttypes.h +++ b/src/icecasttypes.h @@ -134,6 +134,10 @@ typedef struct mount_identifier_tag mount_identifier_t; typedef struct string_renderer_tag string_renderer_t; +/* ---[ geoip.[ch] ]--- */ + +typedef struct geoip_db_tag geoip_db_t; + /* ---[ event.[ch] ]--- */ typedef struct event_tag event_t; @@ -160,6 +164,7 @@ typedef void * refobject_t; /* --- [ For libigloo ]--- */ #define igloo_RO_APPTYPES \ igloo_RO_TYPE(string_renderer_t) \ + igloo_RO_TYPE(geoip_db_t) \ igloo_RO_TYPE(event_t) \ igloo_RO_TYPE(event_registration_t) \ igloo_RO_TYPE(module_t) \ diff --git a/src/main.c b/src/main.c index c1d1a412..b6f146c0 100644 --- a/src/main.c +++ b/src/main.c @@ -91,6 +91,7 @@ #include "listensocket.h" #include "fastevent.h" #include "prng.h" +#include "geoip.h" #include "navigation.h" #include @@ -755,6 +756,10 @@ int main(int argc, char **argv) ICECAST_LOG_INFO("Server's PID is %lli", (long long int)getpid()); __log_system_name(); + config = config_get_config(); + global.geoip_db = geoip_db_new(config->geoipdbfile); + config_release_config(); + /* REM 3D Graphics */ /* let her rip */ diff --git a/src/stats.c b/src/stats.c index 7e69d658..87deddef 100644 --- a/src/stats.c +++ b/src/stats.c @@ -934,8 +934,10 @@ static xmlNodePtr _dump_stats_to_doc (xmlNodePtr root, unsigned int flags, const if (source_real->running) xmlNewTextChild(xmlnode, NULL, XMLSTR("content-type"), XMLSTR(source_real->format->contenttype)); - if (flags & STATS_XML_FLAG_SHOW_LISTENERS) + if (flags & STATS_XML_FLAG_SHOW_LISTENERS) { admin_add_listeners_to_mount(source_real, xmlnode, client->mode); + admin_add_geoip_to_mount(source_real, xmlnode, client->mode); + } } avl_tree_unlock(global.source_tree); diff --git a/src/xml2json.c b/src/xml2json.c index 3e48152c..5287793c 100644 --- a/src/xml2json.c +++ b/src/xml2json.c @@ -467,6 +467,90 @@ static void render_node_legacystats(json_renderer_t *renderer, xmlDocPtr doc, xm json_renderer_write_key(renderer, (const char *)cur->name, JSON_RENDERER_FLAGS_NONE); render_node(renderer, doc, cur, node, cache); nodelist_unset(&nodelist, i); + } else if (strcmp((const char *)cur->name, "geoip") == 0) { + xmlNodePtr geoip = NULL; + + for (size_t j = i; j < len; j++) { + xmlNodePtr subcur = nodelist_get(&nodelist, j); + if (subcur == NULL) + continue; + + if (subcur->type == XML_ELEMENT_NODE && subcur->name && strcmp((const char *)cur->name, (const char *)subcur->name) == 0) { + nodelist_unset(&nodelist, j); + geoip = subcur; + } + } + + if (geoip) { + json_renderer_write_key(renderer, (const char *)cur->name, JSON_RENDERER_FLAGS_NONE); + json_renderer_begin(renderer, JSON_ELEMENT_TYPE_OBJECT); + + { + xmlNodePtr child = geoip->xmlChildrenNode; + + json_renderer_write_key(renderer, "country", JSON_RENDERER_FLAGS_NONE); + json_renderer_begin(renderer, JSON_ELEMENT_TYPE_ARRAY); + while (child) { + if (child->type == XML_ELEMENT_NODE && child->name && strcmp((const char *)child->name, "country") == 0) { + xmlChar *keyval = xmlGetProp(child, XMLSTR("iso-alpha-2")); + + json_renderer_begin(renderer, JSON_ELEMENT_TYPE_OBJECT); + + if (keyval) { + json_renderer_write_key(renderer, "iso-alpha-2", JSON_RENDERER_FLAGS_NONE); + json_renderer_write_string(renderer, (const char *)keyval, JSON_RENDERER_FLAGS_NONE); + xmlFree(keyval); + } + + for (xmlNodePtr subchild = child->xmlChildrenNode; subchild; subchild = subchild->next) { + if (subchild->type == XML_ELEMENT_NODE && subchild->name) { + xmlChar *value = xmlNodeListGetString(doc, subchild->xmlChildrenNode, 1); + if (value) { + json_renderer_write_key(renderer, (const char*)subchild->name, JSON_RENDERER_FLAGS_NONE); + json_renderer_write_int(renderer, strtoll((const char*)value, NULL, 10)); + xmlFree(value); + } + } + } + + json_renderer_end(renderer); + } + child = child->next; + } + json_renderer_end(renderer); + } + + { + xmlNodePtr child = geoip->xmlChildrenNode; + + json_renderer_write_key(renderer, "location", JSON_RENDERER_FLAGS_NONE); + json_renderer_begin(renderer, JSON_ELEMENT_TYPE_ARRAY); + while (child) { + ICECAST_LOG_INFO("child->name=<%s>", child->name); + if (child->type == XML_ELEMENT_NODE && child->name && strcmp((const char *)child->name, "location") == 0) { + static const char * keys[] = {"latitude", "longitude", NULL}; + + json_renderer_begin(renderer, JSON_ELEMENT_TYPE_OBJECT); + for (const char **p = keys; *p; p++) { + xmlChar *keyval = xmlGetProp(child, XMLSTR(*p)); + + + if (keyval) { + json_renderer_write_key(renderer, *p, JSON_RENDERER_FLAGS_NONE); + json_renderer_write_string(renderer, (const char *)keyval, JSON_RENDERER_FLAGS_NONE); + xmlFree(keyval); + } + + } + json_renderer_end(renderer); + } + child = child->next; + } + json_renderer_end(renderer); + } + + json_renderer_end(renderer); + } } } //render_node_generic(renderer, doc, node, parent, cache);