@hyperwatch/hyperwatch 4.2.0 → 5.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 +9 -4
- package/config/apache_syslog_example.js +29 -0
- package/config/default.js +2 -2
- package/config/example.js +5 -5
- package/config/express_websocket_example.js +28 -0
- package/config/websocket_client_example.js +52 -0
- package/docs/configuration.md +67 -6
- package/docs/express-embedding.md +137 -0
- package/docs/input.md +50 -24
- package/docs/tutorials/apache_input.md +16 -14
- package/docs/tutorials/express_input.md +10 -8
- package/package.json +42 -35
- package/scripts/fetch-anthropic-ips.js +60 -0
- package/scripts/fetch-cloudflare-ips.js +27 -0
- package/scripts/fetch-cloudfront-ips.js +29 -0
- package/scripts/fetch-openai-ips.js +34 -0
- package/src/app/api.js +149 -8
- package/src/app/index.js +8 -4
- package/src/app/mount.js +115 -0
- package/src/app/websocket.js +39 -10
- package/src/app/ws-server.js +123 -0
- package/src/constants.js +17 -5
- package/src/data/amazon-searchbot-ips.json +818 -0
- package/src/data/amazon-user-ips.json +1025 -0
- package/src/data/amazonbot-ips.json +1294 -0
- package/src/data/chatgpt-user-ips.json +231 -0
- package/src/data/claude-bot-ips.json +28 -0
- package/src/data/cloudflare-ips.json +24 -0
- package/src/data/cloudfront-ips.json +245 -0
- package/src/data/gptbot-ips.json +20 -0
- package/src/data/openai-searchbot-ips.json +41 -0
- package/src/index.js +24 -3
- package/src/input/http.js +4 -0
- package/src/input/syslog.js +5 -1
- package/src/input/websocket.js +35 -19
- package/src/lib/aggregator.js +169 -20
- package/src/lib/formatter.js +10 -1
- package/src/lib/index.js +2 -0
- package/src/lib/log-buffer.js +45 -0
- package/src/lib/persistence.js +82 -0
- package/src/lib/pipeline.js +122 -11
- package/src/lib/recent-map.js +23 -0
- package/src/lib/speed.js +43 -3
- package/src/lib/util.js +17 -1
- package/src/modules/address.js +59 -2
- package/src/modules/agent.js +1 -1
- package/src/modules/dnsbl.js +11 -1
- package/src/modules/history.js +62 -0
- package/src/modules/identity.js +154 -26
- package/src/modules/index.js +16 -5
- package/src/modules/language.js +1 -1
- package/src/modules/signature.js +64 -18
- package/src/modules/sparkline.js +7 -3
- package/src/modules/status.js +13 -8
- package/src/plugins/proxy.js +10 -203
- package/src/script.js +0 -1
- package/scripts/cloudfront-ips.js +0 -56
|
@@ -10,10 +10,10 @@ Let's start.
|
|
|
10
10
|
|
|
11
11
|
On the same server where Apache is running, or on a server that is reachable by it, install the Hyperwatch processor.
|
|
12
12
|
|
|
13
|
-
As a
|
|
13
|
+
As a prerequisite, you'll need Node.js >= 24. We recommend [nvm](https://github.com/nvm-sh/nvm).
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
nvm install
|
|
16
|
+
nvm install 24
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
#### Install from npm
|
|
@@ -24,7 +24,7 @@ npm install -g @hyperwatch/hyperwatch
|
|
|
24
24
|
|
|
25
25
|
#### Install from Git
|
|
26
26
|
|
|
27
|
-
Alternatively, for
|
|
27
|
+
Alternatively, for development purpose, you can use Git and clone the public repository:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
git clone https://github.com/hyperwatch/hyperwatch.git
|
|
@@ -34,13 +34,13 @@ npm install
|
|
|
34
34
|
|
|
35
35
|
### Configure Hyperwatch
|
|
36
36
|
|
|
37
|
-
In our suggested configuration, Hyperwatch will be listening for access logs in the
|
|
37
|
+
In our suggested configuration, Hyperwatch will be listening for access logs in the `hyperwatch_combined` format on port `1518`.
|
|
38
38
|
|
|
39
|
-
We always
|
|
39
|
+
We always recommend using the `hyperwatch_combined` format, which is logging more detailed information and allows for a much better analysis than the regular `combined` format.
|
|
40
40
|
|
|
41
|
-
To get more familiar, you can inspect default and example configurations in [`config/default.js`](
|
|
41
|
+
To get more familiar, you can inspect default and example configurations in [`config/default.js`](../../config/default.js) and [`config/example.js`](../../config/example.js) file.
|
|
42
42
|
|
|
43
|
-
Now, you can create your own configuration in
|
|
43
|
+
Now, you can create your own configuration in `apache_syslog_example.js` (a complete version is available in [`config/apache_syslog_example.js`](../../config/apache_syslog_example.js)):
|
|
44
44
|
|
|
45
45
|
```javascript
|
|
46
46
|
module.exports = function (hyperwatch) {
|
|
@@ -61,7 +61,7 @@ module.exports = function (hyperwatch) {
|
|
|
61
61
|
|
|
62
62
|
### Configure Apache
|
|
63
63
|
|
|
64
|
-
First, if you're following our recommendation and opted for the
|
|
64
|
+
First, if you're following our recommendation and opted for the `hyperwatch_combined` format, you need to define it in the Apache configuration. This will not replace the standard log format, just create an additional one.
|
|
65
65
|
|
|
66
66
|
```
|
|
67
67
|
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" \"%{Accept}i\" \"%{Accept-Charset}i\" \"%{Accept-Encoding}i\" \"%{Accept-Language}i\" \"%{Connection}i\" \"%{Dnt}i\" \"%{From}i\" \"%{Host}i\"" hyperwatch_combined
|
|
@@ -79,13 +79,13 @@ Note: This is known to be working on _Ubuntu 16.04, 18.04_ with _logger 2.27.1,
|
|
|
79
79
|
|
|
80
80
|
In this example, there are 3 important things:
|
|
81
81
|
|
|
82
|
-
1. If Hyperwatch is running on the same server, we can use
|
|
83
|
-
If it's on a different server, replace
|
|
84
|
-
2. We configured Hyperwatch to listen for syslog messages in the
|
|
82
|
+
1. If Hyperwatch is running on the same server, we can use `localhost` as IP address.
|
|
83
|
+
If it's on a different server, replace `localhost` by the proper private or public IP address.
|
|
84
|
+
2. We configured Hyperwatch to listen for syslog messages in the `hyperwatch_combined` format on port `1518`.
|
|
85
85
|
We're properly passing that port in the configuration
|
|
86
|
-
3. Finally, we're asking Apache to use the
|
|
86
|
+
3. Finally, we're asking Apache to use the `hyperwatch_combined` log format we previously configured.
|
|
87
87
|
|
|
88
|
-
Don't forget to reload
|
|
88
|
+
Don't forget to reload Apache with the updated configuration. On Ubuntu, it would be:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
service apache2 reload
|
|
@@ -101,4 +101,6 @@ hyperwatch apache_syslog_example.js
|
|
|
101
101
|
|
|
102
102
|
### Browse the interface
|
|
103
103
|
|
|
104
|
-
Now, you can point your browser to the IP/port where Hyperwatch is running. If you see
|
|
104
|
+
Now, you can point your browser to the `/status` page on the IP/port where Hyperwatch is running (e.g. `http://localhost:3000/status`). If you see traffic going through your input, congrats you made it!
|
|
105
|
+
|
|
106
|
+
To watch the logs live at `/logs/main` and explore aggregations such as `/addresses` or `/identities`, activate the corresponding modules. See [Global Configuration](../configuration.md#modules).
|
|
@@ -10,10 +10,10 @@ Let's start!
|
|
|
10
10
|
|
|
11
11
|
On the same server where the Node/Express application is running, or on a server that is reachable by it, install the Hyperwatch processor.
|
|
12
12
|
|
|
13
|
-
As a
|
|
13
|
+
As a prerequisite, you'll need Node.js >= 24. We recommend [nvm](https://github.com/nvm-sh/nvm).
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
nvm install
|
|
16
|
+
nvm install 24
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
#### Install from npm
|
|
@@ -24,7 +24,7 @@ npm install -g @hyperwatch/hyperwatch
|
|
|
24
24
|
|
|
25
25
|
#### Install from Git
|
|
26
26
|
|
|
27
|
-
Alternatively, for
|
|
27
|
+
Alternatively, for development purpose, you can use Git and clone the public repository:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
git clone https://github.com/hyperwatch/hyperwatch.git
|
|
@@ -38,7 +38,7 @@ In our suggested configuration, Hyperwatch will be listening for access logs usi
|
|
|
38
38
|
|
|
39
39
|
All communications between your Node/Express application and Hyperwatch will be happening in clear, please only use that setup on your internal network. If on the public internet, we're advising to use the Websocket Secure protocol (wss) which is straightforward but out of the scope of this tutorial.
|
|
40
40
|
|
|
41
|
-
Now, you can create your own configuration in
|
|
41
|
+
Now, you can create your own configuration in `express_websocket_example.js` (a complete version is available in [`config/express_websocket_example.js`](../../config/express_websocket_example.js)):
|
|
42
42
|
|
|
43
43
|
```javascript
|
|
44
44
|
module.exports = function (hyperwatch) {
|
|
@@ -77,10 +77,10 @@ app.use(hyperwatchExpressLogger('websocket', 'ws://localhost:3000/input/log'));
|
|
|
77
77
|
|
|
78
78
|
In this example, there are 3 important things:
|
|
79
79
|
|
|
80
|
-
1. If Hyperwatch is running on the same server, we can use
|
|
81
|
-
If it's on a different server, replace
|
|
80
|
+
1. If Hyperwatch is running on the same server, we can use `localhost` as IP address.
|
|
81
|
+
If it's on a different server, replace `localhost` by the proper private or public IP address.
|
|
82
82
|
2. Replace the port (here `3000`) by the relevant one, it should be the main port where Hyperwatch is running.
|
|
83
|
-
3. Finally, the path `/input/log` should match the one configured on Hyperwatch side, If you're following this tutorial from start to
|
|
83
|
+
3. Finally, the path `/input/log` should match the one configured on Hyperwatch side, If you're following this tutorial from start to end, nothing to change!
|
|
84
84
|
|
|
85
85
|
Now, that you added and configured the Hyperwatch middleware, you can deploy and restart your application.
|
|
86
86
|
|
|
@@ -96,4 +96,6 @@ hyperwatch express_websocket_example.js
|
|
|
96
96
|
|
|
97
97
|
### Browse the interface
|
|
98
98
|
|
|
99
|
-
Now, you can point your browser to the IP/port where Hyperwatch is running. If you see
|
|
99
|
+
Now, you can point your browser to the `/status` page on the IP/port where Hyperwatch is running (e.g. `http://localhost:3000/status`). If you see traffic going through your input, congrats you made it!
|
|
100
|
+
|
|
101
|
+
To watch the logs live at `/logs/main` and explore aggregations such as `/addresses` or `/identities`, activate the corresponding modules. See [Global Configuration](../configuration.md#modules).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperwatch/hyperwatch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Open Source HTTP Traffic Manager",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "François Hodierne <francois@hodierne.net>",
|
|
@@ -10,16 +10,25 @@
|
|
|
10
10
|
],
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/hyperwatch/hyperwatch.git"
|
|
13
|
+
"url": "git+https://github.com/hyperwatch/hyperwatch.git"
|
|
14
14
|
},
|
|
15
15
|
"main": "hyperwatch.js",
|
|
16
16
|
"bin": {
|
|
17
|
-
"hyperwatch": "
|
|
17
|
+
"hyperwatch": "bin/hyperwatch"
|
|
18
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"config",
|
|
22
|
+
"docs",
|
|
23
|
+
"scripts",
|
|
24
|
+
"src",
|
|
25
|
+
"start.js"
|
|
26
|
+
],
|
|
19
27
|
"scripts": {
|
|
20
28
|
"depcheck": "depcheck",
|
|
21
|
-
"lint": "eslint
|
|
22
|
-
"lint:fix": "eslint
|
|
29
|
+
"lint": "eslint",
|
|
30
|
+
"lint:fix": "eslint --fix",
|
|
31
|
+
"prepare": "husky",
|
|
23
32
|
"prettier": "prettier \"**/*.@(js|json|md|yml)\"",
|
|
24
33
|
"prettier:check": "npm run prettier -- --list-different",
|
|
25
34
|
"prettier:write": "npm run prettier -- --write",
|
|
@@ -29,52 +38,50 @@
|
|
|
29
38
|
"dependencies": {
|
|
30
39
|
"@hyperwatch/useragent": "^3.9.3",
|
|
31
40
|
"accept-language-parser": "^1.5.0",
|
|
32
|
-
"ajv": "^8.
|
|
33
|
-
"ajv-formats": "^
|
|
34
|
-
"chalk": "^
|
|
41
|
+
"ajv": "^8.20.0",
|
|
42
|
+
"ajv-formats": "^3.0.1",
|
|
43
|
+
"chalk": "^6.0.0",
|
|
35
44
|
"country-code-emoji": "^2.3.0",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"express
|
|
40
|
-
"geoip-lite": "^
|
|
41
|
-
"immutable": "^
|
|
42
|
-
"ip-cidr": "^
|
|
43
|
-
"lodash": "^4.
|
|
44
|
-
"lru-cache": "^
|
|
45
|
-
"micro-strptime": "^0.
|
|
45
|
+
"csv-stringify": "^6.8.3",
|
|
46
|
+
"debug": "^4.4.3",
|
|
47
|
+
"dnsbl": "^5.1.1",
|
|
48
|
+
"express": "^5.2.1",
|
|
49
|
+
"geoip-lite": "^2.0.3",
|
|
50
|
+
"immutable": "^5.1.9",
|
|
51
|
+
"ip-cidr": "^4.0.2",
|
|
52
|
+
"lodash": "^4.18.1",
|
|
53
|
+
"lru-cache": "^11.5.2",
|
|
54
|
+
"micro-strptime": "^1.0.0",
|
|
46
55
|
"proxy-addr": "^2.0.7",
|
|
47
56
|
"rc": "^1.2.8",
|
|
48
|
-
"syslog-parse": "^
|
|
57
|
+
"syslog-parse": "^2.0.0",
|
|
49
58
|
"tail": "^2.2.6",
|
|
50
|
-
"
|
|
51
|
-
"ws": "^8.14.2"
|
|
59
|
+
"ws": "^8.21.3"
|
|
52
60
|
},
|
|
53
61
|
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
54
63
|
"depcheck": "^1.4.7",
|
|
55
|
-
"eslint": "^
|
|
56
|
-
"eslint-plugin-import": "^
|
|
57
|
-
"eslint-plugin-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"prettier": "^3.
|
|
64
|
+
"eslint": "^10.9.1",
|
|
65
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
66
|
+
"eslint-plugin-n": "^18.3.0",
|
|
67
|
+
"globals": "^17.11.0",
|
|
68
|
+
"husky": "^9.1.7",
|
|
69
|
+
"lint-staged": "^17.4.1",
|
|
70
|
+
"mocha": "^12.0.0",
|
|
71
|
+
"prettier": "^3.9.6",
|
|
63
72
|
"prettier-package-json": "^2.8.0"
|
|
64
73
|
},
|
|
65
74
|
"engines": {
|
|
66
|
-
"node": ">=
|
|
75
|
+
"node": ">=24"
|
|
76
|
+
},
|
|
77
|
+
"allowScripts": {
|
|
78
|
+
"unrs-resolver": false
|
|
67
79
|
},
|
|
68
80
|
"depcheck": {
|
|
69
81
|
"ignores": [
|
|
70
82
|
"prettier-package-json"
|
|
71
83
|
]
|
|
72
84
|
},
|
|
73
|
-
"husky": {
|
|
74
|
-
"hooks": {
|
|
75
|
-
"pre-commit": "lint-staged"
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
85
|
"lint-staged": {
|
|
79
86
|
"*.{js,json,md,yml}": [
|
|
80
87
|
"prettier --write"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const dataDir = path.join(__dirname, '..', 'src', 'data');
|
|
5
|
+
|
|
6
|
+
// Anthropic publishes a single list covering all Claude crawlers
|
|
7
|
+
// (ClaudeBot, Claude-User, Claude-SearchBot).
|
|
8
|
+
const sources = [
|
|
9
|
+
{ name: 'claude-bot-ips', url: 'https://claude.com/crawling/bots.json' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function extractCidrs(data) {
|
|
13
|
+
const prefixes = Array.isArray(data) ? data : data.prefixes;
|
|
14
|
+
if (!Array.isArray(prefixes)) {
|
|
15
|
+
throw new Error('unexpected payload: no prefixes array');
|
|
16
|
+
}
|
|
17
|
+
return prefixes
|
|
18
|
+
.map((prefix) => {
|
|
19
|
+
if (typeof prefix === 'string') {
|
|
20
|
+
return prefix;
|
|
21
|
+
}
|
|
22
|
+
return prefix.ipv4Prefix || prefix.ipv6Prefix || prefix.ip_prefix;
|
|
23
|
+
})
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((cidr) => {
|
|
26
|
+
if (cidr.includes('/')) {
|
|
27
|
+
return cidr;
|
|
28
|
+
}
|
|
29
|
+
return cidr.includes(':') ? `${cidr}/128` : `${cidr}/32`;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function fetchAndStore({ name, url }) {
|
|
34
|
+
const res = await fetch(url);
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
console.error(`Failed to fetch ${name}: ${res.status} ${res.statusText}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const cidrs = extractCidrs(await res.json());
|
|
41
|
+
if (cidrs.length === 0) {
|
|
42
|
+
console.error(`Failed to fetch ${name}: empty list, keeping current file`);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const filePath = path.join(dataDir, `${name}.json`);
|
|
47
|
+
fs.writeFileSync(filePath, `${JSON.stringify(cidrs, null, 2)}\n`);
|
|
48
|
+
console.log(`${name}: ${cidrs.length} CIDRs`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
await fetchAndStore(source);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
main().catch((error) => {
|
|
58
|
+
console.error(error);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const dataDir = path.join(__dirname, '..', 'src', 'data');
|
|
5
|
+
|
|
6
|
+
async function fetchLines(url) {
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
throw new Error(`Failed to fetch ${url}: ${res.status}`);
|
|
10
|
+
}
|
|
11
|
+
const text = await res.text();
|
|
12
|
+
return text.trim().split('\n').filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const ipv4 = await fetchLines('https://www.cloudflare.com/ips-v4/');
|
|
17
|
+
const ipv6 = await fetchLines('https://www.cloudflare.com/ips-v6/');
|
|
18
|
+
const cidrs = [...ipv4, ...ipv6];
|
|
19
|
+
|
|
20
|
+
const filePath = path.join(dataDir, 'cloudflare-ips.json');
|
|
21
|
+
fs.writeFileSync(filePath, `${JSON.stringify(cidrs, null, 2)}\n`);
|
|
22
|
+
console.log(
|
|
23
|
+
`cloudflare-ips: ${cidrs.length} CIDRs (${ipv4.length} IPv4, ${ipv6.length} IPv6)`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const dataDir = path.join(__dirname, '..', 'src', 'data');
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const response = await fetch(
|
|
8
|
+
'https://ip-ranges.amazonaws.com/ip-ranges.json'
|
|
9
|
+
);
|
|
10
|
+
const ipRanges = await response.json();
|
|
11
|
+
|
|
12
|
+
const ipv4 = ipRanges.prefixes
|
|
13
|
+
.filter((p) => p.service === 'CLOUDFRONT')
|
|
14
|
+
.map((p) => p.ip_prefix);
|
|
15
|
+
|
|
16
|
+
const ipv6 = ipRanges.ipv6_prefixes
|
|
17
|
+
.filter((p) => p.service === 'CLOUDFRONT')
|
|
18
|
+
.map((p) => p.ipv6_prefix);
|
|
19
|
+
|
|
20
|
+
const cidrs = [...ipv4, ...ipv6];
|
|
21
|
+
|
|
22
|
+
const filePath = path.join(dataDir, 'cloudfront-ips.json');
|
|
23
|
+
fs.writeFileSync(filePath, `${JSON.stringify(cidrs, null, 2)}\n`);
|
|
24
|
+
console.log(
|
|
25
|
+
`cloudfront-ips: ${cidrs.length} CIDRs (${ipv4.length} IPv4, ${ipv6.length} IPv6)`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const dataDir = path.join(__dirname, '..', 'src', 'data');
|
|
5
|
+
|
|
6
|
+
const sources = [
|
|
7
|
+
{ name: 'chatgpt-user-ips', url: 'https://openai.com/chatgpt-user.json' },
|
|
8
|
+
{ name: 'gptbot-ips', url: 'https://openai.com/gptbot.json' },
|
|
9
|
+
{ name: 'openai-searchbot-ips', url: 'https://openai.com/searchbot.json' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
async function fetchAndStore({ name, url }) {
|
|
13
|
+
const res = await fetch(url);
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
console.error(`Failed to fetch ${name}: ${res.status} ${res.statusText}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
const cidrs = data.prefixes.map((p) => {
|
|
20
|
+
const cidr = p.ipv4Prefix || p.ip_prefix;
|
|
21
|
+
return cidr.includes('/') ? cidr : `${cidr}/32`;
|
|
22
|
+
});
|
|
23
|
+
const filePath = path.join(dataDir, `${name}.json`);
|
|
24
|
+
fs.writeFileSync(filePath, `${JSON.stringify(cidrs, null, 2)}\n`);
|
|
25
|
+
console.log(`${name}: ${cidrs.length} CIDRs`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
for (const source of sources) {
|
|
30
|
+
await fetchAndStore(source);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main().catch(console.error);
|
package/src/app/api.js
CHANGED
|
@@ -1,19 +1,137 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
|
|
5
|
+
const { stringify } = require('csv-stringify/sync');
|
|
4
6
|
const express = require('express');
|
|
5
|
-
const uuid = require('uuid');
|
|
6
7
|
|
|
7
8
|
const monitoring = require('../lib/monitoring');
|
|
9
|
+
const persistence = require('../lib/persistence');
|
|
10
|
+
const pipeline = require('../lib/pipeline');
|
|
8
11
|
const { formatTable } = require('../lib/util');
|
|
9
12
|
const stylesheet = require('../stylesheet');
|
|
10
13
|
|
|
14
|
+
const wsServer = require('./ws-server');
|
|
15
|
+
|
|
11
16
|
const script = fs.readFileSync(path.join(__dirname, '..', 'script.js'));
|
|
12
17
|
|
|
13
18
|
const app = express();
|
|
14
19
|
|
|
20
|
+
// WebSocket upgrades dispatched by mount() when Hyperwatch is embedded
|
|
21
|
+
app.use(wsServer.middleware);
|
|
22
|
+
|
|
15
23
|
app.use(express.json());
|
|
16
24
|
|
|
25
|
+
function renderHtmlTree(node) {
|
|
26
|
+
const label = [];
|
|
27
|
+
if (node.name) {
|
|
28
|
+
label.push(`<strong>${node.name}</strong>`);
|
|
29
|
+
}
|
|
30
|
+
if (node.op) {
|
|
31
|
+
label.push(`<span class="op">[${node.op}]</span>`);
|
|
32
|
+
}
|
|
33
|
+
if (node.module) {
|
|
34
|
+
label.push(`<span class="module">(${node.module})</span>`);
|
|
35
|
+
}
|
|
36
|
+
if (node.fnName) {
|
|
37
|
+
label.push(`<span class="fn">${node.fnName}</span>`);
|
|
38
|
+
}
|
|
39
|
+
if (node.label) {
|
|
40
|
+
label.push(`<span class="label">${node.label}</span>`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let html = `<li>${label.join(' ')}`;
|
|
44
|
+
if (node.children && node.children.length > 0) {
|
|
45
|
+
html += '<ul>';
|
|
46
|
+
for (const child of node.children) {
|
|
47
|
+
html += renderHtmlTree(child);
|
|
48
|
+
}
|
|
49
|
+
html += '</ul>';
|
|
50
|
+
}
|
|
51
|
+
html += '</li>';
|
|
52
|
+
return html;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderHtmlInputs(inputs) {
|
|
56
|
+
if (!inputs || inputs.length === 0) {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
let html = '<ul>';
|
|
60
|
+
for (const input of inputs) {
|
|
61
|
+
html += `<li><strong>${input.name}</strong> <span class="op">[input]</span>`;
|
|
62
|
+
if (input.status) {
|
|
63
|
+
html += ` <span class="module">(${input.status})</span>`;
|
|
64
|
+
}
|
|
65
|
+
html += ` accepted: ${input.accepted}, rejected: ${input.rejected}`;
|
|
66
|
+
if (input.tree) {
|
|
67
|
+
html += `<ul>${renderHtmlTree(input.tree)}</ul>`;
|
|
68
|
+
}
|
|
69
|
+
html += '</li>';
|
|
70
|
+
}
|
|
71
|
+
html += '</ul>';
|
|
72
|
+
return html;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
app.get('/nodes{.:format}', (req, res) => {
|
|
76
|
+
const nodes = Object.keys(pipeline.nodes);
|
|
77
|
+
const format = req.params.format;
|
|
78
|
+
const view = req.query.view;
|
|
79
|
+
|
|
80
|
+
if (format && !['csv', 'json'].includes(format)) {
|
|
81
|
+
res.sendStatus(404);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (format === 'csv') {
|
|
86
|
+
const csv = stringify(
|
|
87
|
+
nodes.map((name) => ({ name })),
|
|
88
|
+
{
|
|
89
|
+
header: true,
|
|
90
|
+
columns: ['name'],
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
94
|
+
res.setHeader('Content-Disposition', 'attachment; filename="nodes.csv"');
|
|
95
|
+
res.send(csv);
|
|
96
|
+
} else if (format === 'json') {
|
|
97
|
+
if (view === 'tree') {
|
|
98
|
+
res.json(pipeline.getTree());
|
|
99
|
+
} else {
|
|
100
|
+
res.json(nodes);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
104
|
+
if (view === 'tree') {
|
|
105
|
+
const tree = pipeline.getTree();
|
|
106
|
+
res.send(
|
|
107
|
+
`<!DOCTYPE html>
|
|
108
|
+
<html>
|
|
109
|
+
<head>
|
|
110
|
+
<style>${stylesheet}
|
|
111
|
+
.op { color: #666; }
|
|
112
|
+
.module { color: #0a0; }
|
|
113
|
+
.fn { color: #00a; }
|
|
114
|
+
.label { color: #a50; }
|
|
115
|
+
ul { list-style: none; padding-left: 1.5em; }
|
|
116
|
+
</style>
|
|
117
|
+
</head>
|
|
118
|
+
<body>${renderHtmlInputs(tree.inputs)}<ul>${renderHtmlTree(tree)}</ul></body>
|
|
119
|
+
</html>`
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
res.send(
|
|
123
|
+
`<!DOCTYPE html>
|
|
124
|
+
<html>
|
|
125
|
+
<head>
|
|
126
|
+
<style>${stylesheet}</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>${formatTable(nodes.map((name) => ({ name })))}</body>
|
|
129
|
+
</html>`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
17
135
|
app.streamToHttp = (
|
|
18
136
|
endpoint,
|
|
19
137
|
stream,
|
|
@@ -47,7 +165,7 @@ app.streamToHttp = (
|
|
|
47
165
|
updateMonitoringStatus();
|
|
48
166
|
|
|
49
167
|
app.get(endpoint, (req, res) => {
|
|
50
|
-
const requestId =
|
|
168
|
+
const requestId = crypto.randomUUID();
|
|
51
169
|
requests[requestId] = [req, res];
|
|
52
170
|
updateMonitoringStatus();
|
|
53
171
|
|
|
@@ -83,24 +201,42 @@ body { display: flex; flex-direction: column-reverse; }
|
|
|
83
201
|
res.write(`<div>${line}</div>\n`);
|
|
84
202
|
}
|
|
85
203
|
});
|
|
86
|
-
});
|
|
204
|
+
}, `http:${endpoint}`);
|
|
87
205
|
};
|
|
88
206
|
|
|
89
207
|
app.registerAggregator = (name, aggregator) => {
|
|
90
|
-
|
|
208
|
+
persistence.register(name, aggregator);
|
|
209
|
+
app.get(`/${name}{.:format}`, (req, res) => {
|
|
91
210
|
const raw = req.query.raw ? true : false;
|
|
92
211
|
const format = req.params.format || (raw ? 'json' : null);
|
|
93
212
|
const limit = req.query.limit || 100;
|
|
94
|
-
const sort = req.query.sort || '
|
|
213
|
+
const sort = req.query.sort || 'count15m';
|
|
214
|
+
|
|
215
|
+
if (format && !['csv', 'json'].includes(format)) {
|
|
216
|
+
res.sendStatus(404);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
95
219
|
|
|
96
220
|
const data = aggregator.getData({
|
|
97
221
|
sort,
|
|
98
222
|
limit,
|
|
99
|
-
format,
|
|
223
|
+
format: format === 'csv' ? 'json' : format,
|
|
100
224
|
raw,
|
|
101
225
|
});
|
|
102
226
|
|
|
103
|
-
if (format === '
|
|
227
|
+
if (format === 'csv') {
|
|
228
|
+
const rows = data.toJS();
|
|
229
|
+
const columns = Object.keys(rows[0] || {}).filter(
|
|
230
|
+
(k) => k !== 'activity'
|
|
231
|
+
);
|
|
232
|
+
const csv = stringify(rows, { header: true, columns });
|
|
233
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
234
|
+
res.setHeader(
|
|
235
|
+
'Content-Disposition',
|
|
236
|
+
`attachment; filename="${name}.csv"`
|
|
237
|
+
);
|
|
238
|
+
res.send(csv);
|
|
239
|
+
} else if (format === 'json') {
|
|
104
240
|
res.send(data);
|
|
105
241
|
} else {
|
|
106
242
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
@@ -117,7 +253,12 @@ app.registerAggregator = (name, aggregator) => {
|
|
|
117
253
|
}
|
|
118
254
|
});
|
|
119
255
|
|
|
120
|
-
app.
|
|
256
|
+
app.delete(`/${name}`, (req, res) => {
|
|
257
|
+
aggregator.reset();
|
|
258
|
+
res.send({ success: true });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
app.get(`/${name}/:identifier{.json}`, (req, res) => {
|
|
121
262
|
const entry = aggregator.get(req.params.identifier);
|
|
122
263
|
if (!entry) {
|
|
123
264
|
res.status(404).send('Not Found');
|
package/src/app/index.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
const http = require('http');
|
|
2
2
|
|
|
3
3
|
const express = require('express');
|
|
4
|
-
const expressWs = require('express-ws');
|
|
5
4
|
|
|
6
5
|
const constants = require('../constants');
|
|
7
6
|
|
|
8
7
|
const api = require('./api');
|
|
8
|
+
const mount = require('./mount');
|
|
9
9
|
const websocket = require('./websocket');
|
|
10
|
+
const wsServer = require('./ws-server');
|
|
10
11
|
|
|
11
12
|
const app = express();
|
|
12
13
|
const httpServer = http.createServer(app);
|
|
13
|
-
expressWs(app, httpServer);
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
httpServer.on('upgrade', (request, socket, head) => {
|
|
16
|
+
wsServer.handleUpgrade(request, socket, head);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.use(api);
|
|
16
20
|
|
|
17
21
|
function start() {
|
|
18
22
|
const port = process.env.PORT || constants.port;
|
|
@@ -25,4 +29,4 @@ function stop() {
|
|
|
25
29
|
httpServer.close();
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
module.exports = { api, start, stop, websocket };
|
|
32
|
+
module.exports = { api, mount, start, stop, websocket };
|