@@ -4,11 +4,17 @@ import { toHex, fromHex } from "../../shared/hex";
4
4
5
5
const RESPONSE_TIMEOUT_MS = 30_000 ; // 30 seconds
6
6
7
+ type Stats = {
8
+ isConnected : boolean ;
9
+ requests : number ;
10
+ } ;
11
+
7
12
export class MyDurableObject extends DurableObject {
8
13
// TODO: think of using a WeakMap
9
14
proxyTo : WebSocket | null = null ;
10
15
resolve : ( ( value : Response ) => void ) | null = null ;
11
16
reject : ( ( value : Error ) => void ) | null = null ;
17
+ requests : number = 0 ;
12
18
constructor ( ctx : DurableObjectState , env : Env ) {
13
19
super ( ctx , env ) ;
14
20
}
@@ -48,6 +54,7 @@ export class MyDurableObject extends DurableObject {
48
54
} ) ;
49
55
50
56
server . addEventListener ( "close" , ( cls : CloseEvent ) => {
57
+ this . proxyTo = null ;
51
58
this . reject ?.( new Error ( "server closed" ) ) ;
52
59
console . info ( "closing connection" , cls . code , cls . reason ) ;
53
60
server . close ( 1001 , `server closed (${ cls . code } : ${ cls . reason } )` ) ;
@@ -60,6 +67,7 @@ export class MyDurableObject extends DurableObject {
60
67
}
61
68
62
69
async proxy ( request : Request ) : Promise < Response > {
70
+ this . requests ++ ;
63
71
console . info ( "proxying request" , request . url ) ;
64
72
if ( ! this . proxyTo ) {
65
73
return new Response ( "no proxy connection" , { status : 502 } ) ;
@@ -121,14 +129,39 @@ export class MyDurableObject extends DurableObject {
121
129
this . proxyTo = null ;
122
130
return true ;
123
131
}
132
+
133
+ async stats ( ) : Promise < Stats > {
134
+ return {
135
+ isConnected : ! ! this . proxyTo ,
136
+ requests : this . requests ,
137
+ } ;
138
+ }
124
139
}
125
140
126
- const DO_NAME = "foo" ;
141
+ function getTunnelId ( path : string ) : string {
142
+ const [ , , uuid ] = path . split ( "/" ) ;
143
+ if ( ! uuid ) {
144
+ throw new Error ( "Missing tunnel URL" ) ;
145
+ }
146
+ if ( uuid . length !== 36 ) {
147
+ throw new Error ( "Invalid tunnel URL" ) ;
148
+ }
149
+ return uuid ;
150
+ }
127
151
128
152
export default {
129
153
async fetch ( request , env , ctx ) : Promise < Response > {
130
154
const url = new URL ( request . url ) ;
131
- if ( url . pathname == "/tunnel" ) {
155
+ if ( url . pathname . startsWith ( "/tunnel/" ) ) {
156
+ const tunnelId = getTunnelId ( url . pathname ) ;
157
+ const doId = env . MY_DURABLE_OBJECT . idFromName ( tunnelId ) ;
158
+ const stub = env . MY_DURABLE_OBJECT . get ( doId ) ;
159
+ const stats = await stub . stats ( ) ;
160
+
161
+ return new Response ( tunnelPage ( url . origin , tunnelId , stats ) , {
162
+ headers : { "content-type" : "text/html" } ,
163
+ } ) ;
164
+ } else if ( url . pathname . startsWith ( "/connect/" ) ) {
132
165
// Expect to receive a WebSocket Upgrade request.
133
166
// If there is one, accept the request and return a WebSocket Response.
134
167
const upgradeHeader = request . headers . get ( "Upgrade" ) ;
@@ -138,32 +171,30 @@ export default {
138
171
} ) ;
139
172
}
140
173
141
- const id = env . MY_DURABLE_OBJECT . idFromName ( DO_NAME ) ;
142
-
143
- // Create a stub to open a communication channel with the Durable
144
- // Object instance.
145
- const stub = env . MY_DURABLE_OBJECT . get ( id ) ;
146
-
174
+ const tunnelId = getTunnelId ( url . pathname ) ;
175
+ const doId = env . MY_DURABLE_OBJECT . idFromName ( tunnelId ) ;
176
+ const stub = env . MY_DURABLE_OBJECT . get ( doId ) ;
147
177
return stub . fetch ( request ) ;
148
178
} else if ( url . pathname . startsWith ( "/proxy/" ) ) {
149
- const id = env . MY_DURABLE_OBJECT . idFromName ( DO_NAME ) ;
150
-
151
- // Create a stub to open a communication channel with the Durable
152
- // Object instance.
153
- const stub = env . MY_DURABLE_OBJECT . get ( id ) ;
179
+ const tunnelId = getTunnelId ( url . pathname ) ;
180
+ const doId = env . MY_DURABLE_OBJECT . idFromName ( tunnelId ) ;
181
+ const stub = env . MY_DURABLE_OBJECT . get ( doId ) ;
154
182
return stub . proxy ( request ) ;
155
- } else if ( url . pathname == "/close" ) {
156
- const id = env . MY_DURABLE_OBJECT . idFromName ( DO_NAME ) ;
157
- const stub = env . MY_DURABLE_OBJECT . get ( id ) ;
183
+ } else if ( url . pathname . startsWith ( "/close/" ) ) {
184
+ const tunnelId = getTunnelId ( url . pathname ) ;
185
+ const doId = env . MY_DURABLE_OBJECT . idFromName ( tunnelId ) ;
186
+ const stub = env . MY_DURABLE_OBJECT . get ( doId ) ;
158
187
159
188
return new Response (
160
- ( await stub . close ( ) ) ? "Closed connection" : "No proxy connection" ,
189
+ ( await stub . close ( ) )
190
+ ? "Closed connection"
191
+ : "No proxy connection found, all good." ,
161
192
{
162
193
headers : { "cache-control" : "no-cache, no-store, max-age=0" } ,
163
194
} ,
164
195
) ;
165
196
} else if ( url . pathname == "/" ) {
166
- return new Response ( indexPage ( url . origin ) , {
197
+ return new Response ( homePage ( ) , {
167
198
headers : { "content-type" : "text/html" } ,
168
199
} ) ;
169
200
}
@@ -185,34 +216,52 @@ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
185
216
} ) ;
186
217
}
187
218
188
- function indexPage ( origin : string ) : string {
219
+ function homePage ( ) : string {
220
+ const uuid = crypto . randomUUID ( ) ;
189
221
return `
190
222
<!doctype html>
191
223
<html lang="en">
192
224
<head>
193
225
<meta charset="UTF-8" />
194
226
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
195
- <title>Hello, World!</title>
196
- <link
197
- rel="stylesheet"
198
- href="/pico.min.css"
199
- >
227
+ <title>Webhooks Proxy Tunnel</title>
228
+ <link rel="stylesheet" href="/pico.min.css">
200
229
</head>
201
230
<body>
202
231
<main class="container">
203
232
<h1>Webhooks Proxy Tunnel</h1>
204
- <p>Use <a href="https://github.com/peter-leonov/webhooks-proxy-tunnel">Webhooks Proxy Tunnel</a> to proxy HTTP requests made to the public URL to your project local web server.</p>
205
- <p>Public URL: <code>${ origin } /proxy/</code></p>
206
- <p>Tunnel URL: <code>${ origin } /tunnel</code></p>
233
+ <p>Use Webhooks Proxy Tunnel (<a href="https://github.com/peter-leonov/webhooks-proxy-tunnel">GitHub</a>) to proxy HTTP requests made to the public URL to your project local web server.</p>
234
+ <p>Here is your very personal tunnel: <a href="/tunnel/${ uuid } ">${ uuid } </a> (refresh the page for a new one).</p>
235
+ </body>
236
+ </html>` ;
237
+ }
238
+
239
+ function tunnelPage ( origin : string , tunnelId : string , stats : Stats ) : string {
240
+ return `
241
+ <!doctype html>
242
+ <html lang="en">
243
+ <head>
244
+ <meta charset="UTF-8" />
245
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
246
+ <title>Webhooks Proxy Tunnel / ${ tunnelId } </title>
247
+ <link rel="stylesheet" href="/pico.min.css">
248
+ </head>
249
+ <body>
250
+ <main class="container">
251
+ <h1>Tunnel ${ tunnelId } </h1>
252
+ <p>Connected: ${ stats . isConnected ? "yes" : "no" } </p>
253
+ <p>Requests: ${ stats . requests } </p>
254
+ <p>Public URL: <code>${ origin } /proxy/${ tunnelId } </code></p>
255
+ <p>Connect URL: <code>${ origin } /connect/${ tunnelId } </code></p>
207
256
<p>
208
257
Local server URL: <input type="text" value="http://localhost:3000" id="target-input" />
209
258
Client command:
210
259
<pre><code>cd webhooks-proxy-tunnel/client
211
- npm start -- ${ origin } /tunnel <span id="target-span">http://localhost:3000</span>
260
+ npm start -- ${ origin } /connect/ ${ tunnelId } <span id="target-span">http://localhost:3000</span>
212
261
</code></pre>
213
262
Connecting a new client kicks out the currently connected one.
214
263
</p>
215
- <p>Force <a href="/close">close</a> the tunnel if the connected client is stuck.</p>
264
+ <p>Force <a href="/close/ ${ tunnelId } ">close</a> the tunnel if the connected client got stuck.</p>
216
265
</main>
217
266
<script>
218
267
const targetInput = document.getElementById("target-input");
0 commit comments