/* 21-23 November 2025, by R. Harmsen: Determine the country from an IPv4 or IPv6 address, using publicly available internet resources. I successively try four of them (found using Grok), and stop as soon as one is successful. So hopefully if in future such a service goes out of business, some of the others will automatically take over. Same if one of them might get exhausted for free access, as a result of too many requests per day. This replaces an earlier mechanism that used a library and database by Maxmind.com. See below. See also https://rudhar.com/sfreview/IP2cntry.htm . */ #include #include #include #include #include #include "IP2cntr.h" static char pattern1[] = /* ipapi.is */ "curl https://api.ipapi.is/?q='%s' 2> /dev/null | grep country_code | " "sed -E 's@^[^a-zA-Z]*country_code[^a-zA-Z]+([A-Z][A-Z])[^a-zA-Z]*$@\\1@'"; static char pattern2[] = /* ipinfo.io */ "curl https://ipinfo.io/'%s'/country 2> /dev/null"; static char pattern3[] = /* ip-api.com */ "curl http://ip-api.com/json/'%s' 2> /dev/null | " "sed -E 's@^.*\"countryCode\" *: *\"([A-Z][A-Z])\".*$@\\1@'"; static char pattern4[] = /* db-ip.com */ "curl https://api.db-ip.com/v2/free/'%s' 2> /dev/null | grep countryCode | " "sed -E 's@^[^a-zA-Z]*countryCode[^a-zA-Z]+([A-Z][A-Z])[^a-zA-Z]*$@\\1@'"; char *patterns[] = {pattern1, pattern2, pattern3, pattern4}; /* To avoid any possibility of spoofing the IP address and thereby perhaps enabling code injection or similar nasty things. */ void sanitize_IP (char *IP) { for (char *p = IP; *p; p++) if (*p != '.' && *p != ':' && !isxdigit(*p)) *p = '-'; } int IP2cntr (char *IPadress, char ISO_country[3], FILE *fperr) { char command[sizeof pattern4 + /* Room for IP address, incl. IPv6 */ 8*4+7*1+1]; FILE *pip; /* Size of the array ISO_country isn't known at runtime, so sizeof wouldn't work. I set the constant to the size minus 1, so not cointing the null byte. */ const size_t netsize = 2; sanitize_IP(IPadress); for (int i = 0; i < sizeof patterns / sizeof patterns[0]; i++) { sprintf(command, patterns[i], IPadress); #ifdef DEBUG fprintf(fperr, "\nLine %3d in %s, index %d: the command for popen(3) is:\n%s\n", __LINE__, __FILE__, i + 1, command); #endif pip = popen(command, "r"); if (!pip) return 1; memset(ISO_country, '\0', netsize + 1); if (fread(ISO_country, 1, netsize, pip) != netsize) { pclose(pip); return 2; } pclose(pip); #ifdef DEBUG fprintf(fperr, "\nLine %3d in %s, index %d: resulting country code is: %s\n", __LINE__, __FILE__, i + 1, ISO_country); #endif if (isascii(ISO_country[0]) && isupper(ISO_country[0]) && isascii(ISO_country[1]) && isupper(ISO_country[1])) return 0; } return 3; } #if 0 /* ========================================================================= 21 November 2025: I no longer use Maxmind.com’s database and library, but online internet commands instead. For an explanation see http://rhar.info/sfreview/IP2cntry.htm ========================================================================*/ #include #include #include #include "IP2cntr.h" static char *DatabasePath = "/var/db/maxmind/GeoLite2-Country/GeoLite2-Country.mmdb"; int IP2cntr (char *IPadress, char ISO_country[3], FILE *fperr) { MMDB_s mmdb; int status, gai_error, mmdb_error, retval = 0; MMDB_lookup_result_s result; MMDB_entry_data_s entry_data; strcpy(ISO_country, "??"); status = MMDB_open(DatabasePath, MMDB_MODE_MMAP, &mmdb); if (status != MMDB_SUCCESS) { if (fperr) { fprintf(fperr, "\nCould not open Country Database\n%s\n%s\n", DatabasePath, MMDB_strerror(status)); } retval = 1; return retval; } result = MMDB_lookup_string(&mmdb, IPadress, &gai_error, &mmdb_error); if (gai_error != 0) { if (fperr) { fprintf(fperr, "\nGai-error looking up IP\n%s\n", MMDB_strerror(gai_error)); } retval = 2; goto close_and_end; } if (mmdb_error != MMDB_SUCCESS) { if (fperr) { fprintf(fperr, "\nMmdb-error looking up IP address\n%s\n", MMDB_strerror(mmdb_error)); } retval = 3; goto close_and_end; } if (!result.found_entry) { if (fperr) { fprintf(fperr, "\nIP adress not found\n%s\n", MMDB_strerror(mmdb_error)); } retval = 4; goto close_and_end; } else { status = MMDB_get_value(&result.entry, &entry_data, "country", "iso_code", NULL); if (status != MMDB_SUCCESS) { if (fperr) { fprintf(fperr, "\nCould not get value\n%s\n", MMDB_strerror(status)); } retval = 5; goto close_and_end; } if (entry_data.has_data && entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) { sprintf(ISO_country, "%.*s", entry_data.data_size <= 2 ? entry_data.data_size : 2, entry_data.utf8_string); } } close_and_end: MMDB_close(&mmdb); return retval; } #endif