hostinfo 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -8
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +127 -0
- package/dist/hints.d.ts +15 -0
- package/dist/hints.js +176 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/locate.d.ts +28 -0
- package/dist/locate.js +59 -0
- package/dist/lookup.d.ts +37 -0
- package/dist/lookup.js +116 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +243 -0
- package/dist/private.d.ts +5 -0
- package/dist/private.js +24 -0
- package/dist/resolve.d.ts +4 -0
- package/dist/resolve.js +13 -0
- package/dist/reverse.d.ts +11 -0
- package/dist/reverse.js +34 -0
- package/dist/trace.d.ts +56 -0
- package/dist/trace.js +146 -0
- package/dist/whois.d.ts +56 -0
- package/dist/whois.js +136 -0
- package/llms.txt +35 -0
- package/package.json +31 -16
- package/index.d.ts +0 -47
- package/index.js +0 -118
- package/index.mjs +0 -4
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/hostinfo)
|
|
5
5
|
[](https://www.npmjs.com/package/hostinfo)
|
|
6
6
|
|
|
7
|
-
Geocode IP addresses to city, country, and coordinates using the free, community-built [hostip.info](https://www.hostip.info/) API. Zero dependencies.
|
|
7
|
+
Geocode IP addresses to city, country, and coordinates using the free, community-built [hostip.info](https://www.hostip.info/) API. When hostip.info has no answer, it can fall back to reverse DNS and traceroute. Zero dependencies.
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
10
|
import { lookup } from 'hostinfo';
|
|
@@ -26,30 +26,171 @@ const info = await lookup('8.8.8.8');
|
|
|
26
26
|
npm install hostinfo
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Requires Node.js
|
|
29
|
+
Requires Node.js 22.12 or newer. Written in TypeScript and published as ESM with bundled types; CommonJS callers can still `require('hostinfo')` thanks to Node's built-in `require(esm)` support.
|
|
30
30
|
|
|
31
31
|
## API
|
|
32
32
|
|
|
33
33
|
### `lookup(ip?, options?) → Promise<HostInfo>`
|
|
34
34
|
|
|
35
|
-
Looks up an IPv4 address. Omit `ip` to geocode the caller's own public address:
|
|
35
|
+
Looks up an IPv4 address (anything else rejects with a `TypeError`, since hostip.info has no IPv6 data). Omit `ip` to geocode the caller's own public address:
|
|
36
36
|
|
|
37
37
|
```js
|
|
38
38
|
const whereAmI = await lookup();
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Fields that hostip.info does not know are `null
|
|
41
|
+
Fields that hostip.info does not know are `null`, including `countryCode` (the API's `"XX"` placeholder becomes `null`). Coordinates are only present for IPs mapped to a city. Private addresses (see [`isPrivateIPv4`](#isprivateipv4ip--boolean)) resolve to all-`null` fields immediately, without a request.
|
|
42
42
|
|
|
43
43
|
**Options**
|
|
44
44
|
|
|
45
45
|
| option | default | |
|
|
46
46
|
| --- | --- | --- |
|
|
47
47
|
| `timeout` | `10000` | Milliseconds before the request aborts. `0` disables. |
|
|
48
|
-
| `signal` | – | Your own `AbortSignal`;
|
|
48
|
+
| `signal` | – | Your own `AbortSignal`; combined with `timeout`, whichever fires first aborts. |
|
|
49
49
|
| `endpoint` | `https://api.hostip.info/` | Alternate API base URL. |
|
|
50
50
|
|
|
51
51
|
Failures (network, HTTP status, unparseable response) reject with a `HostInfoError`; network errors keep the underlying error on `.cause`.
|
|
52
52
|
|
|
53
|
+
### `locate(ip, options?) → Promise<Location>`
|
|
54
|
+
|
|
55
|
+
Best-effort geolocation when hostip.info comes up empty. It tries, in order:
|
|
56
|
+
|
|
57
|
+
1. **hostip.info** — same as `lookup()`.
|
|
58
|
+
2. **Reverse DNS** — network operators usually put a location code in router and server hostnames (`108-254-2-1.lightspeed.hstntx.sbcglobal.net` → Houston, `ae-5.r21.lsanca07.us.bb.gin.ntt.net` → Los Angeles). `locate` recognizes CLLI codes, airport codes, and city names for about 120 major network hubs.
|
|
59
|
+
3. **Traceroute** (opt-in with `trace: true`) — runs a traceroute to the target and borrows the location of the closest hop that has one.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { locate } from 'hostinfo';
|
|
63
|
+
|
|
64
|
+
const where = await locate('32.130.20.13', { trace: true });
|
|
65
|
+
// {
|
|
66
|
+
// ip: '32.130.20.13', city: 'Richardson, TX', country: 'UNITED STATES', countryCode: 'US',
|
|
67
|
+
// latitude: null, longitude: null, hostname: null,
|
|
68
|
+
// source: 'traceroute',
|
|
69
|
+
// via: { hop: 4, ip: '71.149.39.230', rtt: 13.2, hostname: null, info: { … }, hint: null }
|
|
70
|
+
// }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The result has all the `HostInfo` fields plus:
|
|
74
|
+
|
|
75
|
+
| field | |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `hostname` | The target's reverse DNS name, or `null`. |
|
|
78
|
+
| `source` | Where `city`/coordinates came from: `'hostip'`, `'hostname'`, `'traceroute'`, or `null` if nothing was found. |
|
|
79
|
+
| `via` | For `'traceroute'`, the hop whose location was used. |
|
|
80
|
+
|
|
81
|
+
Takes the `lookup()` options plus `trace` (`true` or [trace options](#traceip-options--promisetraceresult)). Treat `'hostname'` and especially `'traceroute'` answers as rough: the hop nearest the target is often in the same metro area, but it can also be a regional hub hundreds of miles away.
|
|
82
|
+
|
|
83
|
+
### `trace(ip, options?) → Promise<TraceResult>`
|
|
84
|
+
|
|
85
|
+
Runs the system `traceroute` (macOS/Linux) or `tracert` (Windows) to an IPv4 address. It reports every hop, and where the trail goes cold if the target never answers:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { trace } from 'hostinfo';
|
|
89
|
+
|
|
90
|
+
const { hops, reached, stopped, lastResponding } = await trace('198.51.100.7');
|
|
91
|
+
if (!reached) console.log(`Dropped after hop ${lastResponding?.hop} (${lastResponding?.hostname ?? lastResponding?.ip})`);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Each hop is `{ hop, ip, rtt, hostname, info, hint }`. `ip` is `null` for a probe that timed out (`*`). `info` is hostip.info's answer, and `hint` is the location guessed from `hostname`. [Private hops](#isprivateipv4ip--boolean) are never sent to hostip.info. `stopped` is `'reached'`, `'max-hops'`, `'gave-up'`, or `'timeout'`.
|
|
95
|
+
|
|
96
|
+
| option | default | |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `maxHops` | `30` | Highest TTL to probe. |
|
|
99
|
+
| `probeTimeout` | `1000` | Milliseconds to wait per probe. macOS/Linux round this up to whole seconds. |
|
|
100
|
+
| `giveUpAfter` | `5` | Stop after this many silent hops in a row. `0` runs all the way to `maxHops`. |
|
|
101
|
+
| `timeout` | `60000` | Cut the trace short after this many milliseconds and return the hops so far. `0` disables. |
|
|
102
|
+
| `signal` | – | Abort the trace. The promise rejects with the signal's reason. |
|
|
103
|
+
| `geolocate` | `true` | Look up each hop's hostname and location. |
|
|
104
|
+
| `endpoint` | – | Alternate hostip.info endpoint. |
|
|
105
|
+
|
|
106
|
+
Rejects with a `HostInfoError` if traceroute isn't installed (e.g. minimal Linux containers: `apt install traceroute`).
|
|
107
|
+
|
|
108
|
+
### `reverse(ip, options?) → Promise<string[]>`
|
|
109
|
+
|
|
110
|
+
Reverse DNS (PTR) lookup for an IPv4 or IPv6 address. Returns `[]` when there's no record. Options: `timeout` (default `5000`) and `signal`.
|
|
111
|
+
|
|
112
|
+
### `whois(ip, options?) → Promise<WhoisInfo>`
|
|
113
|
+
|
|
114
|
+
Registration data for the network an IPv4 or IPv6 address belongs to, from [ARIN's RDAP service](https://www.arin.net/resources/registry/whois/rdap/). ARIN redirects addresses held by other registries (RIPE, APNIC, LACNIC, AFRINIC) to the right one automatically.
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
const info = await whois('8.8.8.8');
|
|
118
|
+
// {
|
|
119
|
+
// organization: 'Google LLC', name: 'GOGL', handle: 'NET-8-8-8-0-2',
|
|
120
|
+
// startAddress: '8.8.8.0', endAddress: '8.8.8.255', cidrs: ['8.8.8.0/24'],
|
|
121
|
+
// type: 'DIRECT ALLOCATION', abuseEmail: 'network-abuse@google.com',
|
|
122
|
+
// registry: 'whois.arin.net', contacts: [ … ], …
|
|
123
|
+
// }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`contacts` lists every entity in the record with its `roles`, `name`, `org`, `email`, `phone`, and `address`. Options: `timeout` (default `10000`), `signal`, and `endpoint` (an alternate RDAP base URL). Rejects with a `HostInfoError` on network or HTTP errors.
|
|
127
|
+
|
|
128
|
+
### `hintFromHostname(hostname) → LocationHint | null`
|
|
129
|
+
|
|
130
|
+
The hostname heuristic on its own: `{ code, city, countryCode, latitude, longitude }` or `null`.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
hintFromHostname('be2345.ccr41.fra03.atlas.cogentco.com'); // { code: 'fra', city: 'Frankfurt', … }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `isPrivateIPv4(ip) → boolean`
|
|
137
|
+
|
|
138
|
+
True for IPv4 addresses that aren't publicly routable: loopback, link-local, RFC 1918, carrier-grade NAT (`100.64.0.0/10`), IETF protocol assignments (`192.0.0.0/24`), benchmarking (`198.18.0.0/15`), multicast (`224.0.0.0/4`), and reserved space (`240.0.0.0/4`). The TEST-NET documentation ranges are deliberately not included. `false` for anything that isn't an IPv4 address.
|
|
139
|
+
|
|
140
|
+
### Command line
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
npx hostinfo 8.8.8.8 # locate an address (or omit it for your own public IP)
|
|
144
|
+
npx hostinfo 8.8.8.8 --trace # fall back to a traceroute if needed
|
|
145
|
+
npx hostinfo trace example.com
|
|
146
|
+
npx hostinfo reverse 1.1.1.1
|
|
147
|
+
npx hostinfo whois 8.8.8.8 # owner, range, and abuse contact from ARIN
|
|
148
|
+
npx hostinfo mcp # run as an MCP server (see below)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Hostnames are resolved to an IPv4 address first. Add `--json` for machine-readable output.
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
$ npx hostinfo trace 1.1.1.1
|
|
155
|
+
traceroute to 1.1.1.1
|
|
156
|
+
1 192.168.86.1 9.518 ms
|
|
157
|
+
2 192.168.1.254 12.512 ms
|
|
158
|
+
3 108.254.2.1 13.706 ms 108-254-2-1.lightspeed.hstntx.sbcglobal.net Houston, TX? (from "hstntx")
|
|
159
|
+
4 71.149.39.230 12.02 ms Richardson, TX, UNITED STATES
|
|
160
|
+
...
|
|
161
|
+
11 1.1.1.1 20.103 ms one.one.one.one Buffalo, NY, UNITED STATES
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A `?` marks a location guessed from the hostname rather than reported by hostip.info.
|
|
165
|
+
|
|
166
|
+
### MCP server (for AI assistants)
|
|
167
|
+
|
|
168
|
+
`hostinfo mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io/) server over stdio, so Claude, Cursor, VS Code, and other MCP clients can geolocate, trace, and WHOIS addresses for you. Add it to your client's config:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"mcpServers": {
|
|
173
|
+
"hostinfo": { "command": "npx", "args": ["-y", "hostinfo", "mcp"] }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
With Claude Code: `claude mcp add hostinfo -- npx -y hostinfo mcp`.
|
|
179
|
+
|
|
180
|
+
| tool | arguments | |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| `locate` | `target?`, `trace?` | [`locate()`](#locateip-options--promiselocation); omit `target` for the server's own public IP |
|
|
183
|
+
| `traceroute` | `target`, `maxHops?` | [`trace()`](#traceip-options--promisetraceresult) |
|
|
184
|
+
| `reverse_dns` | `target` | [`reverse()`](#reverseip-options--promisestring) |
|
|
185
|
+
| `whois` | `target` | [`whois()`](#whoisip-options--promisewhoisinfo) |
|
|
186
|
+
| `hostname_hint` | `hostname` | [`hintFromHostname()`](#hintfromhostnamehostname--locationhint--null) |
|
|
187
|
+
|
|
188
|
+
`target` can be an IP address or a hostname. All tools are read-only, and results are the same JSON the library returns. Like the rest of the package, the server has no dependencies. (Traceroute runs on the machine hosting the server, so it measures that machine's path.)
|
|
189
|
+
|
|
190
|
+
### For AI coding tools
|
|
191
|
+
|
|
192
|
+
[`llms.txt`](llms.txt) is a condensed API reference for LLMs, and it ships in the package at `node_modules/hostinfo/llms.txt`. Contributors' agents should read [`AGENTS.md`](AGENTS.md).
|
|
193
|
+
|
|
53
194
|
### Callback style
|
|
54
195
|
|
|
55
196
|
The original 2011 signature still works if you pass a function last:
|
|
@@ -63,6 +204,8 @@ lookup('8.8.8.8', (err, info) => {
|
|
|
63
204
|
});
|
|
64
205
|
```
|
|
65
206
|
|
|
207
|
+
> **Upgrading from 1.x:** the package is now ESM-only (CommonJS `require` still works on Node 22.12+), Node 18/20 are no longer supported, non-IPv4 input rejects with a `TypeError` instead of silently returning "(Private Address)", an unknown `countryCode` is `null` instead of `'XX'`, private addresses no longer hit the network, and `signal` no longer disables `timeout`.
|
|
208
|
+
>
|
|
66
209
|
> **Upgrading from 0.0.2:** the result is now the flat object shown above rather than the raw `xml2js` parse of the API response, and the `request`/`xml2js` dependencies are gone.
|
|
67
210
|
|
|
68
211
|
## Accuracy
|
|
@@ -72,19 +215,24 @@ hostip.info is a community-maintained database. It's free and requires no API ke
|
|
|
72
215
|
## Development
|
|
73
216
|
|
|
74
217
|
```sh
|
|
75
|
-
npm
|
|
218
|
+
npm install
|
|
219
|
+
npm run typecheck # tsc over src and tests
|
|
220
|
+
npm test # builds dist/, then runs unit tests (mocked fetch) and package-export checks
|
|
76
221
|
npm run test:live # also hits the real API
|
|
77
222
|
```
|
|
78
223
|
|
|
224
|
+
The published package runs on Node 22.12+, but the tests run the TypeScript sources directly, which needs type stripping (on by default from Node 22.18).
|
|
225
|
+
|
|
79
226
|
## Releasing
|
|
80
227
|
|
|
81
228
|
Publishing is automated with GitHub Actions via [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) — no npm tokens stored in the repo:
|
|
82
229
|
|
|
83
230
|
1. Bump `version` in `package.json`, commit, and push (CI must be green).
|
|
84
231
|
2. Create a GitHub release with a matching `vX.Y.Z` tag.
|
|
85
|
-
3. The [publish workflow](.github/workflows/publish.yml) runs the tests and
|
|
232
|
+
3. The [publish workflow](.github/workflows/publish.yml) runs the tests and stages the version on npm (`npm stage publish`) with provenance.
|
|
233
|
+
4. Approve the staged version with 2FA — on npmjs.com under **Staged Packages**, or with `npm stage approve <stage-id>` (the id is in the workflow log).
|
|
86
234
|
|
|
87
|
-
One-time setup: on npmjs.com → package **Settings** → **Trusted Publisher**, select GitHub Actions with repository `neopunisher/node-hostip` and workflow `publish.yml
|
|
235
|
+
One-time setup: on npmjs.com → package **Settings** → **Trusted Publisher**, select GitHub Actions with repository `neopunisher/node-hostip` and workflow `publish.yml`, leaving direct `npm publish` disallowed (staging-only).
|
|
88
236
|
|
|
89
237
|
## License
|
|
90
238
|
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { locate } from './locate.js';
|
|
4
|
+
import { lookup } from './lookup.js';
|
|
5
|
+
import { serveMcp } from './mcp.js';
|
|
6
|
+
import { toIP, toIPv4 } from './resolve.js';
|
|
7
|
+
import { reverse } from './reverse.js';
|
|
8
|
+
import { trace } from './trace.js';
|
|
9
|
+
import { whois } from './whois.js';
|
|
10
|
+
const USAGE = `Usage:
|
|
11
|
+
hostinfo [ip|host] Locate an address (your own public IP if omitted)
|
|
12
|
+
hostinfo trace <ip|host> Traceroute, with each hop's hostname and location
|
|
13
|
+
hostinfo reverse <ip|host> Reverse DNS (PTR) names
|
|
14
|
+
hostinfo whois <ip|host> Network owner, range, and abuse contact (ARIN RDAP)
|
|
15
|
+
hostinfo mcp Run as an MCP server over stdio (for AI assistants)
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--trace Let \`hostinfo <ip>\` fall back to a traceroute (slow)
|
|
19
|
+
--json Print raw JSON
|
|
20
|
+
-h, --help Show this help`;
|
|
21
|
+
function place(info) {
|
|
22
|
+
if (!info)
|
|
23
|
+
return null;
|
|
24
|
+
const parts = [info.city, info.country ?? info.countryCode].filter((p) => p !== null);
|
|
25
|
+
return parts.length > 0 ? parts.join(', ') : null;
|
|
26
|
+
}
|
|
27
|
+
function formatLocation(result) {
|
|
28
|
+
const lines = [result.ip ?? '(unknown ip)'];
|
|
29
|
+
if (result.hostname)
|
|
30
|
+
lines.push(` hostname ${result.hostname}`);
|
|
31
|
+
lines.push(` location ${place(result) ?? 'unknown'}`);
|
|
32
|
+
if (result.latitude !== null && result.longitude !== null) {
|
|
33
|
+
lines.push(` coords ${result.latitude}, ${result.longitude}`);
|
|
34
|
+
}
|
|
35
|
+
if (result.source) {
|
|
36
|
+
const via = result.via ? ` (hop ${result.via.hop}, ${result.via.ip})` : '';
|
|
37
|
+
lines.push(` source ${result.source}${via}`);
|
|
38
|
+
}
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
function formatWhois(info) {
|
|
42
|
+
const range = info.startAddress && info.endAddress ? `${info.startAddress} - ${info.endAddress}` : null;
|
|
43
|
+
const rows = [
|
|
44
|
+
['org', info.organization],
|
|
45
|
+
['network', [info.name, info.handle && `(${info.handle})`].filter(Boolean).join(' ') || null],
|
|
46
|
+
['range', [range, info.cidrs.join(', ')].filter(Boolean).join(' ') || null],
|
|
47
|
+
['type', info.type],
|
|
48
|
+
['country', info.country],
|
|
49
|
+
['abuse', info.abuseEmail],
|
|
50
|
+
['registered', info.registered],
|
|
51
|
+
['updated', info.updated],
|
|
52
|
+
['registry', info.registry],
|
|
53
|
+
];
|
|
54
|
+
const lines = [info.ip];
|
|
55
|
+
for (const [label, value] of rows)
|
|
56
|
+
if (value)
|
|
57
|
+
lines.push(` ${label.padEnd(10)} ${value}`);
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
function formatHop(hop) {
|
|
61
|
+
const n = String(hop.hop).padStart(2);
|
|
62
|
+
if (hop.ip === null)
|
|
63
|
+
return `${n} *`;
|
|
64
|
+
const rtt = hop.rtt === null ? '' : `${hop.rtt} ms`;
|
|
65
|
+
const where = place(hop.info) ?? (hop.hint ? `${hop.hint.city}? (from "${hop.hint.code}")` : '');
|
|
66
|
+
return [`${n} ${hop.ip.padEnd(15)} ${rtt.padStart(10)}`, hop.hostname, where]
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join(' ');
|
|
69
|
+
}
|
|
70
|
+
function formatTrace(result) {
|
|
71
|
+
const lines = [`traceroute to ${result.target}`, ...result.hops.map(formatHop)];
|
|
72
|
+
if (!result.reached) {
|
|
73
|
+
const last = result.lastResponding;
|
|
74
|
+
lines.push(`did not reach ${result.target} (${result.stopped})`);
|
|
75
|
+
if (last)
|
|
76
|
+
lines.push(`last responding hop: ${last.hop} ${last.ip}`);
|
|
77
|
+
}
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
async function main() {
|
|
81
|
+
const { values, positionals } = parseArgs({
|
|
82
|
+
allowPositionals: true,
|
|
83
|
+
options: {
|
|
84
|
+
json: { type: 'boolean' },
|
|
85
|
+
trace: { type: 'boolean' },
|
|
86
|
+
help: { type: 'boolean', short: 'h' },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (values.help)
|
|
90
|
+
return console.log(USAGE);
|
|
91
|
+
const [first, second] = positionals;
|
|
92
|
+
if (first === 'mcp')
|
|
93
|
+
return serveMcp();
|
|
94
|
+
const command = first === 'trace' || first === 'reverse' || first === 'whois' ? first : 'locate';
|
|
95
|
+
const target = command === 'locate' ? first : second;
|
|
96
|
+
if (command !== 'locate' && target === undefined) {
|
|
97
|
+
console.error(USAGE);
|
|
98
|
+
process.exitCode = 2;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
process.once('SIGINT', () => controller.abort());
|
|
103
|
+
const { signal } = controller;
|
|
104
|
+
const print = (value, format) => console.log(values.json ? JSON.stringify(value, null, 2) : format());
|
|
105
|
+
if (command === 'trace') {
|
|
106
|
+
const result = await trace(await toIPv4(target), { signal });
|
|
107
|
+
return print(result, () => formatTrace(result));
|
|
108
|
+
}
|
|
109
|
+
if (command === 'whois') {
|
|
110
|
+
const info = await whois(await toIP(target), { signal });
|
|
111
|
+
return print(info, () => formatWhois(info));
|
|
112
|
+
}
|
|
113
|
+
if (command === 'reverse') {
|
|
114
|
+
const names = await reverse(await toIP(target), { signal });
|
|
115
|
+
return print(names, () => (names.length > 0 ? names.join('\n') : '(no PTR records)'));
|
|
116
|
+
}
|
|
117
|
+
// Without a target, ask hostip.info who we are first.
|
|
118
|
+
const ip = target === undefined ? (await lookup({ signal })).ip : await toIPv4(target);
|
|
119
|
+
if (ip === null)
|
|
120
|
+
throw new Error('hostip.info did not report your public IP');
|
|
121
|
+
const result = await locate(ip, { signal, ...(values.trace && { trace: { signal } }) });
|
|
122
|
+
print(result, () => formatLocation(result));
|
|
123
|
+
}
|
|
124
|
+
main().catch((error) => {
|
|
125
|
+
console.error(`hostinfo: ${error instanceof Error ? error.message : String(error)}`);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
});
|
package/dist/hints.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface LocationHint {
|
|
2
|
+
/** The part of the hostname that matched, e.g. "lax" or "hstntx". */
|
|
3
|
+
code: string;
|
|
4
|
+
city: string;
|
|
5
|
+
countryCode: string;
|
|
6
|
+
latitude: number;
|
|
7
|
+
longitude: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Guess where a router or server sits from the location code embedded in its
|
|
11
|
+
* hostname (e.g. "ae-1.r20.lax01.us.bb.gin.ntt.net" → Los Angeles). Returns
|
|
12
|
+
* null when nothing recognizable is found. This is a heuristic: treat it as a
|
|
13
|
+
* hint, not a fact.
|
|
14
|
+
*/
|
|
15
|
+
export declare function hintFromHostname(hostname: string): LocationHint | null;
|
package/dist/hints.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Network operators name routers after where they sit, using CLLI codes
|
|
2
|
+
// (6 letters, North America), IATA airport codes, or plain city names:
|
|
3
|
+
// 108-254-2-1.lightspeed.hstntx.sbcglobal.net -> Houston
|
|
4
|
+
// ae-5.r21.lsanca07.us.bb.gin.ntt.net -> Los Angeles
|
|
5
|
+
// be2345.ccr41.fra03.atlas.cogentco.com -> Frankfurt
|
|
6
|
+
// Each row: city, country code, latitude, longitude, space-separated codes.
|
|
7
|
+
const PLACES = [
|
|
8
|
+
// North America
|
|
9
|
+
['Ashburn, VA', 'US', 39.04, -77.49, 'asbnva ashburn'],
|
|
10
|
+
['Washington, DC', 'US', 38.9, -77.04, 'washdc iad dca washington'],
|
|
11
|
+
['Reston, VA', 'US', 38.96, -77.36, 'rstnva reston'],
|
|
12
|
+
['Richmond, VA', 'US', 37.54, -77.44, 'rcmdva ric richmond'],
|
|
13
|
+
['Baltimore, MD', 'US', 39.29, -76.61, 'bltmmd bwi baltimore'],
|
|
14
|
+
['New York, NY', 'US', 40.71, -74.01, 'nycmny nyc jfk lga newyork'],
|
|
15
|
+
['Newark, NJ', 'US', 40.74, -74.17, 'nwrknj ewr newark'],
|
|
16
|
+
['Philadelphia, PA', 'US', 39.95, -75.17, 'phlapa phl philadelphia'],
|
|
17
|
+
['Pittsburgh, PA', 'US', 40.44, -80.0, 'pitbpa pit pittsburgh'],
|
|
18
|
+
['Boston, MA', 'US', 42.36, -71.06, 'bstnma bos boston'],
|
|
19
|
+
['Atlanta, GA', 'US', 33.75, -84.39, 'atlnga atl atlanta'],
|
|
20
|
+
['Miami, FL', 'US', 25.76, -80.19, 'miamfl mia miami'],
|
|
21
|
+
['Orlando, FL', 'US', 28.54, -81.38, 'orldfl mco orlando'],
|
|
22
|
+
['Tampa, FL', 'US', 27.95, -82.46, 'tampfl tpa tampa'],
|
|
23
|
+
['Jacksonville, FL', 'US', 30.33, -81.66, 'jcvlfl jax jacksonville'],
|
|
24
|
+
['Charlotte, NC', 'US', 35.23, -80.84, 'chrlnc clt charlotte'],
|
|
25
|
+
['Raleigh, NC', 'US', 35.78, -78.64, 'rlghnc rdu raleigh'],
|
|
26
|
+
['Nashville, TN', 'US', 36.16, -86.78, 'nsvltn bna nashville'],
|
|
27
|
+
['Chicago, IL', 'US', 41.88, -87.63, 'chcgil ord mdw chicago'],
|
|
28
|
+
['Detroit, MI', 'US', 42.33, -83.05, 'dtrtmi dtw detroit'],
|
|
29
|
+
['Cleveland, OH', 'US', 41.5, -81.69, 'clevoh cle cleveland'],
|
|
30
|
+
['Columbus, OH', 'US', 39.96, -83.0, 'clmboh cmh columbus'],
|
|
31
|
+
['Cincinnati, OH', 'US', 39.1, -84.51, 'cncnoh cvg cincinnati'],
|
|
32
|
+
['Indianapolis, IN', 'US', 39.77, -86.16, 'ipltin ind indianapolis'],
|
|
33
|
+
['Milwaukee, WI', 'US', 43.04, -87.91, 'mlwkwi mke milwaukee'],
|
|
34
|
+
['Minneapolis, MN', 'US', 44.98, -93.27, 'mplsmn msp minneapolis'],
|
|
35
|
+
['St. Louis, MO', 'US', 38.63, -90.2, 'stlsmo stl stlouis'],
|
|
36
|
+
['Kansas City, MO', 'US', 39.1, -94.58, 'kscymo mci kansascity'],
|
|
37
|
+
['Omaha, NE', 'US', 41.26, -95.93, 'omahne oma omaha'],
|
|
38
|
+
['Dallas, TX', 'US', 32.78, -96.8, 'dllstx dfw dal dallas'],
|
|
39
|
+
['Houston, TX', 'US', 29.76, -95.37, 'hstntx iah hou houston'],
|
|
40
|
+
['Austin, TX', 'US', 30.27, -97.74, 'austtx aus austin'],
|
|
41
|
+
['San Antonio, TX', 'US', 29.42, -98.49, 'snantx sanantonio'],
|
|
42
|
+
['Oklahoma City, OK', 'US', 35.47, -97.52, 'okcyok okc oklahomacity'],
|
|
43
|
+
['New Orleans, LA', 'US', 29.95, -90.07, 'nworla msy neworleans'],
|
|
44
|
+
['Denver, CO', 'US', 39.74, -104.99, 'dnvrco den denver'],
|
|
45
|
+
['Salt Lake City, UT', 'US', 40.76, -111.89, 'slkcut slc saltlake saltlakecity'],
|
|
46
|
+
['Phoenix, AZ', 'US', 33.45, -112.07, 'phnxaz phx phoenix'],
|
|
47
|
+
['Las Vegas, NV', 'US', 36.17, -115.14, 'lsvgnv las lasvegas'],
|
|
48
|
+
['Los Angeles, CA', 'US', 34.05, -118.24, 'lsanca lax losangeles'],
|
|
49
|
+
['San Diego, CA', 'US', 32.72, -117.16, 'sndgca sandiego'],
|
|
50
|
+
['San Jose, CA', 'US', 37.34, -121.89, 'snjsca sjc sanjose'],
|
|
51
|
+
['Santa Clara, CA', 'US', 37.35, -121.96, 'sntcca santaclara'],
|
|
52
|
+
['Palo Alto, CA', 'US', 37.44, -122.14, 'plalca paloalto'],
|
|
53
|
+
['San Francisco, CA', 'US', 37.77, -122.42, 'snfcca sfo sanfrancisco'],
|
|
54
|
+
['Sacramento, CA', 'US', 38.58, -121.49, 'scrmca smf sacramento'],
|
|
55
|
+
['Portland, OR', 'US', 45.52, -122.68, 'ptldor pdx portland'],
|
|
56
|
+
['Seattle, WA', 'US', 47.61, -122.33, 'sttlwa sea seattle'],
|
|
57
|
+
['Honolulu, HI', 'US', 21.31, -157.86, 'hnllhi hnl honolulu'],
|
|
58
|
+
['Anchorage, AK', 'US', 61.22, -149.9, 'anchak anc anchorage'],
|
|
59
|
+
['Toronto', 'CA', 43.65, -79.38, 'yyz toronto'],
|
|
60
|
+
['Montreal', 'CA', 45.5, -73.57, 'mtl yul montreal'],
|
|
61
|
+
['Vancouver', 'CA', 49.28, -123.12, 'yvr vancouver'],
|
|
62
|
+
['Mexico City', 'MX', 19.43, -99.13, 'mex mexico'],
|
|
63
|
+
// Europe
|
|
64
|
+
['London', 'GB', 51.51, -0.13, 'lon lhr london'],
|
|
65
|
+
['Manchester', 'GB', 53.48, -2.24, 'manchester'],
|
|
66
|
+
['Dublin', 'IE', 53.35, -6.26, 'dub dublin'],
|
|
67
|
+
['Amsterdam', 'NL', 52.37, 4.9, 'ams amsterdam'],
|
|
68
|
+
['Brussels', 'BE', 50.85, 4.35, 'bru brussels'],
|
|
69
|
+
['Paris', 'FR', 48.86, 2.35, 'par cdg paris'],
|
|
70
|
+
['Marseille', 'FR', 43.3, 5.37, 'mrs marseille'],
|
|
71
|
+
['Frankfurt', 'DE', 50.11, 8.68, 'fra frankfurt'],
|
|
72
|
+
['Berlin', 'DE', 52.52, 13.4, 'ber berlin'],
|
|
73
|
+
['Munich', 'DE', 48.14, 11.58, 'muc munich muenchen'],
|
|
74
|
+
['Hamburg', 'DE', 53.55, 9.99, 'hamburg'],
|
|
75
|
+
['Düsseldorf', 'DE', 51.23, 6.77, 'dus duesseldorf dusseldorf'],
|
|
76
|
+
['Zurich', 'CH', 47.38, 8.54, 'zrh zurich'],
|
|
77
|
+
['Geneva', 'CH', 46.2, 6.14, 'gva geneva'],
|
|
78
|
+
['Vienna', 'AT', 48.21, 16.37, 'vie vienna'],
|
|
79
|
+
['Milan', 'IT', 45.46, 9.19, 'mxp lin milan'],
|
|
80
|
+
['Rome', 'IT', 41.9, 12.5, 'fco rome'],
|
|
81
|
+
['Madrid', 'ES', 40.42, -3.7, 'mad madrid'],
|
|
82
|
+
['Barcelona', 'ES', 41.39, 2.17, 'bcn barcelona'],
|
|
83
|
+
['Lisbon', 'PT', 38.72, -9.14, 'lis lisbon'],
|
|
84
|
+
['Copenhagen', 'DK', 55.68, 12.57, 'cph copenhagen'],
|
|
85
|
+
['Stockholm', 'SE', 59.33, 18.07, 'arn sto stockholm'],
|
|
86
|
+
['Oslo', 'NO', 59.91, 10.75, 'osl oslo'],
|
|
87
|
+
['Helsinki', 'FI', 60.17, 24.94, 'hel helsinki'],
|
|
88
|
+
['Warsaw', 'PL', 52.23, 21.01, 'waw warsaw'],
|
|
89
|
+
['Prague', 'CZ', 50.08, 14.44, 'prg prague'],
|
|
90
|
+
['Budapest', 'HU', 47.5, 19.04, 'bud budapest'],
|
|
91
|
+
['Bucharest', 'RO', 44.43, 26.1, 'otp bucharest'],
|
|
92
|
+
['Sofia', 'BG', 42.7, 23.32, 'sof sofia'],
|
|
93
|
+
['Athens', 'GR', 37.98, 23.73, 'ath athens'],
|
|
94
|
+
['Istanbul', 'TR', 41.01, 28.98, 'ist istanbul'],
|
|
95
|
+
['Kyiv', 'UA', 50.45, 30.52, 'kbp kiev kyiv'],
|
|
96
|
+
// Middle East & Africa
|
|
97
|
+
['Tel Aviv', 'IL', 32.09, 34.78, 'tlv telaviv'],
|
|
98
|
+
['Dubai', 'AE', 25.2, 55.27, 'dxb dubai'],
|
|
99
|
+
['Johannesburg', 'ZA', -26.2, 28.05, 'jnb johannesburg'],
|
|
100
|
+
['Cape Town', 'ZA', -33.92, 18.42, 'cpt capetown'],
|
|
101
|
+
['Lagos', 'NG', 6.52, 3.38, 'lagos'],
|
|
102
|
+
['Nairobi', 'KE', -1.29, 36.82, 'nbo nairobi'],
|
|
103
|
+
// Asia-Pacific
|
|
104
|
+
['Tokyo', 'JP', 35.68, 139.69, 'tyo nrt hnd tokyo'],
|
|
105
|
+
['Osaka', 'JP', 34.69, 135.5, 'osa kix osaka'],
|
|
106
|
+
['Seoul', 'KR', 37.57, 126.98, 'sel icn seoul'],
|
|
107
|
+
['Hong Kong', 'HK', 22.32, 114.17, 'hkg hongkong'],
|
|
108
|
+
['Taipei', 'TW', 25.03, 121.57, 'tpe taipei'],
|
|
109
|
+
['Singapore', 'SG', 1.35, 103.82, 'sin singapore'],
|
|
110
|
+
['Kuala Lumpur', 'MY', 3.14, 101.69, 'kul kualalumpur'],
|
|
111
|
+
['Bangkok', 'TH', 13.76, 100.5, 'bkk bangkok'],
|
|
112
|
+
['Jakarta', 'ID', -6.21, 106.85, 'cgk jakarta'],
|
|
113
|
+
['Manila', 'PH', 14.6, 120.98, 'mnl manila'],
|
|
114
|
+
['Mumbai', 'IN', 19.08, 72.88, 'bom mumbai'],
|
|
115
|
+
['Delhi', 'IN', 28.61, 77.21, 'del delhi'],
|
|
116
|
+
['Chennai', 'IN', 13.08, 80.27, 'maa chennai'],
|
|
117
|
+
['Sydney', 'AU', -33.87, 151.21, 'syd sydney'],
|
|
118
|
+
['Melbourne', 'AU', -37.81, 144.96, 'mel melbourne'],
|
|
119
|
+
['Perth', 'AU', -31.95, 115.86, 'perth'],
|
|
120
|
+
['Auckland', 'NZ', -36.85, 174.76, 'akl auckland'],
|
|
121
|
+
// South America
|
|
122
|
+
['São Paulo', 'BR', -23.55, -46.63, 'sao gru saopaulo'],
|
|
123
|
+
['Rio de Janeiro', 'BR', -22.91, -43.17, 'rio gig riodejaneiro'],
|
|
124
|
+
['Buenos Aires', 'AR', -34.6, -58.38, 'eze bue buenosaires'],
|
|
125
|
+
['Santiago', 'CL', -33.45, -70.67, 'scl santiago'],
|
|
126
|
+
['Bogotá', 'CO', 4.71, -74.07, 'bog bogota'],
|
|
127
|
+
['Lima', 'PE', -12.05, -77.04, 'lim lima'],
|
|
128
|
+
];
|
|
129
|
+
// Lower number wins: a 6-letter CLLI code is the most specific, a 3-letter
|
|
130
|
+
// airport code the most likely to collide with an unrelated abbreviation.
|
|
131
|
+
const CLLI = 0;
|
|
132
|
+
const NAME = 1;
|
|
133
|
+
const AIRPORT = 2;
|
|
134
|
+
const CODES = new Map();
|
|
135
|
+
for (const place of PLACES) {
|
|
136
|
+
const codes = place[4].split(' ');
|
|
137
|
+
// CLLI codes are exactly six letters ending in a two-letter state; the
|
|
138
|
+
// first code on a North American row is always its CLLI code.
|
|
139
|
+
const hasClli = place[1] === 'US' && codes[0].length === 6;
|
|
140
|
+
codes.forEach((code, i) => {
|
|
141
|
+
const rank = hasClli && i === 0 ? CLLI : code.length === 3 ? AIRPORT : NAME;
|
|
142
|
+
CODES.set(code, { rank, place });
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Guess where a router or server sits from the location code embedded in its
|
|
147
|
+
* hostname (e.g. "ae-1.r20.lax01.us.bb.gin.ntt.net" → Los Angeles). Returns
|
|
148
|
+
* null when nothing recognizable is found. This is a heuristic: treat it as a
|
|
149
|
+
* hint, not a fact.
|
|
150
|
+
*/
|
|
151
|
+
export function hintFromHostname(hostname) {
|
|
152
|
+
const labels = hostname.toLowerCase().replace(/\.$/, '').split('.');
|
|
153
|
+
// The registrable domain ("ntt.net", "cogentco.com") names the operator,
|
|
154
|
+
// not the location, so leave it out.
|
|
155
|
+
const words = labels
|
|
156
|
+
.slice(0, -2)
|
|
157
|
+
.flatMap((label) => label.split(/[^a-z]+/))
|
|
158
|
+
.filter((word) => word.length > 0);
|
|
159
|
+
// Also try adjacent words joined, so "san-jose" and "los-angeles" match.
|
|
160
|
+
const candidates = [
|
|
161
|
+
...words,
|
|
162
|
+
...words.slice(1).map((word, i) => words[i] + word),
|
|
163
|
+
...words.slice(2).map((word, i) => words[i] + words[i + 1] + word),
|
|
164
|
+
];
|
|
165
|
+
let best;
|
|
166
|
+
for (const code of candidates) {
|
|
167
|
+
const entry = CODES.get(code);
|
|
168
|
+
// Strict < keeps the leftmost match within a rank.
|
|
169
|
+
if (entry && (best === undefined || entry.rank < best.rank))
|
|
170
|
+
best = { code, ...entry };
|
|
171
|
+
}
|
|
172
|
+
if (!best)
|
|
173
|
+
return null;
|
|
174
|
+
const [city, countryCode, latitude, longitude] = best.place;
|
|
175
|
+
return { code: best.code, city, countryCode, latitude, longitude };
|
|
176
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { hintFromHostname } from './hints.ts';
|
|
2
|
+
import { locate } from './locate.ts';
|
|
3
|
+
import { HostInfoError, lookup } from './lookup.ts';
|
|
4
|
+
import { reverse } from './reverse.ts';
|
|
5
|
+
import { isPrivateIPv4 } from './private.ts';
|
|
6
|
+
import { trace } from './trace.ts';
|
|
7
|
+
import { whois } from './whois.ts';
|
|
8
|
+
export { hintFromHostname, type LocationHint } from './hints.ts';
|
|
9
|
+
export { locate, type LocateOptions, type Location } from './locate.ts';
|
|
10
|
+
export { HostInfoError, lookup, type HostInfo, type LookupCallback, type LookupOptions, } from './lookup.ts';
|
|
11
|
+
export { reverse, type ReverseOptions } from './reverse.ts';
|
|
12
|
+
export { isPrivateIPv4 } from './private.ts';
|
|
13
|
+
export { trace, type TraceHop, type TraceOptions, type TraceResult } from './trace.ts';
|
|
14
|
+
export { whois, type WhoisContact, type WhoisInfo, type WhoisOptions } from './whois.ts';
|
|
15
|
+
declare const hostinfo: {
|
|
16
|
+
lookup: typeof lookup;
|
|
17
|
+
locate: typeof locate;
|
|
18
|
+
reverse: typeof reverse;
|
|
19
|
+
trace: typeof trace;
|
|
20
|
+
whois: typeof whois;
|
|
21
|
+
hintFromHostname: typeof hintFromHostname;
|
|
22
|
+
isPrivateIPv4: typeof isPrivateIPv4;
|
|
23
|
+
HostInfoError: typeof HostInfoError;
|
|
24
|
+
};
|
|
25
|
+
export default hostinfo;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { hintFromHostname } from './hints.js';
|
|
2
|
+
import { locate } from './locate.js';
|
|
3
|
+
import { HostInfoError, lookup } from './lookup.js';
|
|
4
|
+
import { reverse } from './reverse.js';
|
|
5
|
+
import { isPrivateIPv4 } from './private.js';
|
|
6
|
+
import { trace } from './trace.js';
|
|
7
|
+
import { whois } from './whois.js';
|
|
8
|
+
export { hintFromHostname } from './hints.js';
|
|
9
|
+
export { locate } from './locate.js';
|
|
10
|
+
export { HostInfoError, lookup, } from './lookup.js';
|
|
11
|
+
export { reverse } from './reverse.js';
|
|
12
|
+
export { isPrivateIPv4 } from './private.js';
|
|
13
|
+
export { trace } from './trace.js';
|
|
14
|
+
export { whois } from './whois.js';
|
|
15
|
+
const hostinfo = {
|
|
16
|
+
lookup,
|
|
17
|
+
locate,
|
|
18
|
+
reverse,
|
|
19
|
+
trace,
|
|
20
|
+
whois,
|
|
21
|
+
hintFromHostname,
|
|
22
|
+
isPrivateIPv4,
|
|
23
|
+
HostInfoError,
|
|
24
|
+
};
|
|
25
|
+
export default hostinfo;
|
package/dist/locate.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type HostInfo, type LookupOptions } from './lookup.ts';
|
|
2
|
+
import { type TraceHop, type TraceOptions } from './trace.ts';
|
|
3
|
+
export interface LocateOptions extends LookupOptions {
|
|
4
|
+
/**
|
|
5
|
+
* When neither hostip.info nor the target's hostname gives a city, traceroute
|
|
6
|
+
* to the target and borrow the location of the closest hop that has one.
|
|
7
|
+
* Pass `true` or trace options. Slow (seconds) — default: false.
|
|
8
|
+
*/
|
|
9
|
+
trace?: boolean | TraceOptions;
|
|
10
|
+
}
|
|
11
|
+
export interface Location extends HostInfo {
|
|
12
|
+
/** The target's reverse DNS name, if it has one. */
|
|
13
|
+
hostname: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Where `city` and the coordinates came from: hostip.info, a location code
|
|
16
|
+
* in the target's hostname, or a traceroute hop. null when no city was found.
|
|
17
|
+
*/
|
|
18
|
+
source: 'hostip' | 'hostname' | 'traceroute' | null;
|
|
19
|
+
/** For `source: 'traceroute'`, the hop whose location was used. */
|
|
20
|
+
via: TraceHop | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort geolocation for an IPv4 address. Asks hostip.info first, then
|
|
24
|
+
* falls back to location codes in the target's reverse DNS name (e.g.
|
|
25
|
+
* "…lax01.example.net"), and optionally to a traceroute. Check `source` to see
|
|
26
|
+
* how much to trust the answer.
|
|
27
|
+
*/
|
|
28
|
+
export declare function locate(ip: string, options?: LocateOptions): Promise<Location>;
|