RIPE NCC RIS from PHP.
The same shape as the Prefix WhoIs client, pointed at RIPE NCC's Routing Information Service: BGP tables collected from route collectors around the world. One file, no external software dependencies. Look up one address, thousands in bulk, every route covering an address, or every prefix an AS announces, and get arrays keyed by the service's own field names. Duplicates are collapsed and an optional 24-hour cache remembers every answer, so a shared public service is asked no more than it needs to be.
Six functions, one include.
<?phprequire 'ris.php';// One address: the most specific route RIS sees, or null if nothing covers it.$r = ris_lookup('4.2.2.1');echo "AS{$r['origin-as']} announces {$r['prefix']}, seen by {$r['num-rispeers']} RIS peers\n";// Every covering route, most specific first: the /24, then the /12 and /9 above it.foreach (ris_lookup_all('8.8.8.8') as $route) { echo str_pad($route['prefix'], 20), "AS{$route['origin-as']}\n";}// Many addresses over one connection (up to 500 per batch; larger lists are batched).// Duplicates are collapsed, invalid or unannounced entries come back as null, IPv6 is fine.$log = ['4.2.2.1', '2001:4860:4860::8888', '8.8.8.8', '192.0.2.1', '8.8.8.8'];foreach (ris_lookup_bulk($log) as $ip => $rec) { echo str_pad($ip, 24), $rec ? "AS{$rec['origin-as']} {$rec['prefix']}" : 'not seen', "\n";}// Everything an AS announces, IPv4 and IPv6.$pfx = ris_prefixes('AS15169');echo count($pfx), " prefixes\n";// Optional: a 24-hour cache on disk, so an address is never asked about twice in a day.ris_cache_enable(); // SQLite file in the system temp dir by defaultris_lookup_bulk($log); // served from the cache: no round tripecho ris_usage(), " queries sent by this process today\n";
origin and all of them in origins. null when nothing covers the address.null. Answers are matched to questions by prefix containment, never by position, so one missing answer cannot shift the rest. Afterwards $options['cached'] and $options['errors'] tell you what happened.15169 or 'AS15169'.ris_cache_stats() reports the backend, hits and misses; ris_cache_disable() turns it off.The 2005 entry point doRISLookupBulk() still works and keeps its original return shape, so existing code needs no changes; new code should use the functions above.
What comes back
Run against the live service 0s ago. ris_lookup('4.2.2.1') returns:
{
"ip": "4.2.2.1",
"route": "4.0.0.0/9",
"origin": "AS3356",
"descr": "LEVEL3 - Level 3 Parent, LLC, US",
"lastupd-frst": "2026-05-12 11:45Z 31.169.49.228@rrc00",
"lastupd-last": "2026-10-13 14:34Z 102.208.105.2@rrc00",
"seen-at": [
"rrc00",
"rrc01",
"rrc03",
"rrc04",
"rrc05",
"rrc06",
"rrc07",
"rrc10",
"rrc11",
"rrc12",
"rrc13",
"rrc14",
"rrc15",
"rrc16",
"rrc18",
"rrc19",
"rrc20",
"rrc21",
"rrc22",
"rrc23",
"rrc24",
"rrc25",
"rrc26"
],
"num-rispeers": 336,
"source": "RISWHOIS",
"prefix": "4.0.0.0/9",
"origin-as": 3356,
"origins": [
"AS3356"
]
}Every route covering 8.8.8.8, most specific first:
8.8.8.0/24 AS15169 GOOGLE - Google LLC, US 8.0.0.0/12 AS3356 LEVEL3 - Level 3 Parent, LLC, US 8.0.0.0/9 AS3356 LEVEL3 - Level 3 Parent, LLC, US
The bulk call with a duplicate, an IPv6 address and an address nobody announces:
4.2.2.1 AS3356 4.0.0.0/9 336 peers 2001:4860:4860::8888 AS15169 2001:4860::/32 362 peers 8.8.8.8 AS15169 8.8.8.0/24 375 peers 192.0.2.1 not seen
ris_prefixes('AS15169') returned 1,425 prefixes; the first five:
2a00:1450:400a::/48 peers 2a00:1450:400b::/48 peers 2a00:1450:400c::/48 peers 2a00:1450:400d::/48 peers 2a00:1450:400e::/48 peers
Keys in every record
ipThe address asked about, in canonical form.prefixThe covering route as announced, IPv4 or IPv6. The service's own route or route6 key is kept alongside it.origin, origin-as, originsThe announcing AS as AS15169, as the number 15169, and, when more than one AS announces the prefix, all of them.descrThe AS name and holder as RIS records it.lastupd-frst, lastupd-lastWhen the route was first and most recently seen to change, with the peer and collector that reported it.seen-atThe route collectors currently seeing the route, as a list.num-rispeersHow many RIS peers carry the route: a rough measure of how widely it is propagated.sourceAlways RISWHOIS.RIS answers what the collectors see: a route's presence, origin and reach. For who holds the address block, its registered network and organization, and location, pair it with the Prefix WhoIs client.
ris.php
<?php/** * RIPE NCC RIS whois client library for PHP * * A small, dependency-free client for RIPE NCC's Routing Information Service * (riswhois.ripe.net): the origin AS and prefix for one address, for many addresses in * one round trip, every covering route of an address, or every prefix an AS announces. * Answers come back as arrays keyed by the field names the service uses, lower-cased * ("origin", "descr", "lastupd-last", "num-rispeers", ...), plus a few normalized keys * ("prefix", "origin-as", "seen-at" as a list). * * require 'ris.php'; * * $one = ris_lookup('4.2.2.1'); // ?array, the most specific route * $all = ris_lookup_all('8.8.8.8'); // every covering route, most specific first * $many = ris_lookup_bulk(['4.2.2.1', '2001:4860:4860::8888', '10.0.0.1']); * // => ['4.2.2.1' => [...], '2001:4860:4860::8888' => [...], '10.0.0.1' => null] * $pfx = ris_prefixes('AS15169'); // every prefix RIS sees from that origin * * ris_cache_enable(); // optional: 24-hour LRU cache (SQLite, built into PHP) * * RIS is a public service run by the RIPE NCC and publishes no per-address allowance; * the library still never asks the same question twice within a batch and, with the * cache enabled, not twice in a day, out of courtesy to a shared resource. Requires * PHP 7.4 or later (8.x recommended) with stream sockets; nothing else. * * Copyright (c) 2005-2026 VOSTROM Holdings, Inc. Released under the same open source * license as the Prefix WhoIs software; see https://pwhois.org/license.who */declare(strict_types=1);const RIS_SERVER = 'riswhois.ripe.net';const RIS_PORT = 43;const RIS_BULK_MAX = 500; // addresses per persistent connectionconst RIS_CONNECT_SECS = 10; // connect timeoutconst RIS_READ_SECS = 30; // overall read deadline per round tripconst RIS_MAX_BYTES = 16 << 20; // 16 MB answer cap per round trip (an AS with thousands of prefixes is large)/** * The most specific route RIS sees covering one IPv4 or IPv6 address. * * Returns the record as [field => value], or null when the address is invalid, the * service could not be reached, or no collector sees a covering route. When several * origins announce the same prefix the first is in 'origin' and all of them in 'origins'. * $options: 'server', 'port'. */function ris_lookup(string $address, array $options = []): ?array{ $ip = ris_normalize_ip($address); if ($ip === null) return null; $hit = ris_cache_get('l:' . $ip); if ($hit !== false) return $hit; $raw = ris_transact('-l ' . $ip . "\n", $options); if ($raw === null) return null; ris_usage_count(1); $rec = ris_best_for($ip, ris_parse($raw)); ris_cache_put('l:' . $ip, $rec); return $rec;}/** * Every route RIS sees covering the address, most specific first: the announced /24 * as well as the /12 and /9 above it, for example. Returns a list of records, an * empty list when nothing covers the address, or null on a transport failure. */function ris_lookup_all(string $address, array $options = []): ?array{ $ip = ris_normalize_ip($address); if ($ip === null) return null; $hit = ris_cache_get('L:' . $ip); if ($hit !== false) return $hit; $raw = ris_transact('-L ' . $ip . "\n", $options); if ($raw === null) return null; ris_usage_count(1); $list = []; foreach (ris_merge_moas(ris_parse($raw)) as $rec) { $rec['ip'] = $ip; $list[] = $rec; } usort($list, static fn(array $a, array $b) => ris_prefix_len($b['prefix']) <=> ris_prefix_len($a['prefix'])); ris_cache_put('L:' . $ip, $list); return $list;}/** * Look up many addresses over one persistent connection per 500. Duplicates are * collapsed, invalid entries are reported as null, and the result is keyed by the * address exactly as you passed it. Each address gets its most specific covering route. * * Returns [address => record|null]. Returns null only when no round trip could be * completed at all; a partial failure leaves the missing addresses null and sets * $options['errors'] (by reference) with a message per batch. $options['cached'] tells * you how many answers came from the cache. */function ris_lookup_bulk(array $addresses, ?array &$options = null): ?array{ $options = $options ?? []; $result = []; $byIp = []; $order = []; foreach ($addresses as $a) { if (!is_string($a) && !is_int($a)) continue; $key = (string) $a; $ip = ris_normalize_ip($key); $result[$key] = null; if ($ip === null) continue; if (!isset($byIp[$ip])) { $byIp[$ip] = []; $order[] = $ip; } $byIp[$ip][] = $key; } if (!$order) return $result; $pending = []; foreach ($order as $ip) { $hit = ris_cache_get('l:' . $ip); if ($hit !== false) { foreach ($byIp[$ip] as $key) $result[$key] = $hit; } else $pending[] = $ip; } $options['cached'] = count($order) - count($pending); $errors = []; $completed = 0; if (!$pending) { $options['errors'] = []; return $result; } foreach (array_chunk($pending, (int) ($options['batch'] ?? RIS_BULK_MAX)) as $n => $chunk) { $req = "-k\n"; foreach ($chunk as $ip) $req .= '-l ' . $ip . "\n"; $req .= "-k\n"; $raw = ris_transact($req, $options); if ($raw === null) { $errors[] = 'batch ' . ($n + 1) . ': no answer from the service'; continue; } $completed++; ris_usage_count(count($chunk)); // Answers are matched to questions by containment, never by position, so a // missing or multi-record answer can never shift the ones after it. $records = ris_parse($raw); foreach ($chunk as $ip) { $rec = ris_best_for($ip, $records); foreach ($byIp[$ip] as $key) $result[$key] = $rec; ris_cache_put('l:' . $ip, $rec); } } $options['errors'] = $errors; return $completed === 0 ? null : $result;}/** * Every prefix RIS currently sees originated by an AS, IPv4 and IPv6, as a list of * ['prefix' => '8.8.8.0/24', 'origin-as' => 15169, 'num-rispeers' => 375] sorted by * prefix. Accepts 15169 or 'AS15169'. Returns an empty list for an AS that originates * nothing, or null on a transport failure. */function ris_prefixes($asn, array $options = []): ?array{ $n = (int) preg_replace('/^\s*as/i', '', trim((string) $asn)); if ($n <= 0 || $n > 4294967295) return null; $hit = ris_cache_get('as:' . $n); if ($hit !== false) return $hit; $raw = ris_transact('-F -i AS' . $n . "\n", $options); if ($raw === null) return null; ris_usage_count(1); $list = []; foreach (preg_split('/\r?\n/', $raw) ?: [] as $line) { if ($line === '' || $line[0] === '%') continue; $f = preg_split('/\t+/', trim($line)); if (count($f) < 2 || !ris_valid_prefix($f[1])) continue; $list[] = ['prefix' => $f[1], 'origin-as' => (int) $f[0], 'num-rispeers' => isset($f[2]) ? (int) $f[2] : null]; } usort($list, static fn(array $a, array $b) => strnatcmp($a['prefix'], $b['prefix'])); ris_cache_put('as:' . $n, $list); return $list;}/* --------------------------------------------------------------------------------- Parsing and matching --------------------------------------------------------------------------------- *//** Canonical text of a valid IPv4/IPv6 address, or null. A trailing /bits is dropped. */function ris_normalize_ip(string $s): ?string{ $s = trim($s); if ($s === '') return null; $s = (string) preg_replace('~/\d{1,3}$~', '', $s); if (!filter_var($s, FILTER_VALIDATE_IP)) return null; $bin = @inet_pton($s); return $bin === false ? null : (string) inet_ntop($bin);}function ris_valid_prefix(string $p): bool{ $parts = explode('/', $p, 2); if (count($parts) !== 2 || !ctype_digit($parts[1]) || !filter_var($parts[0], FILTER_VALIDATE_IP)) return false; return (int) $parts[1] <= (strpos($parts[0], ':') !== false ? 128 : 32);}function ris_prefix_len(string $p): int { return (int) (explode('/', $p, 2)[1] ?? 0); }/** True when the binary address falls inside the CIDR prefix (same family required). */function ris_prefix_contains(string $prefix, string $ipBin): bool{ [$net, $len] = explode('/', $prefix, 2) + [1 => '0']; $nb = @inet_pton($net); $len = (int) $len; if ($nb === false || strlen($nb) !== strlen($ipBin)) return false; $bytes = intdiv($len, 8); $bits = $len % 8; if ($bytes && substr($nb, 0, $bytes) !== substr($ipBin, 0, $bytes)) return false; if ($bits === 0) return true; $mask = (0xff << (8 - $bits)) & 0xff; return (ord($nb[$bytes]) & $mask) === (ord($ipBin[$bytes]) & $mask);}/** * Parse RPSL-style "field: value" text into records. Lines beginning with % are * notices from the service and are skipped; a blank line or a new route/route6 line * ends a record. Field names are lower-cased. Adds 'prefix' (route or route6), * 'origin-as' (int) and turns 'seen-at' into a list and 'num-rispeers' into an int. */function ris_parse(string $raw): array{ $records = []; $cur = []; $flush = static function () use (&$records, &$cur): void { if (!$cur) return; $cur['prefix'] = $cur['route'] ?? $cur['route6'] ?? null; if ($cur['prefix'] !== null && ris_valid_prefix($cur['prefix'])) { $cur['origin-as'] = isset($cur['origin']) ? (int) preg_replace('/\D/', '', $cur['origin']) : null; if (isset($cur['seen-at'])) $cur['seen-at'] = array_values(array_filter(array_map('trim', explode(',', $cur['seen-at'])), 'strlen')); if (isset($cur['num-rispeers'])) $cur['num-rispeers'] = (int) $cur['num-rispeers']; $records[] = $cur; } $cur = []; }; foreach (preg_split('/\r?\n/', $raw) ?: [] as $line) { if (trim($line) === '' || $line[0] === '%') { $flush(); continue; } $p = strpos($line, ':'); if ($p === false) continue; $key = strtolower(trim(substr($line, 0, $p))); if ($key === '' || !preg_match('/^[a-z0-9_-]+$/', $key)) continue; if (($key === 'route' || $key === 'route6') && $cur) $flush(); $val = trim(substr($line, $p + 1)); $val = (string) preg_replace('/[^\P{C}]/u', '', $val); $cur[$key] = $val === '' ? null : $val; } $flush(); return $records;}/** Collapse records that share a prefix (several origins announcing it) into one, with 'origins'. */function ris_merge_moas(array $records): array{ $out = []; foreach ($records as $rec) { $k = $rec['prefix']; if (isset($out[$k])) { $out[$k]['origins'][] = $rec['origin']; continue; } $rec['origins'] = [$rec['origin']]; $out[$k] = $rec; } return array_values($out);}/** The most specific parsed record whose prefix contains the address, tagged with 'ip'; null if none. */function ris_best_for(string $ip, array $records): ?array{ $bin = (string) inet_pton($ip); $best = null; $bestLen = -1; foreach (ris_merge_moas($records) as $rec) { if (!ris_prefix_contains($rec['prefix'], $bin)) continue; $len = ris_prefix_len($rec['prefix']); if ($len > $bestLen) { $best = $rec; $bestLen = $len; } } if ($best === null) return null; return ['ip' => $ip] + $best;}/** One connection, one request, the whole answer. Returns the raw text or null. */function ris_transact(string $request, array $options = []): ?string{ $host = (string) ($options['server'] ?? RIS_SERVER); $port = (int) ($options['port'] ?? RIS_PORT); $addr = filter_var($host, FILTER_VALIDATE_IP) ? $host : gethostbyname($host); if (!filter_var($addr, FILTER_VALIDATE_IP)) return null; $sock = @stream_socket_client('tcp://' . $addr . ':' . $port, $errno, $errstr, RIS_CONNECT_SECS); if (!$sock) return null; stream_set_timeout($sock, RIS_READ_SECS); if (@fwrite($sock, $request) === false) { fclose($sock); return null; } $buf = ''; $deadline = microtime(true) + RIS_READ_SECS; while (!feof($sock) && microtime(true) < $deadline) { $chunk = fread($sock, 65536); if ($chunk === false) break; if ($chunk === '') { $meta = stream_get_meta_data($sock); if (!empty($meta['timed_out'])) break; continue; } $buf .= $chunk; if (strlen($buf) > RIS_MAX_BYTES) break; } fclose($sock); return $buf === '' ? null : $buf;}/* --------------------------------------------------------------------------------- Optional 24-hour cache. Off until you enable it. Backend: SQLite through PDO (part of PHP itself, nothing to install). Every process and application sharing the file gets real row-level upserts and SQLite's own locking, so concurrent writers are safe; expiry and least-recently-used eviction are indexed columns. Where pdo_sqlite is unavailable, a directory of one small file per key is used instead (atomic renames, no shared write contention). --------------------------------------------------------------------------------- *//** * Enable the cache. $path defaults to <system temp dir>/ris-cache.sqlite; give a * directory (existing, or ending in "/") to force the file-per-key backend. $ttl is the * lifetime of an answer in seconds (a day); $max the number of keys kept. Negative * answers (nothing covers the address) are cached too. */function ris_cache_enable(?string $path = null, int $ttl = 86400, int $max = 20000): void{ $c = &ris_state(); $path = $path ?? rtrim(sys_get_temp_dir(), '/') . '/ris-cache.sqlite'; $useDir = is_dir($path) || substr($path, -1) === '/' || !class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers(), true); if (!$useDir && !is_dir(dirname($path))) @mkdir(dirname($path), 0700, true); $c['cache'] = ['mode' => $useDir ? 'dir' : 'sqlite', 'path' => rtrim($path, '/'), 'ttl' => max(60, $ttl), 'max' => max(100, $max), 'pdo' => null, 'hits' => 0, 'misses' => 0, 'writes' => 0]; if ($useDir && !is_dir($c['cache']['path'])) @mkdir($c['cache']['path'], 0700, true);}function ris_cache_disable(): void { $c = &ris_state(); $c['cache'] = null; }/** Hits, misses, writes and entries so far; null when the cache is off. */function ris_cache_stats(): ?array{ $c = &ris_state(); if (!$c['cache']) return null; $n = 0; if ($c['cache']['mode'] === 'sqlite') { $pdo = ris_cache_pdo(); if ($pdo) { try { $n = (int) $pdo->query('SELECT COUNT(*) FROM ris_cache')->fetchColumn(); } catch (Throwable $e) { } } } else { foreach (new DirectoryIterator($c['cache']['path']) as $f) if ($f->isFile() && substr($f->getFilename(), -5) === '.json') $n++; } return ['backend' => $c['cache']['mode'], 'path' => $c['cache']['path'], 'ttl' => $c['cache']['ttl'], 'entries' => $n, 'hits' => $c['cache']['hits'], 'misses' => $c['cache']['misses'], 'writes' => $c['cache']['writes']];}/** Remove expired rows and, if over $max, the least recently used. Runs automatically about once per 500 writes. */function ris_cache_prune(): void{ $c = &ris_state(); if (!$c['cache']) return; $now = time(); $cut = $now - $c['cache']['ttl']; if ($c['cache']['mode'] === 'sqlite') { $pdo = ris_cache_pdo(); if (!$pdo) return; try { $pdo->prepare('DELETE FROM ris_cache WHERE stored < :cut')->execute([':cut' => $cut]); $n = (int) $pdo->query('SELECT COUNT(*) FROM ris_cache')->fetchColumn(); if ($n > $c['cache']['max']) { $st = $pdo->prepare('DELETE FROM ris_cache WHERE k IN (SELECT k FROM ris_cache ORDER BY used ASC LIMIT :n)'); $st->bindValue(':n', $n - $c['cache']['max'], PDO::PARAM_INT); $st->execute(); } } catch (Throwable $e) { } } else { $files = []; foreach (new DirectoryIterator($c['cache']['path']) as $f) { if (!$f->isFile() || substr($f->getFilename(), -5) !== '.json') continue; $m = $f->getMTime(); if ($m < $cut) @unlink($f->getPathname()); else $files[$f->getPathname()] = $m; } if (count($files) > $c['cache']['max']) { asort($files); foreach (array_slice(array_keys($files), 0, count($files) - $c['cache']['max']) as $fp) @unlink($fp); } }}/** @internal shared state */function &ris_state(): array{ static $state = ['cache' => null, 'usage' => null]; return $state;}/** @internal open (and on first use create) the SQLite cache */function ris_cache_pdo(): ?PDO{ $c = &ris_state(); if (!$c['cache'] || $c['cache']['mode'] !== 'sqlite') return null; if ($c['cache']['pdo'] instanceof PDO) return $c['cache']['pdo']; try { $pdo = new PDO('sqlite:' . $c['cache']['path'], null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]); $pdo->exec('PRAGMA journal_mode=WAL'); $pdo->exec('PRAGMA synchronous=NORMAL'); $pdo->exec('PRAGMA busy_timeout=5000'); $pdo->exec('CREATE TABLE IF NOT EXISTS ris_cache (k TEXT PRIMARY KEY, stored INTEGER NOT NULL, used INTEGER NOT NULL, rec TEXT)'); $pdo->exec('CREATE INDEX IF NOT EXISTS ris_cache_used ON ris_cache(used)'); $pdo->exec('CREATE INDEX IF NOT EXISTS ris_cache_stored ON ris_cache(stored)'); return $c['cache']['pdo'] = $pdo; } catch (Throwable $e) { $c['cache']['mode'] = 'dir'; $c['cache']['path'] .= '.d'; @mkdir($c['cache']['path'], 0700, true); return null; } // fall back to files}/** @internal file name for an entry in directory mode */function ris_cache_file(string $k): string { $c = &ris_state(); return $c['cache']['path'] . '/' . hash('sha256', $k) . '.json'; }/** Returns the cached value (array or null) or false when absent/expired. */function ris_cache_get(string $k){ $c = &ris_state(); if (!$c['cache']) return false; $now = time(); $cut = $now - $c['cache']['ttl']; if ($c['cache']['mode'] === 'sqlite' && ($pdo = ris_cache_pdo())) { try { $st = $pdo->prepare('SELECT stored, used, rec FROM ris_cache WHERE k = :k'); $st->execute([':k' => $k]); $row = $st->fetch(PDO::FETCH_ASSOC); if (!$row || (int) $row['stored'] < $cut) { $c['cache']['misses']++; return false; } if ($now - (int) $row['used'] > 60) $pdo->prepare('UPDATE ris_cache SET used = :u WHERE k = :k')->execute([':u' => $now, ':k' => $k]); $c['cache']['hits']++; $rec = json_decode((string) $row['rec'], true); return is_array($rec) ? $rec : null; } catch (Throwable $e) { $c['cache']['misses']++; return false; } } $f = ris_cache_file($k); if (!is_file($f) || (int) @filemtime($f) < $cut) { $c['cache']['misses']++; return false; } $j = json_decode((string) @file_get_contents($f), true); if (!is_array($j) || !array_key_exists('r', $j)) { $c['cache']['misses']++; return false; } @touch($f); $c['cache']['hits']++; return $j['r'];}function ris_cache_put(string $k, ?array $val): void{ $c = &ris_state(); if (!$c['cache']) return; $now = time(); $c['cache']['writes']++; if ($c['cache']['mode'] === 'sqlite' && ($pdo = ris_cache_pdo())) { try { $pdo->prepare('INSERT INTO ris_cache (k, stored, used, rec) VALUES (:k, :t, :t, :r) ON CONFLICT(k) DO UPDATE SET stored = :t, used = :t, rec = :r')->execute([':k' => $k, ':t' => $now, ':r' => json_encode($val)]); } catch (Throwable $e) { } } else { $f = ris_cache_file($k); $tmp = $f . '.' . getmypid() . '.tmp'; if (@file_put_contents($tmp, json_encode(['r' => $val])) !== false) @rename($tmp, $f); else @unlink($tmp); } if ($c['cache']['writes'] % 500 === 0) ris_cache_prune();}/* --------------------------------------------------------------------------------- Usage: what this process has asked the service today. RIS publishes no allowance, so this is a courtesy meter rather than a budget. --------------------------------------------------------------------------------- *//** Queries this process has sent today (UTC), counted locally; cache hits are free. */function ris_usage(): int{ $c = &ris_state(); $day = gmdate('Y-m-d'); if (!is_array($c['usage']) || $c['usage']['day'] !== $day) $c['usage'] = ['day' => $day, 'n' => 0]; return (int) $c['usage']['n'];}function ris_usage_count(int $n): void { ris_usage(); $c = &ris_state(); $c['usage']['n'] += $n; }/* --------------------------------------------------------------------------------- Backward compatibility with the 2005 interface (doRISLookupBulk). It returned, per address that had an answer, 'route', 'origin' (digits only), 'descr', 'lastupd-frst', 'lastupd-last', 'seen-at' (comma string), 'num-rispeers' and 'source', and 0 on failure. That shape is kept for existing callers; new code should use ris_*. --------------------------------------------------------------------------------- */if (!function_exists('doRISLookupBulk')) { function doRISLookupBulk($queryarray) { $opts = []; $r = ris_lookup_bulk(is_array($queryarray) ? $queryarray : [], $opts); if ($r === null) return 0; $out = []; foreach ($r as $ip => $rec) { if ($rec === null) continue; $flat = ['route' => (string) $rec['prefix'], 'origin' => (string) ($rec['origin-as'] ?? '')]; foreach (['descr', 'lastupd-frst', 'lastupd-last', 'source'] as $k) if (isset($rec[$k])) $flat[$k] = (string) $rec[$k]; if (isset($rec['seen-at'])) $flat['seen-at'] = implode(',', (array) $rec['seen-at']); if (isset($rec['num-rispeers'])) $flat['num-rispeers'] = (string) $rec['num-rispeers']; $out[$ip] = $flat; } return $out; }}
RIPE NCC's Routing Information Service
RIS collects BGP from more than twenty route collectors at exchange points around the world and answers whois-style questions about what they see. It is run by the RIPE NCC as a public service; this library is our client for it, not the service itself.
No allowance, so behave
RIS publishes no per-address limit. The library collapses duplicates, keeps one connection per batch and, with the cache on, never asks twice in a day. Please keep it that way in your own code.
Open source
Released under the same license as the Prefix WhoIs software. Take it, ship it, tell us what you built.