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);
|