master ohmu/fastfetch-openbsd / src / detection / publicip / publicip.c
ohmu  ·  2026-10-15
  1#include "publicip.h"
  2#include "common/networking.h"
  3
  4#define FF_UNINITIALIZED ((const char*) (uintptr_t) -1)
  5static FFNetworkingState states[2];
  6static const char* statuses[2] = { FF_UNINITIALIZED, FF_UNINITIALIZED };
  7
  8// Reads the port that follows the colon at `colonIndex`. The port has to be the last thing in
  9// `host`, so this must run before any part of the host is trimmed off.
 10static uint16_t ffPublicIpParseUrlPort(const FFstrbuf* host, uint32_t colonIndex) {
 11    const char* portStr = host->chars + colonIndex + 1;
 12    char* portEnd = nullptr;
 13    unsigned long portValue = strtoul(portStr, &portEnd, 10);
 14    if (portEnd == portStr || *portEnd != '\0' || portValue == 0 || portValue > 65535) {
 15        fputs("Error: invalid port in the PublicIp module URL\n", stderr);
 16        exit(1);
 17    }
 18
 19    return (uint16_t) portValue;
 20}
 21
 22void ffPreparePublicIp(FFPublicIPOptions* options) {
 23    FFNetworkingState* state = &states[options->ipv6];
 24    const char** status = &statuses[options->ipv6];
 25    if (*status != FF_UNINITIALIZED) {
 26        fputs("Error: PublicIp module can only be used once due to internal limitations\n", stderr);
 27        exit(1);
 28    }
 29
 30    state->timeout = options->timeout;
 31    state->ipv6 = options->ipv6;
 32
 33    if (options->url.length == 0) {
 34        state->tfo = true;
 35        *status = ffNetworkingSendHttpRequest(state, options->ipv6 ? "v6.ipinfo.io" : "ipinfo.io", 80, "/json", nullptr);
 36    } else {
 37        FF_STRBUF_AUTO_DESTROY host = ffStrbufCreateCopy(&options->url);
 38        uint32_t hostStartIndex = ffStrbufFirstIndexS(&host, "://");
 39        if (hostStartIndex < host.length) {
 40            if (hostStartIndex != 4 || !ffStrbufStartsWithIgnCaseS(&host, "http")) {
 41                fputs("Error: only http: protocol is supported. Use `Command` module with `curl` if needed\n", stderr);
 42                exit(1);
 43            }
 44            ffStrbufSubstrAfter(&host, hostStartIndex + (uint32_t) (strlen("://") - 1));
 45        }
 46        uint32_t pathStartIndex = ffStrbufFirstIndexC(&host, '/');
 47
 48        FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
 49        if (pathStartIndex != host.length) {
 50            ffStrbufAppendNS(&path, host.length - pathStartIndex, host.chars + pathStartIndex);
 51            ffStrbufSubstrBefore(&host, pathStartIndex);
 52        }
 53
 54        // An optional `:port` must be split off the host, otherwise getaddrinfo() is asked to
 55        // resolve a host name that still carries the port. Only a single colon can separate a
 56        // port: a bare IPv6 literal (`::1`) holds several of them and has no port at all,
 57        // which is why a bracketed literal (`[::1]:8080`) is the only unambiguous spelling.
 58        uint16_t port = 0;
 59        if (ffStrbufStartsWithC(&host, '[')) {
 60            uint32_t bracketEnd = ffStrbufFirstIndexC(&host, ']');
 61            if (bracketEnd == host.length) {
 62                fputs("Error: unmatched '[' in the PublicIp module URL\n", stderr);
 63                exit(1);
 64            }
 65
 66            if (bracketEnd + 1 < host.length) {
 67                if (host.chars[bracketEnd + 1] != ':') {
 68                    fputs("Error: unexpected characters after the IPv6 literal in the PublicIp module URL\n", stderr);
 69                    exit(1);
 70                }
 71                // Read the port while the string is still intact: trimming the brackets first
 72                // would invalidate the index it was found at.
 73                port = ffPublicIpParseUrlPort(&host, bracketEnd + 1);
 74            }
 75
 76            ffStrbufSubstrBefore(&host, bracketEnd);
 77            ffStrbufSubstrAfter(&host, 0); // drop the leading '['
 78        } else {
 79            uint32_t firstColon = ffStrbufFirstIndexC(&host, ':');
 80            if (firstColon < host.length && firstColon == ffStrbufLastIndexC(&host, ':')) {
 81                port = ffPublicIpParseUrlPort(&host, firstColon);
 82                ffStrbufSubstrBefore(&host, firstColon);
 83            }
 84        }
 85
 86        *status = ffNetworkingSendHttpRequest(state, host.chars, port ?: 80, path.length == 0 ? "/" : path.chars, nullptr);
 87    }
 88}
 89
 90static inline void wrapYyjsonFree(yyjson_doc** doc) {
 91    assert(doc);
 92    if (*doc) {
 93        yyjson_doc_free(*doc);
 94    }
 95}
 96
 97const char* ffDetectPublicIp(FFPublicIPOptions* options, FFPublicIpResult* result) {
 98    FFNetworkingState* state = &states[options->ipv6];
 99    const char** status = &statuses[options->ipv6];
100    if (*status == FF_UNINITIALIZED) {
101        ffPreparePublicIp(options);
102    }
103
104    if (*status != nullptr) {
105        const char* error = *status;
106        ffNetworkingResetState(state);
107        *status = FF_UNINITIALIZED;
108        return error;
109    }
110
111    FF_STRBUF_AUTO_DESTROY response = ffStrbufCreateA(4096);
112    const char* error = ffNetworkingRecvHttpResponse(state, &response);
113
114    ffNetworkingResetState(state);
115    *status = FF_UNINITIALIZED;
116
117    if (error == nullptr) {
118        ffStrbufSubstrAfterFirstS(&response, "\r\n\r\n");
119    } else {
120        return error;
121    }
122
123    if (response.length == 0) {
124        return "Empty server response received";
125    }
126
127    if (options->url.length == 0) {
128        [[gnu::cleanup(wrapYyjsonFree)]] yyjson_doc* doc = yyjson_read_opts(response.chars, response.length, 0, nullptr, nullptr);
129        if (doc) {
130            yyjson_val* root = yyjson_doc_get_root(doc);
131            ffStrbufAppendJsonVal(&result->ip, yyjson_obj_get(root, "ip"));
132            ffStrbufDestroy(&result->location);
133            ffStrbufInitF(&result->location, "%s, %s", yyjson_get_str(yyjson_obj_get(root, "city")), yyjson_get_str(yyjson_obj_get(root, "country")));
134            return nullptr;
135        }
136    }
137
138    ffStrbufDestroy(&result->ip);
139    ffStrbufInitMove(&result->ip, &response);
140    ffStrbufTrimRightSpace(&result->ip);
141    return nullptr;
142}