BoxLang 🚀 A New JVM Dynamic Language Learn More...

cf-iplogs

v0.1.0 Projects

cf-iplogs

A CFML client for the ipLogs.com IP reputation and VPN-detection API. It runs the same way on Adobe ColdFusion 2016 through 2025, Lucee, and BoxLang. No signup and no API key are required. The library adds optional response caching and can fall back to locally downloaded data sets when the API is unreachable.

What it does

ipLogs.com scores an IP address and tells you whether it looks like a residential connection, a datacenter, or a VPN/proxy exit. This library wraps the four public endpoints:

  • check - score a single IP
  • bulk-check - score up to 100 IPs per request (this client auto-chunks larger lists)
  • health - service probe
  • vpn-provider - the aggregated VPN-provider snapshot

It also downloads the free CC-BY data sets ipLogs publishes (Tor exits, datacenter ASNs, Spamhaus DROP, and others) so you can match an IP offline.

Install

With CommandBox:

box install iplogs

Or drop IPLogs.cfc into your project and instantiate it directly.

Quick start

ipLogs = new IPLogs();

result = ipLogs.check("8.8.8.8");

if (result.success) {
    writeDump(result.data.verdict);   // clean | suspicious | vpn_likely | vpn_detected
} else {
    writeDump(result.error);
}

Every method returns the same normalized envelope so you always know where the answer came from:

[
    "success":    true,                   // did the call succeed
    "statusCode": 200,
    "source":     "api",                  // api | cache | offline
    "cached":     false,
    "fetchedAt":  "2026-05-30T18:00: 00Z", // ISO-8601 UTC
    "data":       { ... },                // the API payload, or the offline verdict
    "error":      ""
]

Configuration

All init() arguments are optional and have sensible defaults.

Argument Default Purpose
apiBaseUrl https://iplogs.com/v1 API root
dataBaseUrl https://iplogs.com/data data set download root
storage cache cache, server, or none
cacheRegion engine default (ipaCache on Lucee)object-cache region for cacheGet/cachePut
cacheTTL 86400 seconds to keep a successful result (24 hours)
offlineFallback true fall back to local data sets when the API fails
datasetDir <cfc dir>/datasets where downloaded data sets live
datasetTTL 86400 seconds before a data set is treated as stale
fallbackDatasets all 10which data sets the offline check consults
fallbackMap see belowper-data-set verdict, flag, score, and confidence
httpTimeout {check:30, bulk:90, health:10, dataset:60} per-endpoint timeouts in seconds
userAgent cf-iplogs/<version> (+github) request User-Agent
throwOnError false throw on failure instead of returning a success=false envelope

Example:

ipLogs = new IPLogs(
    storage = "cache",
    cacheTTL = 86400,
    offlineFallback = true,
    datasetDir = expandPath("/data/iplogs")
);

Methods

check(ip)

Scores a single IP. Validates the address, checks the cache, calls the API, and falls back to a local match if the API is down and offlineFallback is on.

env = ipLogs.check("23.234.89.127");
writeOutput(env.data.verdict & " (score " & env.data.score & ")");

bulkCheck(ips)

Scores an array of IPs. Lists longer than 100 are split into multiple API calls and merged into a single data.results array. This method is not cached.

env = ipLogs.bulkCheck(["8.8.8.8", "1.1.1.1", "23.234.89.127"]);
for (row in env.data.results) {
    writeOutput(row.ip_info.ip & ": " & row.verdict & "<br>");
}

health()

Probes the service. Not cached.

env = ipLogs.health();
writeOutput(env.success ? "up" : "down");

vpnProvider()

Returns the aggregated VPN-provider snapshot. Cached. Falls back to the local vpn-providers data set when the API is unreachable.

env = ipLogs.vpnProvider();

Data set helpers

ipLogs.downloadDataset("tor-exits");    // fetch one data set to datasetDir
ipLogs.refreshDatasets();               // refresh every stale data set
ipLogs.listDatasets();                  // [{name, present, sizeBytes, ageSeconds, stale}, ...]
ipLogs.localCheck("185.220.101.1");     // offline match, returns an envelope (source="offline")

Caching

Set storage at construction:

  • cache uses the engine object cache through cachePut/cacheGet with a named region. On Lucee there is no default object cache, so register one (see below). Adobe CF ships a default; BoxLang core may not, in which case a cache write is a quiet no-op and the next read is a miss.
  • server keeps results in a struct under server.cf_iplogs, guarded by a named lock. This needs no cache configuration, but it is per-JVM and clears on restart.
  • none disables caching.

Only check and vpnProvider results are cached. health and bulkCheck always go to the network. A cache hit comes back with source="cache" and cached=true.

On Lucee, register a RAM cache in Application.cfc so storage="cache" has somewhere to write:

if (structKeyExists(server, "lucee")) {
    this.cache.connections["ipaCache"] = [
        "class":   "lucee.runtime.cache.ram.RamCache",
        "storage": false,
        "custom":  ["timeToLiveSeconds": 0, "timeToIdleSeconds": 0],
        "default": ""
    ];
    this.cache.object = "ipaCache";
}

The library defaults cacheRegion to ipaCache on Lucee to match.

Offline fallback and data sets

When a check fails and offlineFallback is on, the library matches the IP against the data sets in datasetDir and returns a degraded verdict marked source="offline". Stale data sets refresh on lookup; if a refresh fails, the existing copy on disk is used.

Pre-warming the cache

Downloads are lazy. A data set is only fetched the first time the offline path needs it (or when you ask for it explicitly), so datasetDir starts empty and stays empty for as long as the API keeps answering. That is expected, not a failure.

Two consequences worth planning for:

  • The first offline check is the slow one. localCheck consults every set in fallbackDatasets (all 10 by default), so that first call downloads each missing set, including aws-ranges.json at roughly 2.4 MB (about 2.9 MB for the full set). After that, each set is cached on disk and reused until it passes datasetTTL (24 hours).
  • To avoid paying that cost on a live request, pre-warm the cache once at startup or on a schedule:
// fetch every stale or missing set now (e.g. in Application.cfc onApplicationStart)
ipLogs.refreshDatasets();

// or fetch a single set
ipLogs.downloadDataset("tor-exits");

// or narrow the offline footprint so only a few sets are ever downloaded
ipLogs = new IPLogs(fallbackDatasets = ["tor-exits", "spamhaus-drop", "datacenter-asns"]);

refreshDatasets() skips sets that are still fresh, so it is cheap to call on every startup. Pass force=true to re-download regardless of age.

The offline result keeps the API verdict values (clean, suspicious, vpn_likely, vpn_detected) and adds a flags array so you can tell categories apart. The default mapping:

Category Verdict Flag Score
Tor exit, Mullvad relay, residential-proxy backbonevpn_detected anonymizer 0.95
Named VPN providervpn_detected vpn_provider 0.90
Spamhaus DROP, FireHOL Level 1suspicious threat 0.85
Datacenter ASN, AWS, GCP, Cloudflaresuspicious datacenter 0.50
No matchclean (none)0.10

The threat flag is kept separate from VPN and datacenter signals so a known abuser reads differently from plain hosting, even though both map to suspicious.

Tune it without touching the CFC:

// only consult two data sets, and re-score Tor as vpn_likely
ipLogs = new IPLogs(
    fallbackDatasets = ["tor-exits", "spamhaus-drop"],
    fallbackMap = [
        "tor-exits":     ["verdict":"vpn_likely","flag":"tor","score":0.42,"confidence":0.5,"match":"ip"],
        "spamhaus-drop": ["verdict":"suspicious","flag":"threat","score":0.85,"confidence":0.85,"match":"cidr"]
    ]
);

// or override per call
ipLogs.localCheck("185.220.101.1", ["spamhaus-drop"]);

CIDR matching uses java.math.BigInteger, so IPv4 and IPv6 ranges work the same way on every engine.

The data sets are downloaded from ipLogs.com and licensed CC-BY 4.0. Keep the attribution if you redistribute them.

Cross-engine notes

Verified on Adobe ColdFusion 2016 and 2023, Lucee 5, and BoxLang 1.x, with the same source running on CF 2018/2021/2025 and Lucee 6. On BoxLang, install the bx-compat-cfml and bx-esapi modules. A few choices keep behavior consistent across engines:

  • Timestamps are built from component date functions, not dateTimeFormat masks (BoxLang reads mm as minutes).
  • Cache reads use cacheGet returning null, not cacheKeyExists.
  • File mtimes come from java.io.File.lastModified(), not getFileInfo (which returns a date object on Lucee).
  • HTTP status is matched against ^2\d\d rather than val(statusCode), because val("Connection Failure") is 0 on ACF/Lucee but 4 on BoxLang.

Running the tests

There is no TestBox dependency. tests/tests.cfm is a self-contained harness that instantiates the library directly and runs every assertion, so it compiles and passes on all engines from Adobe CF 2016 through BoxLang.

Pick a CommandBox server file for the engine you want and start it:

box install
box server start server-lucee5.json

Each engine has its own file on its own port:

File Engine Port
server-cf2016.json Adobe ColdFusion 20168810
server-cf2018.json Adobe ColdFusion 20188811
server-cf2021.json Adobe ColdFusion 20218812
server-cf2023.json Adobe ColdFusion 20238813
server-cf2025.json Adobe ColdFusion 20258814
server-lucee5.json Lucee 58815
server-lucee6.json Lucee 68816
server-lucee7.json Lucee 78817
server-boxlang.json BoxLang 1.x8818

Then open the test page in a browser for the HTML report:

http://localhost:<port>/tests/tests.cfm

Add ?format=json (or ?json=1) for the raw JSON envelope, handy for scripted runs:

curl "http://localhost:<port>/tests/tests.cfm?format=json"

tests/demo.cfm exercises every method against the bundled fixtures and renders the envelopes.

License

MIT for the code (see LICENSE). The downloadable data sets are CC-BY 4.0 from ipLogs.com.

$ box install iplogs

No collaborators yet.
   
  • {{ getFullDate("2026-06-05T00:42:07Z") }}
  • {{ getFullDate("2026-06-05T00:42:08Z") }}
  • 30
  • 1