telize

High performance JSON IP and GeoIP REST API (IP Geolocation)
Log | Files | Refs | README | LICENSE

commit a8d52c8d30f5ff5197b574a6969a4141c777cca6
parent 25e13731258e428d07272b88c154b52b59e8d89a
Author: Joris Vink <joris@coders.se>
Date:   Thu, 29 Nov 2018 13:11:33 +0100

Refactor telize.

- Move duplicate code into utils.c
- Simplify logic all around the board.

Diffstat:
Aassets/bad_ip.json | 4++++
Mconf/telize.conf | 22++++++++++++++--------
Msrc/ip.c | 33+++++++--------------------------
Msrc/jsonip.c | 58+++++++++++++++++++---------------------------------------
Msrc/location.c | 250+++++++++++++++++++++++++++++++++----------------------------------------------
Asrc/telize.h | 27+++++++++++++++++++++++++++
Asrc/utils.c | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 281 insertions(+), 218 deletions(-)

diff --git a/assets/bad_ip.json b/assets/bad_ip.json @@ -0,0 +1,4 @@ +{ + "code": 401, + "message": "Input string is not a valid IP address" +} diff --git a/conf/telize.conf b/conf/telize.conf @@ -1,7 +1,7 @@ # telize configuration bind 127.0.0.1 8888 -load ./telize.so init +load ./telize.so telize_init tls_dhparam dh2048.pem @@ -13,19 +13,25 @@ domain * { accesslog telize.log - static /ip ip + static /ip request_ip + static /jsonip request_json_ip + static /location request_location + dynamic ^/location/[a-f0-9\:.]*$ request_location + + restrict /ip get + restrict /jsonip get + restrict /location get + restrict ^/location/[a-f0-9\:.]*$ get - static /jsonip jsonip params qs:get /jsonip { - validate callback v_callback + validate callback v_callback } - static /location location params qs:get /location { - validate callback v_callback + validate callback v_callback } - dynamic ^/location/[a-f0-9\:.]*$ location + params qs:get ^/location/[a-f0-9\:.]*$ { - validate callback v_callback + validate callback v_callback } } diff --git a/src/ip.c b/src/ip.c @@ -12,39 +12,20 @@ /* */ /*****************************************************************************/ -#include <string.h> -#include <sys/socket.h> - -#include <kore/kore.h> -#include <kore/http.h> - -int ip(struct http_request *); +#include "telize.h" int -ip(struct http_request *req) +request_ip(struct http_request *req) { - const char *visitor_ip, *ip; - char *addr; - - addr = kore_malloc(INET6_ADDRSTRLEN); + char ip[INET6_ADDRSTRLEN]; - if (req->owner->family == AF_INET) { - inet_ntop(req->owner->family, &(req->owner->addr.ipv4.sin_addr), addr, INET6_ADDRSTRLEN); - } else { - inet_ntop(req->owner->family, &(req->owner->addr.ipv6.sin6_addr), addr, INET6_ADDRSTRLEN); - } - - if (http_request_header(req, "X-Forwarded-For", &visitor_ip)) { - strtok(visitor_ip, ","); - ip = visitor_ip; - } else { - ip = addr; + if (!telize_request_ip(req, ip, sizeof(ip))) { + http_response(req, HTTP_STATUS_INTERNAL_ERROR, NULL, 0); + return (KORE_RESULT_OK); } http_response_header(req, "content-type", "text/plain"); - http_response(req, 200, ip, strlen(ip)); - - kore_free(addr); + http_response(req, HTTP_STATUS_OK, ip, strlen(ip)); return (KORE_RESULT_OK); } diff --git a/src/jsonip.c b/src/jsonip.c @@ -12,60 +12,40 @@ /* */ /*****************************************************************************/ -#include <stdbool.h> -#include <stdio.h> -#include <sys/socket.h> - -#include <kore/kore.h> -#include <kore/http.h> - -int jsonip(struct http_request *); +#include "telize.h" int -jsonip(struct http_request *req) +request_json_ip(struct http_request *req) { - const char *visitor_ip, *ip; - char *answer, *callback, *addr; - bool is_callback = false; + struct kore_buf json; + char *answer, *callback; + char ip[INET6_ADDRSTRLEN]; - http_populate_get(req); + if (!telize_request_ip(req, ip, sizeof(ip))) { + http_response(req, HTTP_STATUS_INTERNAL_ERROR, NULL, 0); + return (KORE_RESULT_OK); + } - struct kore_buf json; - kore_buf_init(&json, 4096); + callback = NULL; - addr = kore_malloc(INET6_ADDRSTRLEN); + http_populate_get(req); + kore_buf_init(&json, 4096); http_response_header(req, "Access-Control-Allow-Origin", "*"); http_response_header(req, "Cache-Control", "no-cache"); - http_response_header(req, "Content-Type", "application/json; charset=utf-8"); + http_response_header(req, "Content-Type", + "application/json; charset=utf-8"); - if (req->owner->family == AF_INET) { - inet_ntop(req->owner->family, &(req->owner->addr.ipv4.sin_addr), addr, INET6_ADDRSTRLEN); - } else { - inet_ntop(req->owner->family, &(req->owner->addr.ipv6.sin6_addr), addr, INET6_ADDRSTRLEN); - } - - if (http_request_header(req, "X-Forwarded-For", &visitor_ip)) { - strtok(visitor_ip, ","); - ip = visitor_ip; - } else { - ip = addr; - } - - if (http_argument_get_string(req, "callback", &callback)) { + if (http_argument_get_string(req, "callback", &callback)) kore_buf_appendf(&json, "%s(", callback); - is_callback = true; - } kore_buf_appendf(&json, "{\"ip\":\"%s\"", ip); + kore_buf_append(&json, callback != NULL ? "});\n" : "}\n", + callback != NULL ? 4 : 2); - kore_buf_append(&json, is_callback ? "});\n" : "}\n", is_callback ? 4 : 2); answer = kore_buf_stringify(&json, NULL); - - http_response(req, 200, answer, strlen(answer)); - - kore_buf_free(&json); - kore_free(addr); + http_response(req, HTTP_STATUS_OK, answer, strlen(answer)); + kore_buf_cleanup(&json); return (KORE_RESULT_OK); } diff --git a/src/location.c b/src/location.c @@ -12,204 +12,164 @@ /* */ /*****************************************************************************/ -#include <stdio.h> -#include <string.h> -#include <sys/socket.h> -#include <time.h> - -#include <kore/kore.h> -#include <kore/http.h> - -#include <maxminddb.h> - +#include "telize.h" #include "location.h" -#define ENTRY_TYPE_UINT32 0 -#define ENTRY_TYPE_STRING 1 -#define ENTRY_TYPE_DOUBLE 2 +#include "assets.h" -MMDB_s asn; -MMDB_s city; - -int init(int); -int location(struct http_request *); -void getdata(struct kore_buf *, MMDB_lookup_result_s *, MMDB_entry_data_s *, char *, int, ...); +static int response_sent(struct netbuf *); int -init(int state) -{ - if (MMDB_open("/var/db/GeoIP/GeoLite2-City.mmdb", - MMDB_MODE_MMAP, &city) != MMDB_SUCCESS) { - kore_log(LOG_ERR, "can't open GeoLite2 City database: %s", - errno_s); - return (KORE_RESULT_ERROR); - } - - if (MMDB_open("/var/db/GeoIP/GeoLite2-ASN.mmdb", - MMDB_MODE_MMAP, &asn) != MMDB_SUCCESS) { - kore_log(LOG_ERR, "can't open GeoLite2 ASN database: %s", - errno_s); - return (KORE_RESULT_ERROR); - } - - return (KORE_RESULT_OK); -} - -void -getdata(struct kore_buf *json, MMDB_lookup_result_s *lookup, MMDB_entry_data_s *entry_data, char *field, int type, ...) +request_location(struct http_request *req) { - va_list keys; - va_start(keys, type); - - MMDB_vget_value(&lookup->entry, entry_data, keys); - if (entry_data->has_data) { - switch(type) { - case ENTRY_TYPE_UINT32: - kore_buf_appendf(json, ",\"%s\":%d", field, entry_data->uint32); - break; - case ENTRY_TYPE_STRING: - kore_buf_appendf(json, ",\"%s\":\"%.*s\"", field, entry_data->data_size, entry_data->utf8_string); - break; - case ENTRY_TYPE_DOUBLE: - kore_buf_appendf(json, ",\"%s\":%.4f", field, entry_data->double_value); - break; - } + struct tm *info; + struct sockaddr_in ipv4; + struct sockaddr_in6 ipv6; + MMDB_lookup_result_s lookup; + time_t rawtime; + struct kore_buf *json, buf; + MMDB_entry_data_s entry_data; + size_t tz_len, len; + char ip[INET6_ADDRSTRLEN]; + char *ptr, *callback, *tz; + int gai_error, mmdb_error; + + if (!telize_request_ip(req, ip, sizeof(ip))) { + http_response(req, HTTP_STATUS_INTERNAL_ERROR, NULL, 0); + return (KORE_RESULT_OK); } - va_end(keys); -} - -int -location(struct http_request *req) -{ - const char *custom_ip, *visitor_ip, *ip; - char *answer, *callback, *addr; - bool is_callback = false; - - int gai_error, mmdb_error; - MMDB_lookup_result_s lookup; - MMDB_entry_data_s entry_data; - - time_t rawtime; - struct tm *info; - - struct sockaddr_in ipv4; - struct sockaddr_in6 ipv6; - http_populate_get(req); - struct kore_buf json; - kore_buf_init(&json, 4096); - - addr = kore_malloc(INET6_ADDRSTRLEN); - /* Set response headers */ http_response_header(req, "Access-Control-Allow-Origin", "*"); http_response_header(req, "Cache-Control", "no-cache"); - http_response_header(req, "Content-Type", "application/json; charset=utf-8"); - - /* IP address of the client originating the request */ - if (req->owner->family == AF_INET) - inet_ntop(req->owner->family, - &(req->owner->addr.ipv4.sin_addr), addr, INET6_ADDRSTRLEN); - else - inet_ntop(req->owner->family, - &(req->owner->addr.ipv6.sin6_addr), addr, INET6_ADDRSTRLEN); - - /* IP address specified in the "X-Forwarded-For" header */ - if (http_request_header(req, "X-Forwarded-For", &visitor_ip)) { - strtok(visitor_ip, ","); - ip = visitor_ip; - } else { - ip = addr; - } + http_response_header(req, "Content-Type", + "application/json; charset=utf-8"); /* Specific IP passed in the URL */ if (req->hdlr->type == HANDLER_TYPE_DYNAMIC) { - if ((custom_ip = strrchr(req->path, '/')) != NULL) - custom_ip++; + if ((ptr = strrchr(req->path, '/')) != NULL) { + ptr++; + len = strlen(ptr); + + if (len == 0 || len > sizeof(ip) - 1) { + http_response(req, HTTP_STATUS_BAD_REQUEST, + asset_bad_ip_json, asset_len_bad_ip_json); + return (KORE_RESULT_OK); + } - if (strlen(custom_ip)) - ip = custom_ip; - } + memcpy(ip, ptr, len); + ip[len] = '\0'; - /* Check for invalid IP addresses */ - if (!inet_pton(AF_INET, ip, &(ipv4.sin_addr)) && - !inet_pton(AF_INET6, ip, &(ipv6.sin6_addr))) { - answer = "{\"code\": 401, \"message\": \"Input string is not a valid IP address\"}"; - http_response(req, 400, answer, strlen(answer)); - goto cleanup; + if (!inet_pton(AF_INET, ip, &(ipv4.sin_addr)) && + !inet_pton(AF_INET6, ip, &(ipv6.sin6_addr))) { + http_response(req, HTTP_STATUS_BAD_REQUEST, + asset_bad_ip_json, asset_len_bad_ip_json); + return (KORE_RESULT_OK); + } + } } + callback = NULL; + json = kore_buf_alloc(4096); + /* Handle callback parameter */ - if (http_argument_get_string(req, "callback", &callback)) { - kore_buf_appendf(&json, "%s(", callback); - is_callback = true; - } + if (http_argument_get_string(req, "callback", &callback)) + kore_buf_appendf(json, "%s(", callback); - kore_buf_appendf(&json, "{\"ip\":\"%s\"", ip); + kore_buf_appendf(json, "{\"ip\":\"%s\"", ip); /* GeoLite2 City lookup */ - lookup = MMDB_lookup_string(&city, ip, &gai_error, &mmdb_error); + lookup = MMDB_lookup_string(&telize_city, ip, &gai_error, &mmdb_error); - getdata(&json, &lookup, &entry_data, "continent_code", ENTRY_TYPE_STRING, "continent", "code", NULL); - getdata(&json, &lookup, &entry_data, "country", ENTRY_TYPE_STRING, "country", "names", "en", NULL); + telize_getdata(json, &lookup, &entry_data, + "continent_code", ENTRY_TYPE_STRING, "continent", "code", NULL); + telize_getdata(json, &lookup, &entry_data, + "country", ENTRY_TYPE_STRING, "country", "names", "en", NULL); MMDB_get_value(&lookup.entry, &entry_data, "country", "iso_code", NULL); + if (entry_data.has_data) { - kore_buf_appendf(&json, ",\"country_code\":\"%.*s\"", + kore_buf_appendf(json, ",\"country_code\":\"%.*s\"", entry_data.data_size, entry_data.utf8_string); for (size_t loop = 0; loop < COUNTRIES; loop++) { - if (!strncmp(country_code_array[loop], entry_data.utf8_string, 2)) { - kore_buf_appendf(&json, ",\"country_code3\":\"%s\"", country_code3_array[loop]); + if (!strncmp(country_code_array[loop], + entry_data.utf8_string, 2)) { + kore_buf_appendf(json, + ",\"country_code3\":\"%s\"", + country_code3_array[loop]); break; } } } - getdata(&json, &lookup, &entry_data, "region", ENTRY_TYPE_STRING, "subdivisions", "0", "names", "en", NULL); - getdata(&json, &lookup, &entry_data, "region_code", ENTRY_TYPE_STRING, "subdivisions", "0", "iso_code", NULL); - getdata(&json, &lookup, &entry_data, "city", ENTRY_TYPE_STRING, "city", "names", "en", NULL); - getdata(&json, &lookup, &entry_data, "postal_code", ENTRY_TYPE_STRING, "postal", "code", NULL); - getdata(&json, &lookup, &entry_data, "latitude", ENTRY_TYPE_DOUBLE, "location", "latitude", NULL); - getdata(&json, &lookup, &entry_data, "longitude", ENTRY_TYPE_DOUBLE, "location", "longitude", NULL); + telize_getdata(json, &lookup, &entry_data, "region", + ENTRY_TYPE_STRING, "subdivisions", "0", "names", "en", NULL); + + telize_getdata(json, &lookup, &entry_data, "region_code", + ENTRY_TYPE_STRING, "subdivisions", "0", "iso_code", NULL); + + telize_getdata(json, &lookup, &entry_data, "city", + ENTRY_TYPE_STRING, "city", "names", "en", NULL); + + telize_getdata(json, &lookup, &entry_data, "postal_code", + ENTRY_TYPE_STRING, "postal", "code", NULL); + + telize_getdata(json, &lookup, &entry_data, "latitude", + ENTRY_TYPE_DOUBLE, "location", "latitude", NULL); + + telize_getdata(json, &lookup, &entry_data, "longitude", + ENTRY_TYPE_DOUBLE, "location", "longitude", NULL); + + MMDB_get_value(&lookup.entry, &entry_data, + "location", "time_zone", NULL); - MMDB_get_value(&lookup.entry, &entry_data, "location", "time_zone", NULL); if (entry_data.has_data) { - size_t tz_len = entry_data.data_size; - char *tz = strndup(entry_data.utf8_string, tz_len); + tz_len = entry_data.data_size; + tz = strndup(entry_data.utf8_string, tz_len); - struct kore_buf *b; - b = kore_buf_alloc(tz_len); - kore_buf_append(b, tz, tz_len); - kore_buf_replace_string(b, "/", "\\/", 2); - kore_buf_appendf(&json, ",\"timezone\":\"%s\"", kore_buf_stringify(b, NULL)); - kore_buf_free(b); + kore_buf_init(&buf, tz_len); + kore_buf_append(&buf, tz, tz_len); + kore_buf_replace_string(&buf, "/", "\\/", 2); + kore_buf_appendf(json, ",\"timezone\":\"%s\"", + kore_buf_stringify(&buf, NULL)); + kore_buf_cleanup(&buf); setenv("TZ", tz, 1); tzset(); time(&rawtime); info = localtime(&rawtime); - kore_buf_appendf(&json, ",\"offset\":%d", info->tm_gmtoff); + kore_buf_appendf(json, ",\"offset\":%d", info->tm_gmtoff); free(tz); } /* GeoLite2 ASN lookup */ - lookup = MMDB_lookup_string(&asn, ip, &gai_error, &mmdb_error); + lookup = MMDB_lookup_string(&telize_asn, ip, &gai_error, &mmdb_error); + + telize_getdata(json, &lookup, &entry_data, "asn", + ENTRY_TYPE_UINT32, "autonomous_system_number", NULL); - getdata(&json, &lookup, &entry_data, "asn", ENTRY_TYPE_UINT32, "autonomous_system_number", NULL); - getdata(&json, &lookup, &entry_data, "organization", ENTRY_TYPE_STRING, "autonomous_system_organization", NULL); + telize_getdata(json, &lookup, &entry_data, "organization", + ENTRY_TYPE_STRING, "autonomous_system_organization", NULL); - kore_buf_append(&json, is_callback ? "});\n" : "}\n", is_callback ? 4 : 2); - answer = kore_buf_stringify(&json, NULL); + kore_buf_append(json, callback != NULL ? "});\n" : "}\n", + callback != NULL ? 4 : 2); - http_response(req, 200, answer, strlen(answer)); + http_response_stream(req, HTTP_STATUS_OK, json->data, + json->offset, response_sent, json); + + return (KORE_RESULT_OK); +} + +static int +response_sent(struct netbuf *nb) +{ + struct kore_buf *buf = nb->extra; -cleanup: - kore_buf_free(&json); - kore_free(addr); + kore_buf_free(buf); return (KORE_RESULT_OK); } diff --git a/src/telize.h b/src/telize.h @@ -0,0 +1,27 @@ +#ifndef __H_TELIZE_H +#define __H_TELIZE_H + +#include <kore/kore.h> +#include <kore/http.h> + +#include <maxminddb.h> + +#define ENTRY_TYPE_UINT32 0 +#define ENTRY_TYPE_STRING 1 +#define ENTRY_TYPE_DOUBLE 2 + +int request_ip(struct http_request *); +int request_json_ip(struct http_request *); +int request_location(struct http_request *); + +int telize_init(int); +int telize_request_ip(struct http_request *, char *, size_t); +void telize_getdata(struct kore_buf *, MMDB_lookup_result_s *, + MMDB_entry_data_s *, char *, int, ...); + +int location(struct http_request *); + +extern MMDB_s telize_asn; +extern MMDB_s telize_city; + +#endif diff --git a/src/utils.c b/src/utils.c @@ -0,0 +1,105 @@ +#include "telize.h" + +MMDB_s telize_asn; +MMDB_s telize_city; + +int +telize_init(int state) +{ + if (state != KORE_MODULE_LOAD) { + kore_log(LOG_NOTICE, "denying reload of module, not fixed"); + return (KORE_RESULT_ERROR); + } + + if (MMDB_open("/var/db/GeoIP/GeoLite2-City.mmdb", + MMDB_MODE_MMAP, &telize_city) != MMDB_SUCCESS) + fatalx("can't open GeoLite2 City database: %s", errno_s); + + if (MMDB_open("/var/db/GeoIP/GeoLite2-ASN.mmdb", + MMDB_MODE_MMAP, &telize_asn) != MMDB_SUCCESS) + fatalx("can't open GeoLite2 ASN database: %s", errno_s); + + return (KORE_RESULT_OK); +} + +void +telize_getdata(struct kore_buf *json, MMDB_lookup_result_s *lookup, + MMDB_entry_data_s *entry_data, char *field, int type, ...) +{ + va_list keys; + va_start(keys, type); + + MMDB_vget_value(&lookup->entry, entry_data, keys); + + if (entry_data->has_data) { + switch(type) { + case ENTRY_TYPE_UINT32: + kore_buf_appendf(json, ",\"%s\":%d", + field, entry_data->uint32); + break; + case ENTRY_TYPE_STRING: + kore_buf_appendf(json, ",\"%s\":\"%.*s\"", + field, entry_data->data_size, + entry_data->utf8_string); + break; + case ENTRY_TYPE_DOUBLE: + kore_buf_appendf(json, ",\"%s\":%.4f", + field, entry_data->double_value); + break; + } + } + + va_end(keys); +} + +int +telize_request_ip(struct http_request *req, char *buf, size_t len) +{ + char *ptr; + void *addr; + size_t hdr_len; + const char *hdr_ip; + + switch (req->owner->family) { + case AF_INET: + addr = &req->owner->addr.ipv4.sin_addr; + break; + case AF_INET6: + addr = &req->owner->addr.ipv6.sin6_addr; + break; + default: + kore_log(LOG_ERR, "unsupported family %d", req->owner->family); + return (KORE_RESULT_ERROR); + } + + if (inet_ntop(req->owner->family, addr, buf, len) == NULL) { + kore_log(LOG_ERR, "inet_ntop failed (%s)", errno_s); + return (KORE_RESULT_ERROR); + } + + if (!http_request_header(req, "x-forwarded-for", &hdr_ip)) + return (KORE_RESULT_OK); + + if ((ptr = strchr(hdr_ip, ',')) != NULL) { + hdr_len = ptr - hdr_ip; + } else { + hdr_len = strlen(hdr_ip); + } + + if (hdr_len > len - 1) { + kore_log(LOG_ERR, "IP in X-Forwarded-For too large"); + return (KORE_RESULT_ERROR); + } + + memcpy(buf, hdr_ip, hdr_len); + buf[hdr_len] = '\0'; + + if (inet_pton(AF_INET, buf, &req->owner->addr) == -1) { + if (inet_pton(AF_INET6, buf, &req->owner->addr) == -1) { + kore_log(LOG_ERR, "Malformed IP in X-Forwarded-For"); + return (KORE_RESULT_ERROR); + } + } + + return (KORE_RESULT_OK); +}