borecli 1.0.0__py3-none-any.whl
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.
- bore/__init__.py +0 -0
- bore/auth.py +104 -0
- bore/cli.py +378 -0
- bore/config.py +132 -0
- bore/frames.py +376 -0
- bore/handlers/__init__.py +0 -0
- bore/handlers/error_pages.py +223 -0
- bore/handlers/http_handler.py +694 -0
- bore/handlers/websocket_handler.py +975 -0
- bore/parse_duration.py +32 -0
- bore/protocol.py +72 -0
- bore/tunnel/__init__.py +0 -0
- bore/tunnel/client.py +311 -0
- bore/tunnel/dispatcher.py +277 -0
- bore/tunnel/heartbeat.py +154 -0
- bore/tunnel.py +43 -0
- bore/websocket/__init__.py +0 -0
- bore/websocket/connection.py +108 -0
- bore/websocket/frames.py +162 -0
- bore/websocket/manager.py +174 -0
- bore/websocket/proxy.py +147 -0
- bore/websocket_client.py +338 -0
- borecli-1.0.0.dist-info/METADATA +231 -0
- borecli-1.0.0.dist-info/RECORD +28 -0
- borecli-1.0.0.dist-info/WHEEL +5 -0
- borecli-1.0.0.dist-info/entry_points.txt +2 -0
- borecli-1.0.0.dist-info/licenses/LICENSE +0 -0
- borecli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
# borecli/bore/handlers/http_handler.py
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import click
|
|
8
|
+
from bore.frames import (
|
|
9
|
+
make_http_response,
|
|
10
|
+
make_http_chunk,
|
|
11
|
+
send_frame,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from bore.handlers.error_pages import (
|
|
15
|
+
tunnel_unreachable_page,
|
|
16
|
+
tunnel_timeout_page,
|
|
17
|
+
tunnel_internal_error_page,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# ------------------------------------------------------------
|
|
24
|
+
# Limits
|
|
25
|
+
# ------------------------------------------------------------
|
|
26
|
+
#
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
MAX_RESPONSE_BYTES = 50 * 1024 * 1024
|
|
30
|
+
|
|
31
|
+
CHUNK_SIZE = 512 * 1024
|
|
32
|
+
|
|
33
|
+
TIMEOUT = httpx.Timeout(
|
|
34
|
+
connect=5,
|
|
35
|
+
read=120,
|
|
36
|
+
write=30,
|
|
37
|
+
pool=30,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
# ------------------------------------------------------------
|
|
42
|
+
# Shared HTTP Client
|
|
43
|
+
# ------------------------------------------------------------
|
|
44
|
+
#
|
|
45
|
+
|
|
46
|
+
CLIENT = httpx.AsyncClient(
|
|
47
|
+
follow_redirects=False,
|
|
48
|
+
timeout=TIMEOUT,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
# ------------------------------------------------------------
|
|
53
|
+
# Request headers that must never be forwarded
|
|
54
|
+
# ------------------------------------------------------------
|
|
55
|
+
#
|
|
56
|
+
|
|
57
|
+
EXCLUDED_REQUEST_HEADERS = {
|
|
58
|
+
|
|
59
|
+
"host",
|
|
60
|
+
|
|
61
|
+
"connection",
|
|
62
|
+
|
|
63
|
+
"keep-alive",
|
|
64
|
+
|
|
65
|
+
"proxy-authenticate",
|
|
66
|
+
|
|
67
|
+
"proxy-authorization",
|
|
68
|
+
|
|
69
|
+
"te",
|
|
70
|
+
|
|
71
|
+
"trailers",
|
|
72
|
+
|
|
73
|
+
"transfer-encoding",
|
|
74
|
+
|
|
75
|
+
"upgrade",
|
|
76
|
+
|
|
77
|
+
"content-length",
|
|
78
|
+
|
|
79
|
+
"accept-encoding",
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
TEXT_CONTENT_TYPES = (
|
|
84
|
+
|
|
85
|
+
"text/",
|
|
86
|
+
|
|
87
|
+
"application/json",
|
|
88
|
+
|
|
89
|
+
"application/javascript",
|
|
90
|
+
|
|
91
|
+
"application/x-javascript",
|
|
92
|
+
|
|
93
|
+
"application/xml",
|
|
94
|
+
|
|
95
|
+
"application/xhtml+xml",
|
|
96
|
+
|
|
97
|
+
"application/graphql",
|
|
98
|
+
|
|
99
|
+
"application/problem+json",
|
|
100
|
+
|
|
101
|
+
"application/ld+json",
|
|
102
|
+
|
|
103
|
+
"image/svg+xml",
|
|
104
|
+
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def is_binary_content(
|
|
109
|
+
content_type: str | None,
|
|
110
|
+
) -> bool:
|
|
111
|
+
|
|
112
|
+
if not content_type:
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
content_type = content_type.lower()
|
|
117
|
+
|
|
118
|
+
return not any(
|
|
119
|
+
|
|
120
|
+
content_type.startswith(prefix)
|
|
121
|
+
|
|
122
|
+
for prefix in TEXT_CONTENT_TYPES
|
|
123
|
+
|
|
124
|
+
)
|
|
125
|
+
#
|
|
126
|
+
# ------------------------------------------------------------
|
|
127
|
+
# Forward one HTTP request to localhost
|
|
128
|
+
# ------------------------------------------------------------
|
|
129
|
+
#
|
|
130
|
+
|
|
131
|
+
async def forward_request(
|
|
132
|
+
*,
|
|
133
|
+
local_port: int,
|
|
134
|
+
frame: dict,
|
|
135
|
+
) -> dict:
|
|
136
|
+
"""
|
|
137
|
+
Forward a BoreHook HTTP request to the
|
|
138
|
+
user's local application.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
method = frame.get("method", "GET")
|
|
142
|
+
|
|
143
|
+
path = frame.get("path", "/")
|
|
144
|
+
|
|
145
|
+
query = frame.get("query_string", "")
|
|
146
|
+
|
|
147
|
+
request_id = frame["request_id"]
|
|
148
|
+
|
|
149
|
+
binary = frame.get("binary", False)
|
|
150
|
+
|
|
151
|
+
body = frame.get("body", "")
|
|
152
|
+
|
|
153
|
+
#
|
|
154
|
+
# --------------------------------------------------------
|
|
155
|
+
# Decode request body
|
|
156
|
+
# --------------------------------------------------------
|
|
157
|
+
#
|
|
158
|
+
|
|
159
|
+
if binary:
|
|
160
|
+
|
|
161
|
+
body = base64.b64decode(body)
|
|
162
|
+
|
|
163
|
+
else:
|
|
164
|
+
|
|
165
|
+
body = body.encode("utf-8")
|
|
166
|
+
|
|
167
|
+
#
|
|
168
|
+
# --------------------------------------------------------
|
|
169
|
+
# Copy headers
|
|
170
|
+
# --------------------------------------------------------
|
|
171
|
+
#
|
|
172
|
+
|
|
173
|
+
incoming_headers = frame.get("headers", {})
|
|
174
|
+
|
|
175
|
+
headers = {
|
|
176
|
+
|
|
177
|
+
key: value
|
|
178
|
+
|
|
179
|
+
for key, value in incoming_headers.items()
|
|
180
|
+
|
|
181
|
+
if key.lower() not in EXCLUDED_REQUEST_HEADERS
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#
|
|
186
|
+
# --------------------------------------------------------
|
|
187
|
+
# Build localhost URL
|
|
188
|
+
# --------------------------------------------------------
|
|
189
|
+
#
|
|
190
|
+
|
|
191
|
+
url = f"http://127.0.0.1:{local_port}{path}"
|
|
192
|
+
|
|
193
|
+
if query:
|
|
194
|
+
|
|
195
|
+
url += f"?{query}"
|
|
196
|
+
|
|
197
|
+
logger.info(
|
|
198
|
+
"%s %s",
|
|
199
|
+
method,
|
|
200
|
+
url,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
|
|
205
|
+
response = await CLIENT.request(
|
|
206
|
+
|
|
207
|
+
method=method,
|
|
208
|
+
|
|
209
|
+
url=url,
|
|
210
|
+
|
|
211
|
+
headers=headers,
|
|
212
|
+
|
|
213
|
+
content=body,
|
|
214
|
+
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
#
|
|
218
|
+
# ----------------------------------------------------
|
|
219
|
+
# Remove hop-by-hop headers
|
|
220
|
+
# ----------------------------------------------------
|
|
221
|
+
#
|
|
222
|
+
|
|
223
|
+
response_headers = dict(response.headers)
|
|
224
|
+
|
|
225
|
+
response_headers.pop(
|
|
226
|
+
"Transfer-Encoding",
|
|
227
|
+
None,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
response_headers.pop(
|
|
231
|
+
"Content-Encoding",
|
|
232
|
+
None,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
response_headers.pop(
|
|
236
|
+
"Content-Length",
|
|
237
|
+
None,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
raw = response.content
|
|
241
|
+
|
|
242
|
+
if len(raw) > MAX_RESPONSE_BYTES:
|
|
243
|
+
|
|
244
|
+
logger.warning(
|
|
245
|
+
"Response exceeded %d bytes.",
|
|
246
|
+
MAX_RESPONSE_BYTES,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
raw = raw[:MAX_RESPONSE_BYTES]
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
|
|
253
|
+
"request_id": request_id,
|
|
254
|
+
|
|
255
|
+
"status": response.status_code,
|
|
256
|
+
|
|
257
|
+
"headers": response_headers,
|
|
258
|
+
|
|
259
|
+
"raw": raw,
|
|
260
|
+
|
|
261
|
+
"binary": is_binary_content(
|
|
262
|
+
|
|
263
|
+
response_headers.get(
|
|
264
|
+
|
|
265
|
+
"Content-Type",
|
|
266
|
+
|
|
267
|
+
"",
|
|
268
|
+
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
),
|
|
272
|
+
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
except httpx.ConnectError:
|
|
277
|
+
|
|
278
|
+
logger.debug("Unable to connect to localhost.", exc_info=True)
|
|
279
|
+
|
|
280
|
+
click.secho(
|
|
281
|
+
f"⚠️ Request received, but nothing is listening on "
|
|
282
|
+
f"127.0.0.1:{local_port} — is your local app running?",
|
|
283
|
+
fg="yellow",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
"request_id": request_id,
|
|
288
|
+
"status": 200,
|
|
289
|
+
"headers": {"Content-Type": "text/html; charset=utf-8"},
|
|
290
|
+
"raw": tunnel_unreachable_page(local_port=local_port).encode("utf-8"),
|
|
291
|
+
"binary": False,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
except httpx.ReadTimeout:
|
|
295
|
+
|
|
296
|
+
logger.debug("Local application timed out.", exc_info=True)
|
|
297
|
+
|
|
298
|
+
click.secho(
|
|
299
|
+
f"⚠️ Request to 127.0.0.1:{local_port} timed out.",
|
|
300
|
+
fg="yellow",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"request_id": request_id,
|
|
305
|
+
"status": 200,
|
|
306
|
+
"headers": {"Content-Type": "text/html; charset=utf-8"},
|
|
307
|
+
"raw": tunnel_timeout_page().encode("utf-8"),
|
|
308
|
+
"binary": False,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
except Exception:
|
|
312
|
+
|
|
313
|
+
logger.debug("Local application timed out.", exc_info=True)
|
|
314
|
+
|
|
315
|
+
click.secho(
|
|
316
|
+
f"⚠️ Request to 127.0.0.1:{local_port} timed out.",
|
|
317
|
+
fg="yellow",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
"request_id": request_id,
|
|
322
|
+
"status": 200,
|
|
323
|
+
"headers": {"Content-Type": "text/html; charset=utf-8"},
|
|
324
|
+
"raw": tunnel_internal_error_page().encode("utf-8"),
|
|
325
|
+
"binary": False,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#
|
|
329
|
+
# ------------------------------------------------------------
|
|
330
|
+
# Encode response body
|
|
331
|
+
# ------------------------------------------------------------
|
|
332
|
+
#
|
|
333
|
+
|
|
334
|
+
def encode_response_body(
|
|
335
|
+
raw: bytes,
|
|
336
|
+
binary: bool,
|
|
337
|
+
) -> tuple[str, bytes]:
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
Convert raw response bytes into a string suitable
|
|
341
|
+
for transmission over the BoreHook protocol.
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
|
|
345
|
+
encoded_body
|
|
346
|
+
encoded_bytes
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
if binary:
|
|
350
|
+
|
|
351
|
+
encoded = base64.b64encode(
|
|
352
|
+
raw,
|
|
353
|
+
).decode("ascii")
|
|
354
|
+
|
|
355
|
+
encoded_bytes = encoded.encode(
|
|
356
|
+
"ascii",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
|
|
361
|
+
encoded = raw.decode(
|
|
362
|
+
"utf-8",
|
|
363
|
+
errors="replace",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
encoded_bytes = encoded.encode(
|
|
367
|
+
"utf-8",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return encoded, encoded_bytes
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
#
|
|
374
|
+
# ------------------------------------------------------------
|
|
375
|
+
# Split encoded payload into protocol chunks
|
|
376
|
+
# ------------------------------------------------------------
|
|
377
|
+
#
|
|
378
|
+
|
|
379
|
+
def split_into_chunks(
|
|
380
|
+
encoded_bytes: bytes,
|
|
381
|
+
):
|
|
382
|
+
|
|
383
|
+
"""
|
|
384
|
+
Yield encoded payload chunks.
|
|
385
|
+
|
|
386
|
+
This generator is used for large HTTP responses.
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
for offset in range(
|
|
390
|
+
|
|
391
|
+
0,
|
|
392
|
+
|
|
393
|
+
len(encoded_bytes),
|
|
394
|
+
|
|
395
|
+
CHUNK_SIZE,
|
|
396
|
+
|
|
397
|
+
):
|
|
398
|
+
|
|
399
|
+
yield encoded_bytes[
|
|
400
|
+
offset:
|
|
401
|
+
offset + CHUNK_SIZE
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
#
|
|
406
|
+
# ------------------------------------------------------------
|
|
407
|
+
# Build protocol frames
|
|
408
|
+
# ------------------------------------------------------------
|
|
409
|
+
#
|
|
410
|
+
|
|
411
|
+
def build_response_frames(
|
|
412
|
+
result: dict,
|
|
413
|
+
):
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
Convert a forwarded HTTP response into one or more
|
|
417
|
+
BoreHook protocol frames.
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
body, body_bytes = encode_response_body(
|
|
421
|
+
|
|
422
|
+
result["raw"],
|
|
423
|
+
|
|
424
|
+
result["binary"],
|
|
425
|
+
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
#
|
|
429
|
+
# Small response
|
|
430
|
+
#
|
|
431
|
+
|
|
432
|
+
if len(body_bytes) <= CHUNK_SIZE:
|
|
433
|
+
|
|
434
|
+
yield make_http_response(
|
|
435
|
+
|
|
436
|
+
request_id=result["request_id"],
|
|
437
|
+
|
|
438
|
+
status=result["status"],
|
|
439
|
+
|
|
440
|
+
headers=result["headers"],
|
|
441
|
+
|
|
442
|
+
body=body,
|
|
443
|
+
|
|
444
|
+
binary=result["binary"],
|
|
445
|
+
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
#
|
|
451
|
+
# Chunked response
|
|
452
|
+
#
|
|
453
|
+
|
|
454
|
+
chunks = list(
|
|
455
|
+
|
|
456
|
+
split_into_chunks(
|
|
457
|
+
body_bytes,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
total = len(chunks)
|
|
463
|
+
|
|
464
|
+
logger.info(
|
|
465
|
+
|
|
466
|
+
"Sending %s in %d chunks.",
|
|
467
|
+
|
|
468
|
+
result["request_id"],
|
|
469
|
+
|
|
470
|
+
total,
|
|
471
|
+
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
for index, chunk in enumerate(chunks):
|
|
475
|
+
|
|
476
|
+
frame = make_http_chunk(
|
|
477
|
+
|
|
478
|
+
request_id=result["request_id"],
|
|
479
|
+
|
|
480
|
+
chunk_index=index,
|
|
481
|
+
|
|
482
|
+
total_chunks=total,
|
|
483
|
+
|
|
484
|
+
body=chunk.decode(
|
|
485
|
+
|
|
486
|
+
"ascii"
|
|
487
|
+
|
|
488
|
+
if result["binary"]
|
|
489
|
+
|
|
490
|
+
else "utf-8"
|
|
491
|
+
|
|
492
|
+
),
|
|
493
|
+
|
|
494
|
+
binary=result["binary"],
|
|
495
|
+
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
#
|
|
499
|
+
# Metadata only on first chunk
|
|
500
|
+
#
|
|
501
|
+
|
|
502
|
+
if index == 0:
|
|
503
|
+
|
|
504
|
+
frame["status"] = result["status"]
|
|
505
|
+
|
|
506
|
+
frame["headers"] = result["headers"]
|
|
507
|
+
|
|
508
|
+
yield frame
|
|
509
|
+
|
|
510
|
+
#
|
|
511
|
+
# ------------------------------------------------------------
|
|
512
|
+
# Send one HTTP response
|
|
513
|
+
# ------------------------------------------------------------
|
|
514
|
+
#
|
|
515
|
+
|
|
516
|
+
async def send_response(
|
|
517
|
+
websocket,
|
|
518
|
+
result: dict,
|
|
519
|
+
) -> None:
|
|
520
|
+
"""
|
|
521
|
+
Send a forwarded HTTP response back to
|
|
522
|
+
the BoreHook server.
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
request_id = result["request_id"]
|
|
526
|
+
|
|
527
|
+
try:
|
|
528
|
+
|
|
529
|
+
for frame in build_response_frames(result):
|
|
530
|
+
|
|
531
|
+
await send_frame(
|
|
532
|
+
websocket,
|
|
533
|
+
frame,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
logger.info(
|
|
537
|
+
"Response sent (%s)",
|
|
538
|
+
request_id,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
except Exception:
|
|
542
|
+
|
|
543
|
+
logger.exception(
|
|
544
|
+
"Failed sending response %s",
|
|
545
|
+
request_id,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
raise
|
|
549
|
+
|
|
550
|
+
#
|
|
551
|
+
# ------------------------------------------------------------
|
|
552
|
+
# Public API
|
|
553
|
+
# ------------------------------------------------------------
|
|
554
|
+
#
|
|
555
|
+
|
|
556
|
+
async def process_request(
|
|
557
|
+
*,
|
|
558
|
+
websocket,
|
|
559
|
+
local_port: int,
|
|
560
|
+
frame: dict,
|
|
561
|
+
) -> None:
|
|
562
|
+
"""
|
|
563
|
+
Process one HTTP request received from the
|
|
564
|
+
BoreHook server.
|
|
565
|
+
|
|
566
|
+
Workflow
|
|
567
|
+
|
|
568
|
+
Server
|
|
569
|
+
│
|
|
570
|
+
▼
|
|
571
|
+
forward_request()
|
|
572
|
+
│
|
|
573
|
+
▼
|
|
574
|
+
build_response_frames()
|
|
575
|
+
│
|
|
576
|
+
▼
|
|
577
|
+
send_response()
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
request_id = frame.get("request_id")
|
|
581
|
+
|
|
582
|
+
logger.info(
|
|
583
|
+
"Processing HTTP request %s",
|
|
584
|
+
request_id,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
|
|
589
|
+
#
|
|
590
|
+
# Forward request to localhost
|
|
591
|
+
#
|
|
592
|
+
|
|
593
|
+
result = await forward_request(
|
|
594
|
+
|
|
595
|
+
local_port=local_port,
|
|
596
|
+
|
|
597
|
+
frame=frame,
|
|
598
|
+
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
#
|
|
602
|
+
# Send response back to BoreHook
|
|
603
|
+
#
|
|
604
|
+
|
|
605
|
+
await send_response(
|
|
606
|
+
|
|
607
|
+
websocket,
|
|
608
|
+
|
|
609
|
+
result,
|
|
610
|
+
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
logger.info(
|
|
614
|
+
"Completed HTTP request %s",
|
|
615
|
+
request_id,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
except Exception:
|
|
619
|
+
|
|
620
|
+
logger.exception(
|
|
621
|
+
|
|
622
|
+
"HTTP request failed (%s)",
|
|
623
|
+
|
|
624
|
+
request_id,
|
|
625
|
+
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
#
|
|
629
|
+
# Last-resort error response
|
|
630
|
+
#
|
|
631
|
+
|
|
632
|
+
fallback = {
|
|
633
|
+
|
|
634
|
+
"request_id": request_id,
|
|
635
|
+
|
|
636
|
+
"status": 500,
|
|
637
|
+
|
|
638
|
+
"headers": {
|
|
639
|
+
|
|
640
|
+
"Content-Type":
|
|
641
|
+
"text/plain; charset=utf-8",
|
|
642
|
+
|
|
643
|
+
},
|
|
644
|
+
|
|
645
|
+
"raw": b"Internal BoreHook tunnel error.",
|
|
646
|
+
|
|
647
|
+
"binary": False,
|
|
648
|
+
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
|
|
653
|
+
await send_response(
|
|
654
|
+
|
|
655
|
+
websocket,
|
|
656
|
+
|
|
657
|
+
fallback,
|
|
658
|
+
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
except Exception:
|
|
662
|
+
|
|
663
|
+
logger.exception(
|
|
664
|
+
|
|
665
|
+
"Unable to send fallback response.",
|
|
666
|
+
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
#
|
|
670
|
+
# ------------------------------------------------------------
|
|
671
|
+
# Cleanup
|
|
672
|
+
# ------------------------------------------------------------
|
|
673
|
+
#
|
|
674
|
+
|
|
675
|
+
async def shutdown() -> None:
|
|
676
|
+
"""
|
|
677
|
+
Shutdown the shared HTTP client.
|
|
678
|
+
|
|
679
|
+
Called when the BoreHook tunnel client exits.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
try:
|
|
683
|
+
|
|
684
|
+
await CLIENT.aclose()
|
|
685
|
+
|
|
686
|
+
logger.info(
|
|
687
|
+
"HTTP client closed."
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
except Exception:
|
|
691
|
+
|
|
692
|
+
logger.exception(
|
|
693
|
+
"Failed closing HTTP client."
|
|
694
|
+
)
|