Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: C++ SDK tests

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
- name: Install libcurl on Linux
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev
- name: Configure
if: runner.os != 'Windows'
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
- name: Configure Windows
if: runner.os == 'Windows'
shell: pwsh
run: |
vcpkg install "curl[core,ssl]:x64-windows"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cmake -S . -B build -A x64 "-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake"
- name: Build
run: cmake --build build --config Release --parallel 4
- name: Test
run: ctest --test-dir build -C Release --output-on-failure -LE live
- name: Install SDK
run: cmake --install build --config Release --prefix install
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 1.0.1

- Add Arabic (`Language::kAr`) for single and bulk lookups.
- Include CDN, proxy, bot and corporate gateway details in typed lookup results.
- Hide API keys in request errors and typed bulk error messages.
- Keep the HTTP status when an error response is too large or ends early.
- Reject invalid HTTP headers, malformed JSON text and extra results in typed bulk lookups.
- Fix very long timeouts on Windows and use HTTP/1.1 with libcurl versions affected by the HTTP/2 upload issue.

## 1.0.0

- First C++ SDK release for the IPGeolocation.io IP Location API.
Expand Down
11 changes: 10 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16)

project(ipgeolocation_cpp_sdk VERSION 1.0.0 LANGUAGES CXX)
project(ipgeolocation_cpp_sdk VERSION 1.0.1 LANGUAGES CXX)

option(IPGEOLOCATION_BUILD_TESTS "Build the C++ SDK unit tests" ON)
option(IPGEOLOCATION_ENABLE_COVERAGE "Enable LLVM source-based coverage instrumentation" OFF)
Expand Down Expand Up @@ -129,6 +129,9 @@ if (IPGEOLOCATION_BUILD_TESTS)
target_compile_features(ipgeolocation_tests PRIVATE cxx_std_17)
target_include_directories(ipgeolocation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(ipgeolocation_tests PRIVATE ipgeolocation_test_support)
if (WIN32)
target_link_libraries(ipgeolocation_tests PRIVATE ws2_32)
endif ()
add_test(NAME ipgeolocation_tests COMMAND ipgeolocation_tests)

add_executable(ipgeolocation_live_integration_tests
Expand All @@ -155,6 +158,12 @@ if (IPGEOLOCATION_BUILD_TESTS)
SKIP_RETURN_CODE 77
)

if (MSVC)
foreach(test_target ipgeolocation_tests ipgeolocation_live_integration_tests ipgeolocation_live_field_parity_tests)
target_compile_options(${test_target} PRIVATE /utf-8)
endforeach()
endif ()

if (IPGEOLOCATION_ENABLE_COVERAGE)
if (APPLE)
find_program(IPGEOLOCATION_XCRUN_EXECUTABLE xcrun REQUIRED)
Expand Down
52 changes: 13 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Look up IPv4, IPv6, and domains with `/v3/ipgeo` and `/v3/ipgeo-bulk`. Get geolo

Use the SDK from CMake in one of these ways.

The SDK requires `libcurl` with HTTP(S) support. The provided CMake build locates it with `find_package(CURL REQUIRED)`.
The SDK requires `libcurl` with HTTP(S) support. Use libcurl 8.23.0 or newer, or a version patched by your operating system. The provided CMake build locates it with `find_package(CURL REQUIRED)`.

### Install with `vcpkg`

Expand Down Expand Up @@ -142,7 +142,7 @@ Leave `request.ip` unset to resolve the caller IP.
| Supported Endpoints | `/v3/ipgeo`, `/v3/ipgeo-bulk` |
| Supported Inputs | IPv4, IPv6, domain |
| Main Data Returned | Geolocation, company, ASN, time zone, network, hostname, abuse, user-agent, currency, security |
| Authentication | API key, request-origin auth for `/v3/ipgeo` only |
| Authentication | API key |
| Response Formats | Structured JSON, raw JSON, raw XML |
| Bulk Limit | Up to 50,000 IPs or domains per request |
| Transport | `libcurl` |
Expand All @@ -157,7 +157,7 @@ Create an IPGeolocation.io account and copy an API key from your dashboard.
4. Open your dashboard: <https://app.ipgeolocation.io/dashboard>
5. Copy an API key from the `API Keys` section

For server-side C++ code, keep the API key in an environment variable or secret manager. For browser-based single lookups on paid plans, use request-origin auth instead of exposing an API key in frontend code.
Keep your IPGeolocation.io API key in an environment variable or secret manager.

## Authentication

Expand All @@ -172,22 +172,7 @@ if (const char* api_key = std::getenv("IPGEO_API_KEY"); api_key != nullptr) {
ipgeolocation::IpGeolocationClient client(config);
```

### Request-Origin Auth

```cpp
ipgeolocation::IpGeolocationClientConfig config;
config.request_origin = "https://app.example.com";

ipgeolocation::IpGeolocationClient client(config);
```

`request_origin` must be an absolute `http` or `https` origin with no path, query string, fragment, or userinfo.

> [!IMPORTANT]
> Request-origin auth does not work with `/v3/ipgeo-bulk`. Bulk lookup always requires `api_key`.

> [!NOTE]
> If you set both `api_key` and `request_origin`, single lookup still uses the API key. The API key is sent as the `apiKey` query parameter, so avoid logging full request URLs.
The API key is sent as the `apiKey` query parameter, so avoid logging full request URLs.

## Plan Behavior

Expand All @@ -199,7 +184,6 @@ Feature availability depends on your plan and request parameters.
| Domain lookup | Not supported | Supported |
| Bulk lookup | Not supported | Supported |
| Non-English `lang` | Not supported | Supported |
| Request-origin auth | Not supported | Supported for `/v3/ipgeo` only |
| Optional modules via `include` | Not supported | Supported |
| `include = {"*"}` | Base response only | All plan-available modules |

Expand All @@ -209,20 +193,19 @@ Paid plans still need `include` for optional modules. `fields` and `excludes` on

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `api_key` | `std::optional<std::string>` | unset | Required for bulk lookup. Optional for single lookup if `request_origin` is set. |
| `request_origin` | `std::optional<std::string>` | unset | Must be an absolute `http` or `https` origin with no path, query string, fragment, or userinfo. |
| `api_key` | `std::optional<std::string>` | unset | Your IPGeolocation.io API key. |
| `base_url` | `std::string` | `https://api.ipgeolocation.io` | Override the API base URL. |
| `connect_timeout` | `std::chrono::milliseconds` | `10000ms` | Time to open the connection. Must be greater than zero and must be less than or equal to `read_timeout`. |
| `read_timeout` | `std::chrono::milliseconds` | `30000ms` | Time to wait while reading the response body. Must be greater than zero and must be greater than or equal to `connect_timeout`. |
| `max_response_body_chars` | `std::size_t` | `33554432` | Maximum response body size the SDK will buffer before failing the request. Must be greater than zero. |
| `read_timeout` | `std::chrono::milliseconds` | `30000ms` | Maximum time for the whole request, including the connection and the response. Must be greater than zero and at least as long as `connect_timeout`. |
| `max_response_body_chars` | `std::size_t` | `33554432` | Largest response to read, in bytes. Must be greater than zero. |

Config values are validated when the client is created. Request values are validated before each request is sent.

Request methods are safe to call concurrently on a shared client instance. `Close()` is idempotent, but call it only when no other thread is actively issuing requests.

Typed JSON parsing rejects payloads nested deeper than `256` levels.

The transport buffers up to `max_response_body_chars` bytes of response body data before failing with `response body exceeded maxResponseBodyChars`.
If a successful response is too large, the SDK throws `TransportError`. If an API error is too large, you still get its HTTP status.

## Available Methods

Expand All @@ -244,7 +227,7 @@ The transport buffers up to `max_response_body_chars` bytes of response body dat
|-------|------------|-------|
| `ip` | Single lookup | IPv4, IPv6, or domain. Leave it unset for caller IP lookup. |
| `ips` | Bulk lookup | Collection of 1 to 50,000 IPs or domains. |
| `lang` | Single and bulk | One of `Language::kEn`, `kDe`, `kRu`, `kJa`, `kFr`, `kCn`, `kEs`, `kCs`, `kIt`, `kKo`, `kFa`, or `kPt`. |
| `lang` | Single and bulk | One of `Language::kEn`, `kDe`, `kRu`, `kJa`, `kFr`, `kCn`, `kEs`, `kCs`, `kIt`, `kKo`, `kFa`, `kPt`, or `kAr` (Arabic). |
| `include` | Single and bulk | Collection of module names such as `security`, `abuse`, `user_agent`, `hostname`, `liveHostname`, `hostnameFallbackLive`, `geo_accuracy`, `dma_code`, or `*`. |
| `fields` | Single and bulk | Collection of field paths to keep, for example `location.country_name` or `security.threat_score`. |
| `excludes` | Single and bulk | Collection of field paths to remove from the response. |
Expand Down Expand Up @@ -416,14 +399,14 @@ Header names are preserved in the case the server returned them. Response header

The SDK throws exceptions derived from `IpGeolocationError`:

- `ValidationError`: invalid config, invalid request, blank headers, bad origin, or missing auth
- `ValidationError`: invalid settings, invalid request, invalid headers, or missing credentials
- `ClientClosedError`: method called after `Close()`
- `TransportError`: `libcurl` transport failure
- `RequestTimeoutError`: connection or read timeout
- `SerializationError`: invalid JSON in typed methods
- `ApiError`: non-2xx API response, including HTTP status code and response body

`ApiError` exposes `status_code()` and `response_body()`. Its `what()` message is taken from the API's JSON error body when present. If the body is not parseable JSON, the message falls back to the raw body or a status-based string.
`ApiError` gives you the HTTP status, the error message and the response body. If an error includes your API key, the SDK hides it. This also applies to errors in typed bulk results. Raw bulk lookups return the API response as received. If the error body is too large or the connection ends before it arrives in full, you still get the HTTP status.

Example:

Expand All @@ -447,13 +430,9 @@ try {

## Troubleshooting

### `bulk lookup requires apiKey in client config`

Bulk lookup does not support request-origin auth. Set `api_key` in the client config.

### `requestOrigin must not include a path`
### Missing API key

Use an origin only, such as `https://app.example.com`. Do not include `/path`, query strings, fragments, or userinfo.
Set `api_key` before making a lookup.

### `headers must not contain CR or LF`

Expand All @@ -477,11 +456,6 @@ Response fields are `std::optional` so omitted fields stay omitted. Check `has_v

## Frequently Asked Questions

<details>
<summary>Can I use this SDK without an API key?</summary>
Only for single lookup with paid-plan request-origin auth. Bulk lookup always requires an API key.
</details>

<details>
<summary>Can I request XML and still get typed models?</summary>
No. Typed methods only support JSON. Use `LookupIpGeolocationRaw` or `BulkLookupIpGeolocationRaw` for XML.
Expand Down
2 changes: 1 addition & 1 deletion conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

class IpGeolocationCppSdkConan(ConanFile):
name = "ipgeolocation-cpp-sdk"
version = "1.0.0"
version = "1.0.1"
license = "MIT"
url = "https://github.com/IPGeolocation/ip-geolocation-api-cpp-sdk"
homepage = "https://github.com/IPGeolocation/ip-geolocation-api-cpp-sdk"
Expand Down
1 change: 1 addition & 0 deletions include/ipgeolocation/language.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum class Language {
kKo,
kFa,
kPt,
kAr,
};

} // namespace ipgeolocation
70 changes: 70 additions & 0 deletions include/ipgeolocation/models.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#pragma once

#include <cstddef>
#include <optional>
#include <string>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>

#include "ipgeolocation/visibility.hpp"
Expand Down Expand Up @@ -49,6 +53,8 @@ struct IPGEOLOCATION_API Network {
std::optional<std::string> connection_type;
std::optional<std::string> route;
std::optional<bool> is_anycast;
std::optional<bool> is_cdn{};
std::optional<std::string> cdn_provider_name{};
};

struct IPGEOLOCATION_API Asn {
Expand Down Expand Up @@ -115,6 +121,17 @@ struct IPGEOLOCATION_API Security {
std::optional<bool> is_spam;
std::optional<bool> is_cloud_provider;
std::optional<std::string> cloud_provider_name;
std::optional<bool> is_known_good_bot{};
std::optional<bool> is_corporate_gateway{};
std::optional<double> proxy_days_seen_30d{};
std::optional<double> proxy_days_seen_7d{};
std::optional<double> proxy_provider_count_30d{};
std::optional<double> bot_confidence_score{};
std::optional<std::string> bot_operator_name{};
std::optional<std::string> bot_type{};
std::optional<std::string> bot_last_seen{};
std::optional<std::string> corporate_gateway_type{};
std::optional<std::string> corporate_gateway_provider_name{};
};

struct IPGEOLOCATION_API Abuse {
Expand Down Expand Up @@ -190,4 +207,57 @@ struct IPGEOLOCATION_API BulkLookupResult {
}
};

namespace internal {

template <typename Model>
struct BindingMembers;

template <>
struct BindingMembers<Network> {
inline static constexpr auto fields =
std::make_tuple(&Network::connection_type, &Network::route, &Network::is_anycast);
};

template <>
struct BindingMembers<Security> {
inline static constexpr auto fields = std::make_tuple(
&Security::threat_score, &Security::is_tor, &Security::is_proxy, &Security::proxy_provider_names,
&Security::proxy_confidence_score, &Security::proxy_last_seen, &Security::is_residential_proxy,
&Security::is_vpn, &Security::vpn_provider_names, &Security::vpn_confidence_score, &Security::vpn_last_seen,
&Security::is_relay, &Security::relay_provider_name, &Security::is_anonymous, &Security::is_known_attacker,
&Security::is_bot, &Security::is_spam, &Security::is_cloud_provider, &Security::cloud_provider_name);
};

}

template <std::size_t I, typename Model,
std::enable_if_t<std::is_same_v<std::decay_t<Model>, Network> ||
std::is_same_v<std::decay_t<Model>, Security>, int> = 0>
constexpr decltype(auto) get(Model&& value) noexcept {
constexpr auto field = std::get<I>(internal::BindingMembers<std::decay_t<Model>>::fields);
return (std::forward<Model>(value).*field);
}

} // namespace ipgeolocation

namespace std {

template <>
struct tuple_size<ipgeolocation::Network>
: tuple_size<decltype(ipgeolocation::internal::BindingMembers<ipgeolocation::Network>::fields)> {};

template <>
struct tuple_size<ipgeolocation::Security>
: tuple_size<decltype(ipgeolocation::internal::BindingMembers<ipgeolocation::Security>::fields)> {};

template <size_t I>
struct tuple_element<I, ipgeolocation::Network> {
using type = remove_reference_t<decltype(ipgeolocation::get<I>(declval<ipgeolocation::Network&>()))>;
};

template <size_t I>
struct tuple_element<I, ipgeolocation::Security> {
using type = remove_reference_t<decltype(ipgeolocation::get<I>(declval<ipgeolocation::Security&>()))>;
};

}
2 changes: 1 addition & 1 deletion include/ipgeolocation/version.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

namespace ipgeolocation {

inline constexpr const char* VERSION = "1.0.0";
inline constexpr const char* VERSION = "1.0.1";

} // namespace ipgeolocation
10 changes: 5 additions & 5 deletions src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ ApiResponse<IpGeolocationResponse> IpGeolocationClient::LookupIpGeolocation(
const auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(finished_at - started_at).count();

if (response.status_code / 100 != 2) {
throw internal::ToApiError(response.status_code, response.body);
throw internal::ToApiError(response.status_code, response.body, impl_->config.api_key);
}

ApiResponse<IpGeolocationResponse> result;
Expand Down Expand Up @@ -72,7 +72,7 @@ ApiResponse<std::string> IpGeolocationClient::LookupIpGeolocationRaw(
const auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(finished_at - started_at).count();

if (response.status_code / 100 != 2) {
throw internal::ToApiError(response.status_code, response.body);
throw internal::ToApiError(response.status_code, response.body, impl_->config.api_key);
}

ApiResponse<std::string> result;
Expand Down Expand Up @@ -107,11 +107,11 @@ ApiResponse<std::vector<BulkLookupResult>> IpGeolocationClient::BulkLookupIpGeol
const auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(finished_at - started_at).count();

if (response.status_code / 100 != 2) {
throw internal::ToApiError(response.status_code, response.body);
throw internal::ToApiError(response.status_code, response.body, impl_->config.api_key);
}

ApiResponse<std::vector<BulkLookupResult>> result;
result.data = internal::ParseBulkLookupResults(response.body);
result.data = internal::ParseBulkLookupResults(response.body, normalized.ips.size(), impl_->config.api_key);
result.metadata = internal::ToMetadata(response.status_code, duration_ms, response.headers);
return result;
}
Expand All @@ -138,7 +138,7 @@ ApiResponse<std::string> IpGeolocationClient::BulkLookupIpGeolocationRaw(
const auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(finished_at - started_at).count();

if (response.status_code / 100 != 2) {
throw internal::ToApiError(response.status_code, response.body);
throw internal::ToApiError(response.status_code, response.body, impl_->config.api_key);
}

ApiResponse<std::string> result;
Expand Down
Loading
Loading