BoxLang 🚀 A New JVM Dynamic Language Learn More...
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.
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 IPbulk-check - score up to 100 IPs per request (this
client auto-chunks larger lists)health - service probevpn-provider - the aggregated VPN-provider snapshotIt 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.
With CommandBox:
box install iplogs
Or drop IPLogs.cfc into your project and instantiate it directly.
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": ""
]
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 10 | which data sets the offline check consults |
fallbackMap
| see below | per-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")
);
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 & ")");
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>");
}
Probes the service. Not cached.
env = ipLogs.health();
writeOutput(env.success ? "up" : "down");
Returns the aggregated VPN-provider snapshot. Cached. Falls back to
the local vpn-providers data set when the API is unreachable.
env = ipLogs.vpnProvider();
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")
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.
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.
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:
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).// 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 backbone | vpn_detected
| anonymizer
| 0.95 |
| Named VPN provider | vpn_detected
| vpn_provider
| 0.90 |
| Spamhaus DROP, FireHOL Level 1 | suspicious
| threat
| 0.85 |
| Datacenter ASN, AWS, GCP, Cloudflare | suspicious
| datacenter
| 0.50 |
| No match | clean
| (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.
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:
dateTimeFormat masks (BoxLang reads mm as minutes).cacheGet returning null, not cacheKeyExists.java.io.File.lastModified(),
not getFileInfo (which returns a date object on Lucee).^2\d\d rather than
val(statusCode), because val("Connection
Failure") is 0 on ACF/Lucee but 4 on BoxLang.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 2016 | 8810 |
server-cf2018.json
| Adobe ColdFusion 2018 | 8811 |
server-cf2021.json
| Adobe ColdFusion 2021 | 8812 |
server-cf2023.json
| Adobe ColdFusion 2023 | 8813 |
server-cf2025.json
| Adobe ColdFusion 2025 | 8814 |
server-lucee5.json
| Lucee 5 | 8815 |
server-lucee6.json
| Lucee 6 | 8816 |
server-lucee7.json
| Lucee 7 | 8817 |
server-boxlang.json
| BoxLang 1.x | 8818 |
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.
MIT for the code (see LICENSE). The downloadable data
sets are CC-BY 4.0 from ipLogs.com.
$
box install iplogs