#include "publicip.h" #include "common/networking.h" #define FF_UNINITIALIZED ((const char*) (uintptr_t) -1) static FFNetworkingState states[2]; static const char* statuses[2] = { FF_UNINITIALIZED, FF_UNINITIALIZED }; void ffPreparePublicIp(FFPublicIPOptions* options) { FFNetworkingState* state = &states[options->ipv6]; const char** status = &statuses[options->ipv6]; if (*status != FF_UNINITIALIZED) { fputs("Error: PublicIp module can only be used once due to internal limitations\n", stderr); exit(1); } state->timeout = options->timeout; state->ipv6 = options->ipv6; if (options->url.length == 0) { state->compression = true; state->tfo = true; *status = ffNetworkingSendHttpRequest(state, options->ipv6 ? "v6.ipinfo.io" : "ipinfo.io", "/json", NULL); } else { FF_STRBUF_AUTO_DESTROY host = ffStrbufCreateCopy(&options->url); uint32_t hostStartIndex = ffStrbufFirstIndexS(&host, "://"); if (hostStartIndex < host.length) { if (hostStartIndex != 4 || !ffStrbufStartsWithIgnCaseS(&host, "http")) { fputs("Error: only http: protocol is supported. Use `Command` module with `curl` if needed\n", stderr); exit(1); } ffStrbufSubstrAfter(&host, hostStartIndex + (uint32_t) (strlen("://") - 1)); } uint32_t pathStartIndex = ffStrbufFirstIndexC(&host, '/'); FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); if (pathStartIndex != host.length) { ffStrbufAppendNS(&path, pathStartIndex, host.chars + (host.length - pathStartIndex)); host.length = pathStartIndex; host.chars[pathStartIndex] = '\0'; } *status = ffNetworkingSendHttpRequest(state, host.chars, path.length == 0 ? "/" : path.chars, NULL); } } static inline void wrapYyjsonFree(yyjson_doc** doc) { assert(doc); if (*doc) { yyjson_doc_free(*doc); } } const char* ffDetectPublicIp(FFPublicIPOptions* options, FFPublicIpResult* result) { FFNetworkingState* state = &states[options->ipv6]; const char** status = &statuses[options->ipv6]; if (*status == FF_UNINITIALIZED) { ffPreparePublicIp(options); } if (*status != NULL) { return *status; } FF_STRBUF_AUTO_DESTROY response = ffStrbufCreateA(4096); const char* error = ffNetworkingRecvHttpResponse(state, &response); *state = (FFNetworkingState) {}; *status = FF_UNINITIALIZED; if (error == NULL) { ffStrbufSubstrAfterFirstS(&response, "\r\n\r\n"); } else { return error; } if (response.length == 0) { return "Empty server response received"; } if (options->url.length == 0) { yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_opts(response.chars, response.length, 0, NULL, NULL); if (doc) { yyjson_val* root = yyjson_doc_get_root(doc); ffStrbufAppendJsonVal(&result->ip, yyjson_obj_get(root, "ip")); ffStrbufDestroy(&result->location); ffStrbufInitF(&result->location, "%s, %s", yyjson_get_str(yyjson_obj_get(root, "city")), yyjson_get_str(yyjson_obj_get(root, "country"))); return NULL; } } ffStrbufDestroy(&result->ip); ffStrbufInitMove(&result->ip, &response); ffStrbufTrimRightSpace(&result->ip); return NULL; }