Patch by Robert Scheck for geoipupdate <= 2.5.0 which backports the support for hashed license keys. It is based on the following upstream commits: 1. https://github.com/maxmind/geoipupdate/commit/b7862460b6769f5c40b72de835b868882c3f3883 2. https://github.com/maxmind/geoipupdate/commit/4b41ea887e836ddb1a36c0c3a071ac49e8d71a25 3. https://github.com/maxmind/geoipupdate/commit/01fcfc1170dcedc35c758e111b8905cf6513d8b9 4. https://github.com/maxmind/geoipupdate/commit/6794f2eed0063a50749bf400e135f4f47ca2f465 5. https://github.com/maxmind/geoipupdate/commit/0670ee3e1237a72bae785877b354dbfe135de6be 6. https://github.com/maxmind/geoipupdate/commit/cddabc6f645ea3abbb8117ef0d03069df560fc36 7. https://github.com/maxmind/geoipupdate/commit/35a640087a4eca4e37306b717fa8513e26487b47 8. https://github.com/maxmind/geoipupdate/commit/642978928019f87181dd7c108b0363841c0135a9 9. https://github.com/maxmind/geoipupdate/commit/824ef039bbc8a2ed5a90d1da5a51d4a1639fdb09 --- geoipupdate-2.5.0/bin/geoipupdate.c 2017-10-30 15:38:24.000000000 +0100 +++ geoipupdate-2.5.0/bin/geoipupdate.c.licensekey 2023-04-13 21:23:51.340750299 +0200 @@ -22,6 +22,7 @@ enum gu_status { GU_OK = 0, GU_ERROR = 1, + GU_NO_UPDATE = 2, }; typedef struct { @@ -41,17 +42,14 @@ static int md5hex(const char *, char *); static void common_req(CURL *, geoipupdate_s *); static size_t get_expected_file_md5(char *, size_t, size_t, void *); -static void +static int download_to_file(geoipupdate_s *, const char *, const char *, char *); static long get_server_time(geoipupdate_s *); static size_t mem_cb(void *, size_t, size_t, void *); static in_mem_s *in_mem_s_new(void); static void in_mem_s_delete(in_mem_s *); -static in_mem_s *get(geoipupdate_s *, const char *); -static void md5hex_license_ipaddr(geoipupdate_s *, const char *, char *); static int update_database_general_all(geoipupdate_s *); static int update_database_general(geoipupdate_s *, const char *); -static int update_country_database(geoipupdate_s *); static int gunzip_and_replace(geoipupdate_s const *const, char const *const, char const *const, @@ -196,14 +194,12 @@ return GU_ERROR; } - err = (gu->license.account_id == NO_ACCOUNT_ID) - ? update_country_database(gu) - : update_database_general_all(gu); + err = update_database_general_all(gu); } geoipupdate_s_delete(gu); } curl_global_cleanup(); - return err ? GU_ERROR : GU_OK; + return err & GU_ERROR ? GU_ERROR : GU_OK; } static ssize_t my_getline(char **linep, size_t *linecapp, FILE *stream) { @@ -242,8 +238,9 @@ say_if(up->verbose, "AccountID %d\n", up->license.account_id); continue; } - if (sscanf(strt, "LicenseKey %12s", &up->license.license_key[0]) == 1) { - say_if(up->verbose, "LicenseKey %s\n", up->license.license_key); + if (sscanf(strt, "LicenseKey %99s", &up->license.license_key[0]) == 1) { + say_if( + up->verbose, "LicenseKey %.4s...\n", up->license.license_key); continue; } @@ -534,15 +531,11 @@ // Make an HTTP request and download the response body to a file. // // If the HTTP status is not 2xx, we have a error message in the body rather -// than a file. Write it to stderr and exit. -// -// TODO(wstorey@maxmind.com): Return boolean/int whether we succeeded rather -// than exiting. Beyond being cleaner and easier to test, it will allow us to -// clean up after ourselves better. -static void download_to_file(geoipupdate_s *gu, - const char *url, - const char *fname, - char *expected_file_md5) { +// than a file. Write it to stderr and return an error. +static int download_to_file(geoipupdate_s *gu, + const char *url, + const char *fname, + char *expected_file_md5) { FILE *f = fopen(fname, "wb"); if (f == NULL) { fprintf(stderr, "Can't open %s: %s\n", fname, strerror(errno)); @@ -553,6 +546,20 @@ CURL *curl = gu->curl; expected_file_md5[0] = '\0'; + + char account_id[10] = {0}; + int n = snprintf(account_id, 10, "%d", gu->license.account_id); + exit_if(n < 0, + "Error creating account ID string for %d: %s\n", + gu->license.account_id, + strerror(errno)); + exit_if(n < 0 || n >= 10, + "An unexpectedly large account ID was encountered: %d\n", + gu->license.account_id); + + curl_easy_setopt(curl, CURLOPT_USERNAME, account_id); + curl_easy_setopt(curl, CURLOPT_PASSWORD, gu->license.license_key); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, get_expected_file_md5); curl_easy_setopt(curl, CURLOPT_HEADERDATA, expected_file_md5); @@ -577,7 +584,19 @@ exit(1); } - if (status < 200 || status >= 300) { + if (status == 304) { + say_if(gu->verbose, "No new updates available\n"); + unlink(fname); + return GU_NO_UPDATE; + } + + if (status == 401) { + fprintf(stderr, "Your account ID or license key is invalid\n"); + unlink(fname); + return GU_ERROR; + } + + if (status != 200) { fprintf(stderr, "Received an unexpected HTTP status code of %ld from %s:\n", status, @@ -589,7 +608,7 @@ free(message); } unlink(fname); - exit(1); + return GU_ERROR; } // We have HTTP 2xx. @@ -600,8 +619,9 @@ fprintf(stderr, "Did not receive a valid expected database MD5 from server\n"); unlink(fname); - exit(1); + return GU_ERROR; } + return GU_OK; } // Retrieve the server file time for the previous HTTP request. @@ -645,7 +665,17 @@ } } -static in_mem_s *get(geoipupdate_s *gu, const char *url) { +static int update_database_general(geoipupdate_s *gu, const char *edition_id) { + char *url = NULL, *geoip_filename = NULL, *geoip_gz_filename = NULL; + char hex_digest[33] = {0}; + + // Get the filename. + xasprintf(&url, + "%s://%s/app/update_getfilename?product_id=%s", + gu->proto, + gu->host, + edition_id); + in_mem_s *mem = in_mem_s_new(); say_if(gu->verbose, "url: %s\n", url); @@ -663,47 +693,16 @@ long status = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); - exit_if(status < 200 || status >= 300, - "Received an unexpected HTTP status code of %ld from %s", - status, - url); - - return mem; -} - -// Generate an MD5 hash of the concatenation of license key with IP address. -// -// This hash is suitable for the challenge parameter for downloading from the -// /update_secure endpoint. -static void md5hex_license_ipaddr(geoipupdate_s *gu, - const char *client_ipaddr, - char *new_digest_str) { - unsigned char digest[16]; - MD5_CONTEXT context; - md5_init(&context); - md5_write(&context, - (unsigned char *)gu->license.license_key, - strlen(gu->license.license_key)); - md5_write(&context, (unsigned char *)client_ipaddr, strlen(client_ipaddr)); - md5_final(&context); - memcpy(digest, context.buf, 16); - for (int i = 0; i < 16; i++) { - snprintf(&new_digest_str[2 * i], 3, "%02x", digest[i]); + if (status != 200) { + fprintf(stderr, + "Received an unexpected HTTP status code of %ld from %s\n", + status, + url); + free(url); + in_mem_s_delete(mem); + return GU_ERROR; } -} - -static int update_database_general(geoipupdate_s *gu, const char *edition_id) { - char *url = NULL, *geoip_filename = NULL, *geoip_gz_filename = NULL, - *client_ipaddr = NULL; - char hex_digest[33] = {0}, hex_digest2[33] = {0}; - // Get the filename. - xasprintf(&url, - "%s://%s/app/update_getfilename?product_id=%s", - gu->proto, - gu->host, - edition_id); - in_mem_s *mem = get(gu, url); free(url); if (mem->size == 0) { fprintf(stderr, "edition_id %s not found\n", edition_id); @@ -718,63 +717,27 @@ md5hex(geoip_filename, hex_digest); say_if(gu->verbose, "md5hex_digest: %s\n", hex_digest); - // Look up our IP. - xasprintf(&url, "%s://%s/app/update_getipaddr", gu->proto, gu->host); - mem = get(gu, url); - free(url); - - client_ipaddr = strdup(mem->ptr); - if (NULL == client_ipaddr) { - fprintf(stderr, "Unable to allocate memory for client IP address.\n"); - free(geoip_filename); - in_mem_s_delete(mem); - return GU_ERROR; - } - - in_mem_s_delete(mem); - - say_if(gu->verbose, "Client IP address: %s\n", client_ipaddr); - - // Make the challenge MD5 hash. - md5hex_license_ipaddr(gu, client_ipaddr, hex_digest2); - - free(client_ipaddr); - say_if(gu->verbose, "md5hex_digest2 (challenge): %s\n", hex_digest2); - // Download. xasprintf(&url, - "%s://%s/app/" - "update_secure?db_md5=%s&challenge_md5=%s&user_id=%d&edition_id=%" - "s", + "%s://%s/geoip/databases/%s/update?db_md5=%s", gu->proto, gu->host, - hex_digest, - hex_digest2, - gu->license.account_id, - edition_id); + edition_id, + hex_digest); xasprintf(&geoip_gz_filename, "%s.gz", geoip_filename); char expected_file_md5[33] = {0}; - download_to_file(gu, url, geoip_gz_filename, expected_file_md5); + int rc = download_to_file(gu, url, geoip_gz_filename, expected_file_md5); free(url); - // Was there actually an update? We can tell because if not we will have - // the same MD5 reported back. Note in the past we would check the response - // body which does still say whether we have an update. - if (strcmp(hex_digest, expected_file_md5) == 0) { - say_if(gu->verbose, "No new updates available\n"); - unlink(geoip_gz_filename); - free(geoip_filename); - free(geoip_gz_filename); - return GU_OK; - } - - long filetime = -1; - if (gu->preserve_file_times) { - filetime = get_server_time(gu); + if (rc == GU_OK) { + long filetime = -1; + if (gu->preserve_file_times) { + filetime = get_server_time(gu); + } + rc = gunzip_and_replace( + gu, geoip_gz_filename, geoip_filename, expected_file_md5, filetime); } - int rc = gunzip_and_replace( - gu, geoip_gz_filename, geoip_filename, expected_file_md5, filetime); free(geoip_gz_filename); free(geoip_filename); @@ -790,53 +753,6 @@ return err; } -static int update_country_database(geoipupdate_s *gu) { - char *geoip_filename = NULL, *geoip_gz_filename = NULL, *url = NULL; - char hex_digest[33] = {0}; - - xasprintf(&geoip_filename, "%s/GeoIP.dat", gu->database_dir); - xasprintf(&geoip_gz_filename, "%s/GeoIP.dat.gz", gu->database_dir); - - // Calculate the MD5 hash of the database we currently have, if any. We get - // back a zero MD5 hash if we don't have it yet. - md5hex(geoip_filename, hex_digest); - say_if(gu->verbose, "md5hex_digest: %s\n", hex_digest); - - xasprintf(&url, - "%s://%s/app/update?license_key=%s&md5=%s", - gu->proto, - gu->host, - &gu->license.license_key[0], - hex_digest); - - char expected_file_md5[33] = {0}; - download_to_file(gu, url, geoip_gz_filename, expected_file_md5); - free(url); - - // Was there actually an update? We can tell because if not we will have - // the same MD5 reported back. Note in the past we would check the response - // body which does still say whether we have an update. - if (strcmp(hex_digest, expected_file_md5) == 0) { - say_if(gu->verbose, "No new updates available\n"); - unlink(geoip_gz_filename); - free(geoip_filename); - free(geoip_gz_filename); - return GU_OK; - } - - long filetime = -1; - if (gu->preserve_file_times) { - filetime = get_server_time(gu); - } - int rc = gunzip_and_replace( - gu, geoip_gz_filename, geoip_filename, expected_file_md5, filetime); - - free(geoip_gz_filename); - free(geoip_filename); - - return rc; -} - // Decompress the compressed database and move it into place in the database // directory. // --- geoipupdate-2.5.0/bin/geoipupdate.h 2017-10-30 15:38:24.000000000 +0100 +++ geoipupdate-2.5.0/bin/geoipupdate.h.licensekey 2023-04-13 20:59:03.483969697 +0200 @@ -12,7 +12,11 @@ typedef struct { int account_id; - char license_key[13]; + // For a long time, license keys were restricted to 12 characters. However, + // we want to change this for newer license keys. The array size is + // arbitrarily 100 as that seems big enough to hold any future license + // key. + char license_key[100]; edition_s *first; } license_s;