whoispk 1.1.1 → 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 +137 -69
- package/bin/whoispk.js +249 -63
- package/package.json +32 -26
- package/src/errors.js +12 -0
- package/src/index.d.ts +62 -0
- package/src/index.js +118 -0
- package/src/parse.js +175 -0
- package/.npmignore +0 -6
- package/.travis.yml +0 -6
- package/index.js +0 -1
- package/lib/whoispk.js +0 -109
- package/test/test.js +0 -77
package/README.md
CHANGED
|
@@ -1,108 +1,176 @@
|
|
|
1
|
-
|
|
2
|
-
=======
|
|
1
|
+
<div align="center">
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
[]()
|
|
6
|
-
[](https://travis-ci.org/abbassiddiqi/whoispk)
|
|
7
|
-
[](http://opensource.org/licenses/MIT)
|
|
3
|
+
# whoispk
|
|
8
4
|
|
|
9
|
-
|
|
5
|
+
**Check whether a `.pk` domain is taken, from your terminal or from Node.js.**
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/whoispk)
|
|
8
|
+
[](https://www.npmjs.com/package/whoispk)
|
|
9
|
+
[](https://github.com/abbassiddiqi/whoispk/actions/workflows/ci.yml)
|
|
10
|
+
[](package.json)
|
|
11
|
+
[](package.json)
|
|
12
|
+
[](LICENSE)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
<img src="docs/assets/demo.svg" alt="whoispk checking three domains: daraz.pk is registered, magicnotatall.pk is available, x.pk is not registrable" width="720">
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
`.pk` domains don't have a public WHOIS server, so the usual `whois` command can't look them up. whoispk asks PKNIC, the `.pk` registry, and gives you a clear answer: **registered**, **available**, or **not registrable**. It also shows the creation date, expiry date and nameservers.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
No install needed:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npx whoispk yourbrand
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`.pk` is added for you. `yourbrand`, `yourbrand.pk` and `https://www.yourbrand.pk/` all look up the same domain, and `.com.pk`, `.org.pk` and the other second-level names work too.
|
|
29
|
+
|
|
30
|
+
To have the `whoispk` command always available:
|
|
14
31
|
|
|
15
32
|
```sh
|
|
16
33
|
npm install -g whoispk
|
|
17
34
|
```
|
|
18
35
|
|
|
19
|
-
|
|
36
|
+
## Features
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
- **Clear answers.** Each domain is registered, available, or not registrable. For a name PKNIC rejects, you get its reason.
|
|
39
|
+
- **Many domains at once.** Pass several names, or pipe in a list. Lookups run in parallel and print in your order.
|
|
40
|
+
- **Made for scripts.** `--json` output, exit codes you can rely on, and no spinner in piped output.
|
|
41
|
+
- **Also a library.** A promise-based API with TypeScript types, timeouts, retries and cancelling.
|
|
42
|
+
- **Small.** No dependencies, about 10 kB installed. It uses Node's built-in `fetch`.
|
|
43
|
+
- **Watched for breakage.** Tests run every week against the real PKNIC site, so a change to their page gets noticed quickly.
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
-V, --version output the version number
|
|
25
|
-
-h, --help output usage information
|
|
45
|
+
## Recipes
|
|
26
46
|
|
|
27
|
-
|
|
47
|
+
**Check a shortlist of name ideas**, one per line (`#` starts a comment):
|
|
28
48
|
|
|
29
49
|
```sh
|
|
30
|
-
|
|
50
|
+
whoispk --json < ideas.txt | jq -r '.[] | select(.available) | .domain'
|
|
31
51
|
```
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
**Use it in a shell script:**
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
if whoispk --json chaiwala | jq -e '.available' > /dev/null; then
|
|
57
|
+
echo "chaiwala.pk is free. Register it!"
|
|
58
|
+
fi
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Warn before your domains expire:**
|
|
34
62
|
|
|
35
63
|
```js
|
|
36
|
-
|
|
64
|
+
import { lookup } from 'whoispk';
|
|
37
65
|
|
|
38
|
-
|
|
66
|
+
for (const name of ['mybrand.pk', 'mybrand.com.pk']) {
|
|
67
|
+
const r = await lookup(name);
|
|
68
|
+
if (!r.registered) continue;
|
|
69
|
+
|
|
70
|
+
const days = Math.round((Date.parse(r.expireDate) - Date.now()) / 86_400_000);
|
|
71
|
+
if (days < 30) console.warn(`${r.domain} expires in ${days} days (${r.expireDate})`);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
39
74
|
|
|
40
|
-
|
|
41
|
-
throw err;
|
|
42
|
-
}
|
|
75
|
+
## CLI
|
|
43
76
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} else {
|
|
47
|
-
console.log('Domain not Found');
|
|
48
|
-
}
|
|
77
|
+
```
|
|
78
|
+
whoispk [options] <domain...>
|
|
49
79
|
|
|
50
|
-
|
|
80
|
+
-j, --json Print results as JSON
|
|
81
|
+
-t, --timeout <ms> Per-request timeout (default: 15000)
|
|
82
|
+
-c, --concurrency <n> Parallel lookups (default: 3)
|
|
83
|
+
--no-color Disable colours (NO_COLOR is respected too)
|
|
51
84
|
```
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
The exit code is `0` when every lookup finished, `1` when any failed (e.g. PKNIC couldn't be reached), and `2` for bad usage.
|
|
87
|
+
|
|
88
|
+
See the **[CLI reference](docs/cli.md)** for the JSON format, stdin input and more.
|
|
89
|
+
|
|
90
|
+
## Library
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npm install whoispk
|
|
94
|
+
```
|
|
54
95
|
|
|
55
96
|
```js
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.lookup('daraz.pk')
|
|
60
|
-
.then(function (domainInfo) {
|
|
61
|
-
|
|
62
|
-
if (domainInfo.isFound) {
|
|
63
|
-
console.log(domainInfo.data);
|
|
64
|
-
} else {
|
|
65
|
-
console.log('Domain not Found');
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
.catch(function (err) {
|
|
69
|
-
throw err;
|
|
70
|
-
});
|
|
97
|
+
import { lookup } from 'whoispk'; // or: const { lookup } = require('whoispk');
|
|
98
|
+
|
|
99
|
+
const result = await lookup('daraz.pk');
|
|
71
100
|
```
|
|
72
101
|
|
|
73
|
-
The console will show following information if domainInfo.isFound is true:
|
|
74
102
|
```js
|
|
75
103
|
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'Clifton, Karachi; South Sindh75600'
|
|
86
|
-
],
|
|
87
|
-
billing_contact: [
|
|
88
|
-
'iS-Fun Internet Services GmbH',
|
|
89
|
-
'Robin Fundinger',
|
|
90
|
-
'Karlsbad, 76307'
|
|
91
|
-
],
|
|
92
|
-
nameservers: [
|
|
93
|
-
'kip.ns.cloudflare.com',
|
|
94
|
-
'naomi.ns.cloudflare.com'
|
|
95
|
-
]
|
|
104
|
+
domain: 'daraz.pk',
|
|
105
|
+
status: 'registered', // 'registered' | 'available' | 'invalid'
|
|
106
|
+
registered: true,
|
|
107
|
+
available: false,
|
|
108
|
+
createDate: '2012-05-14', // YYYY-MM-DD, or null
|
|
109
|
+
expireDate: '2028-05-14', // YYYY-MM-DD, or null
|
|
110
|
+
nameservers: ['ns1.alibabadns.cn', 'ns1.alibabadns.com', 'ns2.alibabadns.cn', 'ns2.alibabadns.com'],
|
|
111
|
+
reasons: [], // PKNIC's reasons, when status is 'invalid'
|
|
112
|
+
fields: { 'Domain Name': 'daraz.pk', 'Create Date': 'May 14, 2012', 'Expire Date': 'May 14, 2028' }
|
|
96
113
|
}
|
|
97
114
|
```
|
|
98
115
|
|
|
99
|
-
|
|
116
|
+
Options include `timeout`, `retries`, `signal` and `fetch`. Failures reject with a `WhoisPkError` that has a `code`, such as `TIMEOUT` or `NETWORK_ERROR`. See the **[API reference](docs/api.md)** for all of it.
|
|
117
|
+
|
|
118
|
+
## FAQ
|
|
119
|
+
|
|
120
|
+
<details>
|
|
121
|
+
<summary><b>Why isn't the owner's name or contact shown?</b></summary>
|
|
122
|
+
|
|
123
|
+
PKNIC stopped publishing registrant, address and contact details because of GDPR and ICANN WHOIS policy. Only the dates and nameservers are public now. If you own the domain, you can see the full record by logging in to your PKNIC account.
|
|
124
|
+
</details>
|
|
125
|
+
|
|
126
|
+
<details>
|
|
127
|
+
<summary><b>Does "available" mean I can definitely register it?</b></summary>
|
|
128
|
+
|
|
129
|
+
Not quite. It means PKNIC has no record of the domain. PKNIC only checks whether the name is allowed, for example that it isn't reserved or blocked, when you actually register it. Names it rejects straight away, such as ones that are too short, show up as **not registrable**.
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary><b>How do I register a domain I found?</b></summary>
|
|
134
|
+
|
|
135
|
+
Through [PKNIC](https://pknic.net.pk) or one of its registrars. whoispk only looks domains up.
|
|
136
|
+
</details>
|
|
137
|
+
|
|
138
|
+
<details>
|
|
139
|
+
<summary><b>It says <code>PARSE_ERROR</code>. What now?</b></summary>
|
|
140
|
+
|
|
141
|
+
PKNIC has probably changed its lookup page. Please [open an issue](https://github.com/abbassiddiqi/whoispk/issues). [How it works](docs/how-it-works.md) explains the parser, if you'd like to send a fix.
|
|
142
|
+
</details>
|
|
143
|
+
|
|
144
|
+
<details>
|
|
145
|
+
<summary><b>Is there a rate limit?</b></summary>
|
|
146
|
+
|
|
147
|
+
PKNIC doesn't publish one, but please be considerate. The CLI runs at most 3 lookups at a time by default, and retries only once, after a pause. Avoid checking thousands of names in a tight loop.
|
|
148
|
+
</details>
|
|
149
|
+
|
|
150
|
+
<details>
|
|
151
|
+
<summary><b>I'm upgrading from 1.x.</b></summary>
|
|
152
|
+
|
|
153
|
+
Version 2 needs Node.js 20.19 or later. The callback API is gone; use the promise. Dates are now `YYYY-MM-DD`. See the [changelog](CHANGELOG.md).
|
|
154
|
+
</details>
|
|
155
|
+
|
|
156
|
+
## Documentation
|
|
157
|
+
|
|
158
|
+
| Guide | What's in it |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| [CLI reference](docs/cli.md) | Every option, stdin input, JSON output, exit codes, `jq` recipes |
|
|
161
|
+
| [API reference](docs/api.md) | `lookup`, `parse`, `normalizeDomain`, result fields, errors |
|
|
162
|
+
| [How it works](docs/how-it-works.md) | How PKNIC's page is read, and what to do when it changes |
|
|
163
|
+
| [Changelog](CHANGELOG.md) | What changed in each release |
|
|
164
|
+
| [Releasing](docs/releasing.md) | How maintainers publish a new version to npm |
|
|
100
165
|
|
|
101
166
|
## Contributing
|
|
102
167
|
|
|
103
|
-
|
|
104
|
-
|
|
168
|
+
Bug reports and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers setup, tests and guidelines.
|
|
169
|
+
|
|
170
|
+
whoispk started in 2017. You can read about the original version in [this Medium post](https://medium.com/@abbassiddiqi/whoispk-command-line-tool-5ea275926688).
|
|
105
171
|
|
|
106
172
|
## License
|
|
107
173
|
|
|
108
|
-
|
|
174
|
+
[MIT](LICENSE) © Abbas Siddiqi
|
|
175
|
+
|
|
176
|
+
whoispk isn't affiliated with or endorsed by PKNIC. Results come from PKNIC's public lookup page and may change without notice.
|
package/bin/whoispk.js
CHANGED
|
@@ -1,73 +1,259 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { lookup, ENDPOINT } from '../src/index.js';
|
|
5
|
+
|
|
6
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
8
|
+
|
|
9
|
+
const HELP = `
|
|
10
|
+
Usage: whoispk [options] <domain...>
|
|
11
|
+
|
|
12
|
+
Check whether .pk domains are registered, using PKNIC's public lookup.
|
|
13
|
+
".pk" is appended when missing, so "whoispk magic" looks up magic.pk.
|
|
14
|
+
Domains can also be piped in, one per line: cat domains.txt | whoispk
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
-j, --json Print results as JSON
|
|
18
|
+
-t, --timeout <ms> Per-request timeout in milliseconds (default: 15000)
|
|
19
|
+
-c, --concurrency <n> Parallel lookups when checking several domains (default: 3)
|
|
20
|
+
--no-color Disable colours (also respects NO_COLOR)
|
|
21
|
+
-v, --version Print the version
|
|
22
|
+
-h, --help Show this help
|
|
23
|
+
|
|
24
|
+
Exit codes:
|
|
25
|
+
0 all lookups completed
|
|
26
|
+
1 one or more lookups failed (network error, PKNIC down, …)
|
|
27
|
+
2 invalid usage
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Argument parsing
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
let args;
|
|
35
|
+
try {
|
|
36
|
+
args = parseArgs({
|
|
37
|
+
allowPositionals: true,
|
|
38
|
+
options: {
|
|
39
|
+
json: { type: 'boolean', short: 'j' },
|
|
40
|
+
timeout: { type: 'string', short: 't' },
|
|
41
|
+
concurrency: { type: 'string', short: 'c' },
|
|
42
|
+
'no-color': { type: 'boolean' },
|
|
43
|
+
version: { type: 'boolean', short: 'v' },
|
|
44
|
+
help: { type: 'boolean', short: 'h' },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
usageError(err.message);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
const { values: opts, positionals } = args;
|
|
52
|
+
|
|
53
|
+
if (opts.help) {
|
|
54
|
+
process.stdout.write(HELP.trimStart());
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
if (opts.version) {
|
|
58
|
+
console.log(pkg.version);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
const timeout = parsePositiveInt(opts.timeout, 15_000, '--timeout');
|
|
63
|
+
const concurrency = parsePositiveInt(opts.concurrency, 3, '--concurrency');
|
|
64
|
+
|
|
65
|
+
let domains = positionals;
|
|
66
|
+
if (!domains.length && !process.stdin.isTTY) {
|
|
67
|
+
domains = (await readStdin())
|
|
68
|
+
.split(/\r?\n/)
|
|
69
|
+
.flatMap((line) => line.replace(/#.*/, '').split(/[\s,]+/))
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
if (!domains.length) {
|
|
73
|
+
process.stdout.write(HELP.trimStart());
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Styling
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const useColor =
|
|
82
|
+
!opts['no-color'] &&
|
|
83
|
+
!('NO_COLOR' in process.env) &&
|
|
84
|
+
(process.env.FORCE_COLOR ? process.env.FORCE_COLOR !== '0' : Boolean(process.stdout.isTTY));
|
|
85
|
+
|
|
86
|
+
const ansi = (open, close) => (s) => (useColor ? `\x1b[${open}m${s}\x1b[${close}m` : String(s));
|
|
87
|
+
const c = {
|
|
88
|
+
bold: ansi(1, 22),
|
|
89
|
+
dim: ansi(2, 22),
|
|
90
|
+
red: ansi(31, 39),
|
|
91
|
+
green: ansi(32, 39),
|
|
92
|
+
yellow: ansi(33, 39),
|
|
93
|
+
cyan: ansi(36, 39),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Run lookups (bounded concurrency, printed in input order)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const endpoint = process.env.WHOISPK_ENDPOINT || ENDPOINT;
|
|
101
|
+
const spinner = createSpinner(!opts.json && process.stderr.isTTY);
|
|
102
|
+
const limit = createLimiter(concurrency);
|
|
103
|
+
|
|
104
|
+
let done = 0;
|
|
105
|
+
spinner.update(progressText(done, domains.length));
|
|
106
|
+
|
|
107
|
+
const pending = domains.map((domain) =>
|
|
108
|
+
limit(() => lookup(domain, { timeout, endpoint }))
|
|
109
|
+
.then(
|
|
110
|
+
(result) => ({ ok: true, input: domain, result }),
|
|
111
|
+
(error) => ({ ok: false, input: domain, error }),
|
|
112
|
+
)
|
|
113
|
+
.finally(() => spinner.update(progressText(++done, domains.length))),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
let failed = 0;
|
|
117
|
+
const jsonResults = [];
|
|
118
|
+
|
|
119
|
+
for (const [i, promise] of pending.entries()) {
|
|
120
|
+
const outcome = await promise;
|
|
121
|
+
if (!outcome.ok) failed++;
|
|
122
|
+
|
|
123
|
+
if (opts.json) {
|
|
124
|
+
jsonResults.push(
|
|
125
|
+
outcome.ok
|
|
126
|
+
? outcome.result
|
|
127
|
+
: { domain: outcome.input, status: 'error', error: { code: outcome.error.code ?? null, message: outcome.error.message } },
|
|
128
|
+
);
|
|
129
|
+
continue;
|
|
51
130
|
}
|
|
52
131
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
132
|
+
spinner.clear();
|
|
133
|
+
if (i > 0) console.log('');
|
|
134
|
+
console.log(outcome.ok ? formatResult(outcome.result) : formatError(outcome.input, outcome.error));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
spinner.stop();
|
|
138
|
+
|
|
139
|
+
if (opts.json) {
|
|
140
|
+
console.log(JSON.stringify(jsonResults.length === 1 ? jsonResults[0] : jsonResults, null, 2));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
process.exitCode = failed ? 1 : 0;
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Formatting
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function formatResult(r) {
|
|
150
|
+
const lines = [];
|
|
151
|
+
const label = (s) => ` ${c.dim(s.padEnd(13))}`;
|
|
152
|
+
|
|
153
|
+
if (r.status === 'registered') {
|
|
154
|
+
lines.push(`${c.bold(r.domain)} ${c.red('● Registered')}`);
|
|
155
|
+
if (r.createDate) lines.push(`${label('Created')}${formatDate(r.createDate)}`);
|
|
156
|
+
if (r.expireDate) lines.push(`${label('Expires')}${formatDate(r.expireDate)} ${c.dim(`(${relative(r.expireDate)})`)}`);
|
|
157
|
+
r.nameservers.forEach((ns, i) => lines.push(`${label(i === 0 ? 'Nameservers' : '')}${ns}`));
|
|
158
|
+
} else if (r.status === 'available') {
|
|
159
|
+
lines.push(`${c.bold(r.domain)} ${c.green('● Available')}`);
|
|
160
|
+
lines.push(` ${c.dim('Not registered — may be available for registration.')}`);
|
|
161
|
+
} else {
|
|
162
|
+
lines.push(`${c.bold(r.domain)} ${c.yellow('● Not registrable')}`);
|
|
163
|
+
for (const reason of r.reasons) lines.push(` ${c.dim(reason)}`);
|
|
66
164
|
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatError(domain, err) {
|
|
170
|
+
return `${c.bold(domain)} ${c.red('✖ Lookup failed')}\n ${c.dim(err.message)}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatDate(iso) {
|
|
174
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
175
|
+
return `${MONTHS[m - 1]} ${d}, ${y}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function relative(iso) {
|
|
179
|
+
const days = Math.round((Date.parse(`${iso}T00:00:00Z`) - Date.now()) / 86_400_000);
|
|
180
|
+
if (days === 0) return 'expires today';
|
|
181
|
+
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'always' });
|
|
182
|
+
const abs = Math.abs(days);
|
|
183
|
+
const [value, unit] =
|
|
184
|
+
abs >= 330 ? [Math.round(days / 365), 'year'] : abs >= 45 ? [Math.round(days / 30), 'month'] : [days, 'day'];
|
|
185
|
+
return `${days < 0 ? 'expired ' : 'expires '}${rtf.format(value, unit)}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function progressText(n, total) {
|
|
189
|
+
return total === 1 ? `Looking up ${domains[0]} on PKNIC…` : `Looking up domains on PKNIC… ${n}/${total}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Helpers
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
function createSpinner(enabled) {
|
|
197
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
198
|
+
let text = '';
|
|
199
|
+
let frame = 0;
|
|
200
|
+
let timer = null;
|
|
201
|
+
const render = () => process.stderr.write(`\r\x1b[K${c.cyan(frames[frame++ % frames.length])} ${text}`);
|
|
202
|
+
return {
|
|
203
|
+
update(t) {
|
|
204
|
+
if (!enabled) return;
|
|
205
|
+
text = t;
|
|
206
|
+
if (!timer) {
|
|
207
|
+
timer = setInterval(render, 80);
|
|
208
|
+
timer.unref();
|
|
209
|
+
}
|
|
210
|
+
render();
|
|
211
|
+
},
|
|
212
|
+
clear() {
|
|
213
|
+
if (enabled && timer) process.stderr.write('\r\x1b[K');
|
|
214
|
+
},
|
|
215
|
+
stop() {
|
|
216
|
+
if (!enabled) return;
|
|
217
|
+
clearInterval(timer);
|
|
218
|
+
timer = null;
|
|
219
|
+
process.stderr.write('\r\x1b[K');
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function createLimiter(max) {
|
|
225
|
+
let active = 0;
|
|
226
|
+
const queue = [];
|
|
227
|
+
const next = () => {
|
|
228
|
+
if (active >= max || !queue.length) return;
|
|
229
|
+
active++;
|
|
230
|
+
const { fn, resolve, reject } = queue.shift();
|
|
231
|
+
fn().then(resolve, reject).finally(() => {
|
|
232
|
+
active--;
|
|
233
|
+
next();
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
return (fn) =>
|
|
237
|
+
new Promise((resolve, reject) => {
|
|
238
|
+
queue.push({ fn, resolve, reject });
|
|
239
|
+
next();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parsePositiveInt(raw, fallback, name) {
|
|
244
|
+
if (raw === undefined) return fallback;
|
|
245
|
+
const n = Number(raw);
|
|
246
|
+
if (!Number.isInteger(n) || n <= 0) usageError(`${name} must be a positive integer`);
|
|
247
|
+
return n;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function usageError(message) {
|
|
251
|
+
console.error(`whoispk: ${message}\nRun "whoispk --help" for usage.`);
|
|
252
|
+
process.exit(2);
|
|
67
253
|
}
|
|
68
254
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
255
|
+
async function readStdin() {
|
|
256
|
+
let data = '';
|
|
257
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
258
|
+
return data;
|
|
73
259
|
}
|
package/package.json
CHANGED
|
@@ -1,49 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whoispk",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Check .pk domain availability and registration details from PKNIC, from the command line or Node.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"whois",
|
|
7
7
|
"pk",
|
|
8
8
|
"pknic",
|
|
9
9
|
"whoispk",
|
|
10
|
-
"pakistan"
|
|
10
|
+
"pakistan",
|
|
11
|
+
"domain",
|
|
12
|
+
"availability",
|
|
13
|
+
"cli"
|
|
11
14
|
],
|
|
12
|
-
"
|
|
13
|
-
"
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./src/index.js",
|
|
17
|
+
"types": "./src/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./src/index.d.ts",
|
|
21
|
+
"default": "./src/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
14
25
|
"bin": {
|
|
15
|
-
"whoispk": "
|
|
26
|
+
"whoispk": "bin/whoispk.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"src"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20.19"
|
|
16
34
|
},
|
|
17
35
|
"scripts": {
|
|
18
|
-
"test": "
|
|
36
|
+
"test": "node --test",
|
|
37
|
+
"test:live": "WHOISPK_LIVE=1 node --test",
|
|
38
|
+
"fixtures": "node scripts/update-fixtures.js",
|
|
39
|
+
"prepublishOnly": "npm test"
|
|
19
40
|
},
|
|
20
41
|
"author": {
|
|
21
42
|
"name": "Abbas Siddiqi",
|
|
22
43
|
"email": "abbas.siddiqi1@gmail.com",
|
|
23
|
-
"url": "abbassiddiqi.com"
|
|
44
|
+
"url": "https://abbassiddiqi.com"
|
|
24
45
|
},
|
|
25
46
|
"repository": {
|
|
26
47
|
"type": "git",
|
|
27
|
-
"url": "git://github.com/abbassiddiqi/whoispk.git"
|
|
48
|
+
"url": "git+https://github.com/abbassiddiqi/whoispk.git"
|
|
28
49
|
},
|
|
29
50
|
"bugs": {
|
|
30
51
|
"url": "https://github.com/abbassiddiqi/whoispk/issues"
|
|
31
52
|
},
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
"url": "https://github.com/abbassiddiqi/whoispk/blob/master/LICENSE"
|
|
35
|
-
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"axios": "^0.16.2",
|
|
38
|
-
"chalk": "^2.1.0",
|
|
39
|
-
"change-case": "^3.0.1",
|
|
40
|
-
"cheerio": "^1.0.0-rc.2",
|
|
41
|
-
"cli-spinner": "^0.2.6",
|
|
42
|
-
"commander": "^2.11.0",
|
|
43
|
-
"querystring": "^0.2.0"
|
|
44
|
-
},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"chai": "^4.1.1",
|
|
47
|
-
"mocha": "^3.5.0"
|
|
48
|
-
}
|
|
53
|
+
"homepage": "https://github.com/abbassiddiqi/whoispk#readme",
|
|
54
|
+
"license": "MIT"
|
|
49
55
|
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown by whoispk. `code` is one of:
|
|
3
|
+
* INVALID_INPUT, HTTP_ERROR, NETWORK_ERROR, TIMEOUT, ABORTED, PARSE_ERROR.
|
|
4
|
+
*/
|
|
5
|
+
export class WhoisPkError extends Error {
|
|
6
|
+
constructor(message, { code, cause, status } = {}) {
|
|
7
|
+
super(message, cause ? { cause } : undefined);
|
|
8
|
+
this.name = 'WhoisPkError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
if (status !== undefined) this.status = status;
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type DomainStatus = 'registered' | 'available' | 'invalid';
|
|
2
|
+
|
|
3
|
+
export interface LookupResult {
|
|
4
|
+
/** The normalised domain that was looked up, e.g. "daraz.pk". */
|
|
5
|
+
domain: string;
|
|
6
|
+
status: DomainStatus;
|
|
7
|
+
registered: boolean;
|
|
8
|
+
/** True when PKNIC reports the domain as not registered (it may be available to register). */
|
|
9
|
+
available: boolean;
|
|
10
|
+
/** Registration date as YYYY-MM-DD, when registered. */
|
|
11
|
+
createDate: string | null;
|
|
12
|
+
/** Expiry date as YYYY-MM-DD, when registered. */
|
|
13
|
+
expireDate: string | null;
|
|
14
|
+
/** DNS entries listed by PKNIC, in the order shown. */
|
|
15
|
+
nameservers: string[];
|
|
16
|
+
/** Why PKNIC rejected the name, when status is "invalid". */
|
|
17
|
+
reasons: string[];
|
|
18
|
+
/** All labelled fields from the record table, verbatim (e.g. { "Create Date": "Nov 7, 2013" }). */
|
|
19
|
+
fields: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LookupOptions {
|
|
23
|
+
/** Per-request timeout in milliseconds. Default 15000. */
|
|
24
|
+
timeout?: number;
|
|
25
|
+
/** Retries on network errors, timeouts, 429 and 5xx. Default 1. */
|
|
26
|
+
retries?: number;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
/** Override the PKNIC lookup URL. */
|
|
29
|
+
endpoint?: string;
|
|
30
|
+
/** Custom fetch implementation. Defaults to the global fetch. */
|
|
31
|
+
fetch?: typeof globalThis.fetch;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type WhoisPkErrorCode =
|
|
35
|
+
| 'INVALID_INPUT'
|
|
36
|
+
| 'HTTP_ERROR'
|
|
37
|
+
| 'NETWORK_ERROR'
|
|
38
|
+
| 'TIMEOUT'
|
|
39
|
+
| 'ABORTED'
|
|
40
|
+
| 'PARSE_ERROR';
|
|
41
|
+
|
|
42
|
+
export class WhoisPkError extends Error {
|
|
43
|
+
code: WhoisPkErrorCode;
|
|
44
|
+
/** HTTP status, for HTTP_ERROR. */
|
|
45
|
+
status?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const ENDPOINT: string;
|
|
49
|
+
export const USER_AGENT: string;
|
|
50
|
+
|
|
51
|
+
export function lookup(domain: string, options?: LookupOptions): Promise<LookupResult>;
|
|
52
|
+
export function parse(html: string): Omit<LookupResult, 'domain'> & { domain: string | null };
|
|
53
|
+
export function normalizeDomain(input: string): string;
|
|
54
|
+
|
|
55
|
+
declare const whoispk: {
|
|
56
|
+
lookup: typeof lookup;
|
|
57
|
+
parse: typeof parse;
|
|
58
|
+
normalizeDomain: typeof normalizeDomain;
|
|
59
|
+
WhoisPkError: typeof WhoisPkError;
|
|
60
|
+
ENDPOINT: string;
|
|
61
|
+
};
|
|
62
|
+
export default whoispk;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { WhoisPkError } from './errors.js';
|
|
3
|
+
import { parse } from './parse.js';
|
|
4
|
+
|
|
5
|
+
export { parse, WhoisPkError };
|
|
6
|
+
|
|
7
|
+
export const ENDPOINT = 'https://pk6.pknic.net.pk/pk5/lookup.PK';
|
|
8
|
+
|
|
9
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
10
|
+
export const USER_AGENT = `whoispk/${version} (+https://github.com/abbassiddiqi/whoispk)`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalise user input into a bare .pk domain name.
|
|
14
|
+
*
|
|
15
|
+
* "https://www.Daraz.pk/some/path" -> "daraz.pk"
|
|
16
|
+
* "magic" -> "magic.pk"
|
|
17
|
+
* "example.com.pk." -> "example.com.pk"
|
|
18
|
+
*
|
|
19
|
+
* @param {string} input
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeDomain(input) {
|
|
23
|
+
if (typeof input !== 'string' || !input.trim()) {
|
|
24
|
+
throw new WhoisPkError('A domain name is required', { code: 'INVALID_INPUT' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let domain = input.trim().toLowerCase();
|
|
28
|
+
domain = domain.replace(/^[a-z][a-z0-9+.-]*:\/\//, ''); // scheme
|
|
29
|
+
domain = domain.replace(/^[^@/]*@/, ''); // userinfo
|
|
30
|
+
domain = domain.replace(/[/?#].*$/, ''); // path, query, fragment
|
|
31
|
+
domain = domain.replace(/:\d+$/, ''); // port
|
|
32
|
+
domain = domain.replace(/\.+$/, ''); // trailing dot(s)
|
|
33
|
+
domain = domain.replace(/^www\./, '');
|
|
34
|
+
|
|
35
|
+
if (!domain) {
|
|
36
|
+
throw new WhoisPkError(`"${input}" is not a valid domain name`, { code: 'INVALID_INPUT' });
|
|
37
|
+
}
|
|
38
|
+
if (!domain.endsWith('.pk') && domain !== 'pk') domain += '.pk';
|
|
39
|
+
|
|
40
|
+
return domain;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
|
|
45
|
+
async function request(domain, { endpoint, timeout, signal, fetch: fetchImpl }) {
|
|
46
|
+
const signals = [AbortSignal.timeout(timeout)];
|
|
47
|
+
if (signal) signals.push(signal);
|
|
48
|
+
|
|
49
|
+
let response;
|
|
50
|
+
try {
|
|
51
|
+
response = await fetchImpl(endpoint, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
55
|
+
accept: 'text/html',
|
|
56
|
+
'user-agent': USER_AGENT,
|
|
57
|
+
},
|
|
58
|
+
body: new URLSearchParams({ name: domain }).toString(),
|
|
59
|
+
signal: AbortSignal.any(signals),
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
throw new WhoisPkError('Lookup aborted', { code: 'ABORTED', cause: err });
|
|
64
|
+
}
|
|
65
|
+
if (err?.name === 'TimeoutError') {
|
|
66
|
+
throw new WhoisPkError(`PKNIC did not respond within ${timeout} ms`, { code: 'TIMEOUT', cause: err });
|
|
67
|
+
}
|
|
68
|
+
throw new WhoisPkError(`Could not reach PKNIC: ${err?.cause?.message ?? err?.message ?? err}`, {
|
|
69
|
+
code: 'NETWORK_ERROR',
|
|
70
|
+
cause: err,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new WhoisPkError(`PKNIC responded with HTTP ${response.status}`, {
|
|
76
|
+
code: 'HTTP_ERROR',
|
|
77
|
+
status: response.status,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return response.text();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Look up a .pk domain on PKNIC.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} input Domain name, e.g. "daraz.pk" (".pk" is appended if missing).
|
|
88
|
+
* @param {import('./index.js').LookupOptions} [options]
|
|
89
|
+
* @returns {Promise<import('./index.js').LookupResult>}
|
|
90
|
+
*/
|
|
91
|
+
export async function lookup(input, options = {}) {
|
|
92
|
+
const {
|
|
93
|
+
timeout = 15_000,
|
|
94
|
+
retries = 1,
|
|
95
|
+
signal,
|
|
96
|
+
endpoint = ENDPOINT,
|
|
97
|
+
fetch: fetchImpl = globalThis.fetch,
|
|
98
|
+
} = options;
|
|
99
|
+
|
|
100
|
+
const domain = normalizeDomain(input);
|
|
101
|
+
|
|
102
|
+
for (let attempt = 0; ; attempt++) {
|
|
103
|
+
try {
|
|
104
|
+
const html = await request(domain, { endpoint, timeout, signal, fetch: fetchImpl });
|
|
105
|
+
const result = parse(html);
|
|
106
|
+
return { ...result, domain: result.domain ?? domain };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const retryable =
|
|
109
|
+
err.code === 'NETWORK_ERROR' ||
|
|
110
|
+
err.code === 'TIMEOUT' ||
|
|
111
|
+
(err.code === 'HTTP_ERROR' && (err.status >= 500 || err.status === 429));
|
|
112
|
+
if (!retryable || attempt >= retries) throw err;
|
|
113
|
+
await sleep(500 * 2 ** attempt);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default { lookup, parse, normalizeDomain, WhoisPkError, ENDPOINT };
|
package/src/parse.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for the HTML returned by PKNIC's public lookup page
|
|
3
|
+
* (https://pk6.pknic.net.pk/pk5/lookup.PK).
|
|
4
|
+
*
|
|
5
|
+
* PKNIC embeds machine-readable marker tags in the page, e.g.
|
|
6
|
+
* `<XPC_DOMAIN_NOT_AVAILABLE/>` and `<XPC_RESULT_NOTFOUND/>`. Those are used to
|
|
7
|
+
* decide the status; visible text is only used as a fallback and to extract
|
|
8
|
+
* the record details.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { WhoisPkError } from './errors.js';
|
|
12
|
+
|
|
13
|
+
const MONTHS = {
|
|
14
|
+
jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,
|
|
15
|
+
jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ENTITIES = { nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
19
|
+
|
|
20
|
+
/** Decode the handful of HTML entities PKNIC uses. */
|
|
21
|
+
function decodeEntities(str) {
|
|
22
|
+
return str.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity) => {
|
|
23
|
+
if (entity[0] === '#') {
|
|
24
|
+
const code = entity[1] === 'x' || entity[1] === 'X'
|
|
25
|
+
? parseInt(entity.slice(2), 16)
|
|
26
|
+
: parseInt(entity.slice(1), 10);
|
|
27
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : match;
|
|
28
|
+
}
|
|
29
|
+
return ENTITIES[entity.toLowerCase()] ?? match;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Strip tags, decode entities and collapse whitespace. */
|
|
34
|
+
function toText(html) {
|
|
35
|
+
return decodeEntities(html.replace(/<[^>]*>/g, ' '))
|
|
36
|
+
.replace(/\s+/g, ' ')
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** "Nov 7, 2013" -> "2013-11-07"; returns null if it doesn't look like a date. */
|
|
41
|
+
export function toIsoDate(text) {
|
|
42
|
+
const m = /^([a-z]{3})[a-z]*\.?\s+(\d{1,2}),?\s+(\d{4})$/i.exec(text.trim());
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
const month = MONTHS[m[1].toLowerCase()];
|
|
45
|
+
if (!month) return null;
|
|
46
|
+
return `${m[3]}-${String(month).padStart(2, '0')}-${m[2].padStart(2, '0')}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The main content cell of the page (left column), without HTML comments. */
|
|
50
|
+
function mainSection(html) {
|
|
51
|
+
const body = html.replace(/<!--[\s\S]*?-->/g, '');
|
|
52
|
+
const start = body.search(/id=["']?Tmain\b/i);
|
|
53
|
+
const end = body.search(/id=["']?Tside\b/i);
|
|
54
|
+
if (start !== -1 && end > start) return body.slice(start, end);
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse the record table into labelled fields and nameservers.
|
|
60
|
+
* Rows are laid out as [spacer, label, spacer, value]. A label with no value
|
|
61
|
+
* (e.g. "DNS Nameservers") starts a list; following unlabelled rows belong to it.
|
|
62
|
+
*/
|
|
63
|
+
function parseRecordTable(section) {
|
|
64
|
+
const fields = {};
|
|
65
|
+
const lists = {};
|
|
66
|
+
let currentList = null;
|
|
67
|
+
|
|
68
|
+
// Split on <tr> rather than matching <tr>…</tr>: PKNIC's markup leaves some rows unclosed.
|
|
69
|
+
for (const row of section.split(/<tr\b[^>]*>/i).slice(1)) {
|
|
70
|
+
const cells = row
|
|
71
|
+
.replace(/<\/table>[\s\S]*$/i, '')
|
|
72
|
+
.split(/<t[dh]\b[^>]*>/i)
|
|
73
|
+
.slice(1)
|
|
74
|
+
.map(toText);
|
|
75
|
+
if (cells.length < 4) continue;
|
|
76
|
+
|
|
77
|
+
const label = cells[1].replace(/:$/, '').trim();
|
|
78
|
+
const value = cells[3];
|
|
79
|
+
|
|
80
|
+
if (label && value) {
|
|
81
|
+
fields[label] = value;
|
|
82
|
+
currentList = null;
|
|
83
|
+
} else if (label) {
|
|
84
|
+
currentList = label;
|
|
85
|
+
lists[label] = [];
|
|
86
|
+
} else if (value && currentList) {
|
|
87
|
+
lists[currentList].push(value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { fields, lists };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function findField(fields, pattern) {
|
|
95
|
+
const key = Object.keys(fields).find((k) => pattern.test(k));
|
|
96
|
+
return key === undefined ? undefined : fields[key];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse a PKNIC lookup response.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} html
|
|
103
|
+
* @returns {import('./index.js').LookupResult}
|
|
104
|
+
*/
|
|
105
|
+
export function parse(html) {
|
|
106
|
+
if (typeof html !== 'string') {
|
|
107
|
+
throw new TypeError('parse() expects an HTML string');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const markers = new Set([...html.matchAll(/<XPC_([A-Z_]+)\s*\/?>/g)].map((m) => m[1]));
|
|
111
|
+
const section = mainSection(html);
|
|
112
|
+
|
|
113
|
+
const base = {
|
|
114
|
+
domain: null,
|
|
115
|
+
status: null,
|
|
116
|
+
registered: false,
|
|
117
|
+
available: false,
|
|
118
|
+
createDate: null,
|
|
119
|
+
expireDate: null,
|
|
120
|
+
nameservers: [],
|
|
121
|
+
reasons: [],
|
|
122
|
+
fields: {},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// --- Available -----------------------------------------------------------
|
|
126
|
+
const notFound = /Domain not found:\s*(?:<[^>]*>\s*)*([^<\s]+)/i.exec(section);
|
|
127
|
+
if (markers.has('RESULT_NOTFOUND') || markers.has('DOMAIN_MAYBE_AVAILABLE') || notFound) {
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
domain: notFound ? toText(notFound[1]).toLowerCase() : null,
|
|
131
|
+
status: 'available',
|
|
132
|
+
available: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Invalid / not registrable --------------------------------------------
|
|
137
|
+
const errors = [...section.matchAll(/<li\b[^>]*class=["']?error["']?[^>]*>([\s\S]*?)(?=<\/li>|<li\b|<\/ul>|<br|<\/div>)/gi)]
|
|
138
|
+
.map((m) => toText(m[1]))
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
if (errors.length || /has the following issues/i.test(section)) {
|
|
141
|
+
const searched = /Your searched domain:\s*<b>([\s\S]*?)<\/b>/i.exec(section);
|
|
142
|
+
const domain = searched ? toText(searched[1]).toLowerCase() : null;
|
|
143
|
+
// Reasons are prefixed with the domain ("x.pk: Invalid Domain name …"); drop that prefix.
|
|
144
|
+
const reasons = errors.map((e) =>
|
|
145
|
+
domain && e.toLowerCase().startsWith(`${domain}:`) ? e.slice(domain.length + 1).trim() : e,
|
|
146
|
+
);
|
|
147
|
+
return { ...base, domain, status: 'invalid', reasons };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- Registered ------------------------------------------------------------
|
|
151
|
+
const heading = /The Domain record for\s*([^<\s]+)/i.exec(section);
|
|
152
|
+
if (heading || /Domain is Registered/i.test(section)) {
|
|
153
|
+
const { fields, lists } = parseRecordTable(section);
|
|
154
|
+
const created = findField(fields, /^create/i);
|
|
155
|
+
const expires = findField(fields, /^expir/i);
|
|
156
|
+
const nsKey = Object.keys(lists).find((k) => /name\s*servers?/i.test(k));
|
|
157
|
+
const domain = findField(fields, /^domain name$/i) ?? (heading ? toText(heading[1]) : null);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...base,
|
|
161
|
+
domain: domain ? domain.toLowerCase() : null,
|
|
162
|
+
status: 'registered',
|
|
163
|
+
registered: true,
|
|
164
|
+
createDate: created ? toIsoDate(created) : null,
|
|
165
|
+
expireDate: expires ? toIsoDate(expires) : null,
|
|
166
|
+
nameservers: nsKey ? lists[nsKey] : [],
|
|
167
|
+
fields,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new WhoisPkError(
|
|
172
|
+
'Unrecognised response from PKNIC. The lookup page layout may have changed; please open an issue at https://github.com/abbassiddiqi/whoispk/issues',
|
|
173
|
+
{ code: 'PARSE_ERROR' },
|
|
174
|
+
);
|
|
175
|
+
}
|
package/.npmignore
DELETED
package/.travis.yml
DELETED
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require('./lib/whoispk.js');
|
package/lib/whoispk.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const axios = require('axios');
|
|
4
|
-
const cheerio = require('cheerio');
|
|
5
|
-
const changeCase = require('change-case');
|
|
6
|
-
const querystring = require('querystring');
|
|
7
|
-
|
|
8
|
-
exports.lookup = lookup;
|
|
9
|
-
|
|
10
|
-
function lookup (addr, options, done) {
|
|
11
|
-
|
|
12
|
-
if (typeof addr === "undefined" && !addr) {
|
|
13
|
-
throw new Error("address cannot be empty");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// A callback function is provided
|
|
17
|
-
if (typeof done === "undefined" && typeof options === "function") {
|
|
18
|
-
done = options;
|
|
19
|
-
options = {};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Called via Promise, no Callback function given
|
|
23
|
-
if (
|
|
24
|
-
typeof done === "undefined" &&
|
|
25
|
-
(typeof options === "object" || typeof options === "undefined")
|
|
26
|
-
) {
|
|
27
|
-
options = {};
|
|
28
|
-
return fetchDomainInfo(addr);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
Promise.resolve(fetchDomainInfo(addr))
|
|
32
|
-
.then(function (domainInfo) {
|
|
33
|
-
done(null, domainInfo);
|
|
34
|
-
})
|
|
35
|
-
.catch(function (err) {
|
|
36
|
-
done(err);
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function fetchDomainInfo (addr) {
|
|
41
|
-
|
|
42
|
-
const pknicUrl = "https://pk6.pknic.net.pk/pk5/lookup.PK";
|
|
43
|
-
|
|
44
|
-
return axios
|
|
45
|
-
.post(pknicUrl, querystring.stringify({ name: addr }))
|
|
46
|
-
.then(function (response) {
|
|
47
|
-
|
|
48
|
-
const domainInfo = parseHTMLResponse(response.data);
|
|
49
|
-
domainInfo.address = addr;
|
|
50
|
-
|
|
51
|
-
return domainInfo;
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function parseHTMLResponse (data) {
|
|
56
|
-
|
|
57
|
-
const domainInfo = {
|
|
58
|
-
isFound: false
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const $ = cheerio.load(data);
|
|
62
|
-
const $formBox = $(".formbox","#Tmain");
|
|
63
|
-
const $whiteBox = $(".whitebox", "#Tmain");
|
|
64
|
-
|
|
65
|
-
if ($formBox.length) {
|
|
66
|
-
// Domain Not Found
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if ($whiteBox.length) {
|
|
70
|
-
// Domain Found
|
|
71
|
-
|
|
72
|
-
domainInfo.isFound = true;
|
|
73
|
-
domainInfo.data = {};
|
|
74
|
-
|
|
75
|
-
const $rows = $whiteBox.find("tr");
|
|
76
|
-
|
|
77
|
-
let lastHeading = "";
|
|
78
|
-
|
|
79
|
-
$rows.each(function(i, row) {
|
|
80
|
-
|
|
81
|
-
const $row = cheerio(row);
|
|
82
|
-
|
|
83
|
-
if ($row.children().length == 4) {
|
|
84
|
-
|
|
85
|
-
let key = $row.children().eq(1).text().replace(/\s+/g,' ').trim();
|
|
86
|
-
let value = $row.children().eq(3).text().replace(/\s+/g,' ').trim();
|
|
87
|
-
|
|
88
|
-
key = changeCase.snakeCase(key);
|
|
89
|
-
key = key.replace(/\:$/,'');
|
|
90
|
-
|
|
91
|
-
if (key && value) {
|
|
92
|
-
if (key == "registrant_name") { key = "registrant"};
|
|
93
|
-
if (key == "agent_organization") {key = "agent_org"};
|
|
94
|
-
domainInfo.data[key] = value;
|
|
95
|
-
}
|
|
96
|
-
if (key && !value) {
|
|
97
|
-
lastHeading = key;
|
|
98
|
-
domainInfo.data[lastHeading] = [];
|
|
99
|
-
}
|
|
100
|
-
if (!key && value) {
|
|
101
|
-
domainInfo.data[lastHeading].push(value);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return domainInfo;
|
|
109
|
-
}
|
package/test/test.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
const chai = require('chai');
|
|
2
|
-
const expect = require('chai').expect;
|
|
3
|
-
|
|
4
|
-
const whoispk = require('../lib/whoispk.js');
|
|
5
|
-
|
|
6
|
-
describe('whoispk', function () {
|
|
7
|
-
|
|
8
|
-
describe('lookup', function () {
|
|
9
|
-
|
|
10
|
-
it('returns a promise', function () {
|
|
11
|
-
const promise = whoispk.lookup('daraz.pk');
|
|
12
|
-
expect(promise.then).to.be.a('function');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('calls callback', function (done) {
|
|
16
|
-
this.timeout(4000);
|
|
17
|
-
whoispk.lookup('daraz.pk', function (err, domainInfo) {
|
|
18
|
-
expect(err).to.be.null;
|
|
19
|
-
expect(domainInfo).to.be.an('object');
|
|
20
|
-
|
|
21
|
-
done();
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('returns domain information via Promise', function (done) {
|
|
26
|
-
this.timeout(4000);
|
|
27
|
-
whoispk
|
|
28
|
-
.lookup('daraz.pk')
|
|
29
|
-
.then(function (domainInfo) {
|
|
30
|
-
expect(domainInfo).to.be.an('object');
|
|
31
|
-
expect(domainInfo.isFound).to.be.true;
|
|
32
|
-
done();
|
|
33
|
-
})
|
|
34
|
-
.catch(function (err) {
|
|
35
|
-
done(err);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('returns domain information via Callback', function (done) {
|
|
40
|
-
this.timeout(4000);
|
|
41
|
-
whoispk
|
|
42
|
-
.lookup('daraz.pk', function (err, domainInfo) {
|
|
43
|
-
if (err) {
|
|
44
|
-
done(err);
|
|
45
|
-
} else {
|
|
46
|
-
expect(domainInfo).to.be.an('object');
|
|
47
|
-
expect(domainInfo.isFound).to.be.true;
|
|
48
|
-
done();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('returns domain not found via Promise', function (done) {
|
|
54
|
-
this.timeout(4000);
|
|
55
|
-
whoispk
|
|
56
|
-
.lookup('a.pk')
|
|
57
|
-
.then(function (domainInfo) {
|
|
58
|
-
expect(domainInfo.isFound).to.be.false;
|
|
59
|
-
done();
|
|
60
|
-
})
|
|
61
|
-
.catch(function (err) {
|
|
62
|
-
done(err);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns domain not found via Callback', function (done) {
|
|
67
|
-
this.timeout(4000);
|
|
68
|
-
whoispk.lookup('a.pk', function (err, domainInfo) {
|
|
69
|
-
expect(err).to.be.null;
|
|
70
|
-
expect(domainInfo.isFound).to.be.false;
|
|
71
|
-
done();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
});
|