Skip to content

Commit 683e798

Browse files
committed
Bidirectional forwarding
Send target response to Webhook.site Increase minimum node version to 16 (due to AbortSignal.timeout) Adds version number output to "help" command Version 0.2.0
1 parent 9042113 commit 683e798

File tree

8 files changed

+189
-22
lines changed

8 files changed

+189
-22
lines changed

.nvmrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v14
1+
v16

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ If you have installed [Docker](https://docs.docker.com/get-docker/), you can sim
1616

1717
### Node.js
1818

19-
[Node](https://nodejs.org/en/download) version 14 or greater required.
19+
[Node](https://nodejs.org/en/download) version 16 or greater required.
2020

2121
To install: `npm install -g @webhooksite/cli`
2222

commands/forward.js

+57-10
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,66 @@
1-
import fetch from "node-fetch";
1+
import fetch, {Response} from "node-fetch";
22
import listen from "./lib/listen.js";
33
import replaceVariables from "./lib/replace-variables.js";
44
import log from "./lib/log.js";
5+
import {setApiKey, setResponse, updateTokenListen} from "./lib/api.js";
6+
7+
const getTargetPath = function (url) {
8+
// We only want the `/a/b/c` part:
9+
// https://my-url.webhook.site/a/b/c
10+
const pathMatchDomain = url.match(/https?:\/\/[a-zA-Z0-9-]{3,36}\.webhook\.site(\/[^?#]+)/)
11+
if (pathMatchDomain) {
12+
return pathMatchDomain[1];
13+
}
14+
15+
// We only want the `/a/b/c` part:
16+
// https://webhook.site/00000000-0000-0000-00000-000000000000/a/b/c
17+
const pathMatch = url.match(/https?:\/\/[^\/]*\/[a-z0-9-]+(\/[^?#]+)/)
18+
return pathMatch ? pathMatch[1] : '';
19+
}
520

621
export default async (argv) => {
722
if (!argv.token && !process.env.WH_TOKEN) {
823
throw new Error('Please specify a token (--token)')
924
}
25+
const tokenId = argv.token ?? process.env.WH_TOKEN;
26+
const apiKey = argv['api-key'] ?? process.env.WH_API_KEY;
27+
const listenSeconds = argv['listen-timeout'] ?? process.env.WH_LISTEN_TIMEOUT ?? 5;
28+
29+
setApiKey(apiKey);
30+
31+
// Listen for the amount of seconds
32+
await updateTokenListen(tokenId, listenSeconds);
33+
34+
const clearTokenListen = async function () {
35+
await updateTokenListen(tokenId, 0);
36+
process.exit()
37+
}
38+
39+
// Remove token listening on exit
40+
process.on('exit', clearTokenListen)
41+
process.on('SIGINT', clearTokenListen)
1042

1143
listen(
12-
argv.token ?? process.env.WH_TOKEN,
13-
argv['api-key'] ?? process.env.WH_API_KEY,
44+
tokenId,
45+
apiKey,
1446
(data) => {
1547
let target = replaceVariables(argv.target ?? process.env.WH_TARGET ?? 'https://localhost', data.variables)
1648

1749
if (!argv['keep-url']) {
50+
let path = '';
1851
const query = data.request.query !== null
1952
? '?' + new URLSearchParams(data.request.query).toString()
2053
: '';
2154

22-
// We only want the `/a/b/c` part:
23-
// https://webhook.site/00000000-0000-0000-00000-000000000000/a/b/c
24-
const pathMatch = data.request.url.match(/https?:\/\/[^\/]*\/[a-z0-9-]+(\/[^?#]+)/)
25-
const path = pathMatch ? pathMatch[1] : '';
26-
target = target + path + query;
55+
target = target + getTargetPath(data.request.url) + query;
2756
}
2857

2958
let options = {
3059
method: data.request.method,
3160
headers: data.request.headers,
3261
body: null,
62+
// Enough time to clear token listen property when command exits.
63+
signal: AbortSignal.timeout(listenSeconds*1000),
3364
};
3465

3566
const removeHeaders = [
@@ -48,12 +79,28 @@ export default async (argv) => {
4879
}
4980

5081
fetch(target, options)
51-
.then((res) => {
82+
.then(async (res) => {
5283
log.info({
5384
msg: 'Forwarded incoming request',
5485
url: res.url,
55-
status: res.status
86+
status: res.status,
5687
});
88+
if (listenSeconds > 0) {
89+
await setResponse(
90+
tokenId,
91+
data.request.uuid,
92+
res.status,
93+
res.arrayBuffer(),
94+
res.headers.raw(),
95+
listenSeconds*1000
96+
)
97+
}
98+
})
99+
.catch((err) => {
100+
log.error({
101+
msg: 'Error forwarding incoming request',
102+
err,
103+
})
57104
})
58105
}
59106
)

commands/help.js

+14-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import "colors";
2+
import * as fs from "node:fs";
3+
const packageJson = JSON.parse(fs.readFileSync('package.json'))
24

35
export default () => {
4-
console.log(`${'⚓ whcli: Webhook.site CLI'.bold}
6+
console.log(`⚓ whcli: Webhook.site CLI ${packageJson.version}
57
Usage: whcli [command] [--arg...]
68
Documentation: https://docs.webhook.site/cli.html
79
@@ -17,6 +19,10 @@ ${'Commands and Arguments'.bold}
1719
are replaced; see below.
1820
--keep-url When used, skips merging path and query strings
1921
into the target URL.
22+
--listen-timeout=5 Amount of seconds to wait for a response before
23+
forwarding it back to Webhook.site.
24+
Default 5. Max 10.
25+
Set to 0 to disable bidirectional forwarding.
2026
2127
${'exec'.underline} Execute shell commands on traffic to a Webhook.site URL.
2228
--token= Specifies which Webhook.site URL (token ID)
@@ -32,12 +38,13 @@ ${'Variable Replacement'.bold}
3238
3339
${'Environment Variables'.bold}
3440
Some command arguments can be specified via environment variables:
35-
${'WH_TOKEN'.underline} Specifies --token
36-
${'WH_API_KEY'.underline} Specifies --api-key
37-
${'WH_TARGET'.underline} Specifies --target
38-
${'WH_COMMAND'.underline} Specifies --command
39-
${'WH_LOG_LEVEL'.underline} Sets log level (silent, trace, debug, info,
40-
warn, error, fatal) Defaults to info.
41+
${'WH_TOKEN'.underline} Specifies --token
42+
${'WH_API_KEY'.underline} Specifies --api-key
43+
${'WH_TARGET'.underline} Specifies --target
44+
${'WH_COMMAND'.underline} Specifies --command
45+
${'WH_LISTEN_TIMEOUT'.underline} Specifies --listen-timeout
46+
${'WH_LOG_LEVEL'.underline} Sets log level (silent, trace, debug, info,
47+
warn, error, fatal) Defaults to info.
4148
${'NODE_TLS_REJECT_UNAUTHORIZED'.underline} Set to "0" to disable e.g. self-signed
4249
or expired certificate errors.
4350
`)

commands/lib/api.js

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import fetch from "node-fetch";
2+
import * as self from "./api.js";
3+
import log from "./log.js";
4+
5+
let apiKey = process.env.WH_API_KEY ?? null;
6+
let apiUrl = process.env.WH_API ?? 'https://webhook.site';
7+
8+
const getHeaders = function () {
9+
let headers = {
10+
'Accept': 'application/json',
11+
'Content-Type': 'application/json',
12+
}
13+
14+
if (apiKey) {
15+
headers['Api-Key'] = apiKey;
16+
}
17+
18+
return headers;
19+
}
20+
21+
export function setApiKey(newApiKey) {
22+
apiKey = newApiKey;
23+
}
24+
25+
export async function getToken(id) {
26+
return fetch(`${apiUrl}/token/${id}`, {
27+
method: 'GET',
28+
headers: getHeaders(),
29+
})
30+
.then(async res => {
31+
if (res.status === 200) {
32+
return res.json();
33+
}
34+
throw Error('Could not get token: ' + await res.text());
35+
});
36+
}
37+
38+
export async function updateToken(id, tokenData) {
39+
return fetch(`${apiUrl}/token/${id}`, {
40+
method: 'PUT',
41+
body: JSON.stringify(tokenData),
42+
headers: getHeaders(),
43+
})
44+
.then(async res => {
45+
if (res.status === 200) {
46+
return res.json();
47+
}
48+
throw Error('Could not update token: ' + await res.text());
49+
});
50+
}
51+
52+
export async function setResponse(tokenId, requestId, status, content, headers, timeout) {
53+
await fetch(
54+
`${apiUrl}/token/${tokenId}/request/${requestId}/response`,
55+
{
56+
method: 'PUT',
57+
body: JSON.stringify({
58+
status,
59+
content: Buffer.from(await content).toString('base64'),
60+
headers: headers,
61+
}),
62+
headers: getHeaders(),
63+
signal: AbortSignal.timeout(timeout)
64+
}
65+
)
66+
.then(async (res) => {
67+
if (res.status === 200) {
68+
log.info({
69+
msg: 'Forwarded response to Webhook.site',
70+
status: res.status,
71+
})
72+
return;
73+
}
74+
75+
if (res.status === 413) {
76+
log.error({
77+
msg: 'Error forwarding response to Webhook.site: 10 MB response size exceeded',
78+
})
79+
return;
80+
}
81+
82+
log.info({
83+
msg: 'Error forwarding response to Webhook.site',
84+
status: res.status,
85+
error: (await res.text()),
86+
})
87+
})
88+
.catch((err) => {
89+
log.error({
90+
msg: 'Error forwarding response to Webhook.site',
91+
err,
92+
})
93+
});
94+
}
95+
96+
export async function updateTokenListen(id, listenSeconds) {
97+
const tokenData = await self.getToken(id)
98+
tokenData['listen'] = listenSeconds;
99+
return await self.updateToken(id, tokenData);
100+
}

commands/lib/listen.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ export default (tokenId, apiKey, onRequest) => {
2020
}
2121
})
2222

23-
channel.socket.on('error', (error) => {
24-
logger.trace('WS: Error', { error })
23+
channel.socket.on('error', (err) => {
24+
logger.trace(err, 'WS: Error')
25+
})
26+
27+
channel.error((err) => {
28+
logger.trace(err, 'WS: Error');
2529
})
2630

2731
channel.listen('.request.created', (data) => {

commands/lib/log.js

+6
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ const log = pino({
44
level: process.env.WH_LOG_LEVEL ?? 'info',
55
})
66

7+
const whitelistedExceptions = ['write EPIPE'];
78
process.on('uncaughtException',
89
(err) => {
910
log.fatal(err, 'Unhandled Exception');
11+
12+
if (whitelistedExceptions.includes(err.message)) {
13+
return
14+
}
15+
1016
setTimeout(() => { process.abort() }, 1000).unref()
1117
process.exit(1);
1218
});

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
"name": "@webhooksite/cli",
33
"description": "Client for Webhook.site",
44
"repository": "webhooksite/cli",
5-
"version": "0.1.11",
5+
"version": "0.2.0",
66
"type": "module",
77
"publishConfig": {
88
"registry": "https://registry.npmjs.org/"
99
},
10+
"engines" : {
11+
"node" : ">=16.0.0"
12+
},
1013
"dependencies": {
1114
"colors": "^1.4.0",
1215
"laravel-echo": "^1.16.1",

0 commit comments

Comments
 (0)