8
8
from typing import Any , Literal
9
9
10
10
from ..client import ClientProtocol
11
- from ..datastructures import HeadersLike
11
+ from ..datastructures import Headers , HeadersLike
12
+ from ..exceptions import InvalidProxyMessage , InvalidProxyStatus
12
13
from ..extensions .base import ClientExtensionFactory
13
14
from ..extensions .permessage_deflate import enable_client_permessage_deflate
14
- from ..headers import validate_subprotocols
15
+ from ..headers import build_authorization_basic , build_host , validate_subprotocols
15
16
from ..http11 import USER_AGENT , Response
16
17
from ..protocol import CONNECTING , Event
18
+ from ..streams import StreamReader
17
19
from ..typing import LoggerLike , Origin , Subprotocol
18
20
from ..uri import Proxy , WebSocketURI , get_proxy , parse_proxy , parse_uri
19
21
from .connection import Connection
@@ -140,6 +142,8 @@ def connect(
140
142
additional_headers : HeadersLike | None = None ,
141
143
user_agent_header : str | None = USER_AGENT ,
142
144
proxy : str | Literal [True ] | None = True ,
145
+ proxy_ssl : ssl_module .SSLContext | None = None ,
146
+ proxy_server_hostname : str | None = None ,
143
147
# Timeouts
144
148
open_timeout : float | None = 10 ,
145
149
ping_interval : float | None = 20 ,
@@ -194,6 +198,9 @@ def connect(
194
198
to :obj:`None` to disable the proxy or to the address of a proxy
195
199
to override the system configuration. See the :doc:`proxy docs
196
200
<../../topics/proxies>` for details.
201
+ proxy_ssl: Configuration for enabling TLS on the proxy connection.
202
+ proxy_server_hostname: Host name for the TLS handshake with the proxy.
203
+ ``proxy_server_hostname`` overrides the host name from ``proxy``.
197
204
open_timeout: Timeout for opening the connection in seconds.
198
205
:obj:`None` disables the timeout.
199
206
ping_interval: Interval between keepalive pings in seconds.
@@ -287,6 +294,8 @@ def connect(
287
294
parse_proxy (proxy ),
288
295
ws_uri ,
289
296
deadline ,
297
+ proxy_ssl = proxy_ssl ,
298
+ proxy_server_hostname = proxy_server_hostname ,
290
299
** kwargs ,
291
300
)
292
301
else :
@@ -431,6 +440,84 @@ def connect_socks_proxy(
431
440
raise ImportError ("python-socks is required to use a SOCKS proxy" )
432
441
433
442
443
+ def connect_http_proxy (
444
+ proxy : Proxy ,
445
+ ws_uri : WebSocketURI ,
446
+ deadline : Deadline ,
447
+ * ,
448
+ proxy_ssl : ssl_module .SSLContext | None = None ,
449
+ proxy_server_hostname : str | None = None ,
450
+ ** kwargs : Any ,
451
+ ) -> socket .socket :
452
+ if proxy .scheme [:5 ] != "https" and proxy_ssl is not None :
453
+ raise ValueError ("proxy_ssl argument is incompatible with an http:// proxy" )
454
+
455
+ # Connect socket
456
+
457
+ kwargs .setdefault ("timeout" , deadline .timeout ())
458
+ sock = socket .create_connection ((proxy .host , proxy .port ), ** kwargs )
459
+
460
+ # Initialize TLS wrapper and perform TLS handshake
461
+
462
+ if proxy .scheme [:5 ] == "https" :
463
+ if proxy_ssl is None :
464
+ proxy_ssl = ssl_module .create_default_context ()
465
+ if proxy_server_hostname is None :
466
+ proxy_server_hostname = proxy .host
467
+ sock .settimeout (deadline .timeout ())
468
+ sock = proxy_ssl .wrap_socket (sock , server_hostname = proxy_server_hostname )
469
+ sock .settimeout (None )
470
+
471
+ # Send CONNECT request to the proxy.
472
+
473
+ proxy_headers = Headers ()
474
+ proxy_headers ["Host" ] = build_host (ws_uri .host , ws_uri .port , ws_uri .secure )
475
+ if proxy .username is not None :
476
+ assert proxy .password is not None # enforced by parse_proxy
477
+ proxy_headers ["Proxy-Authorization" ] = build_authorization_basic (
478
+ proxy .username ,
479
+ proxy .password ,
480
+ )
481
+
482
+ connect_host = build_host (
483
+ ws_uri .host ,
484
+ ws_uri .port ,
485
+ ws_uri .secure ,
486
+ always_include_port = True ,
487
+ )
488
+ # We cannot use the Request class because it supports only GET requests.
489
+ proxy_request = f"CONNECT { connect_host } HTTP/1.1\r \n " .encode ()
490
+ proxy_request += proxy_headers .serialize ()
491
+ sock .sendall (proxy_request )
492
+
493
+ # Read response from the proxy.
494
+
495
+ reader = StreamReader ()
496
+ parser = Response .parse (
497
+ reader .read_line ,
498
+ reader .read_exact ,
499
+ reader .read_to_eof ,
500
+ include_body = False ,
501
+ )
502
+ try :
503
+ while True :
504
+ sock .settimeout (deadline .timeout ())
505
+ reader .feed_data (sock .recv (4096 ))
506
+ next (parser )
507
+ except StopIteration as exc :
508
+ response = exc .value
509
+ except Exception as exc :
510
+ raise InvalidProxyMessage (
511
+ "did not receive a valid HTTP response from proxy"
512
+ ) from exc
513
+ finally :
514
+ sock .settimeout (None )
515
+ if not 200 <= response .status_code < 300 :
516
+ raise InvalidProxyStatus (response )
517
+
518
+ return sock
519
+
520
+
434
521
def connect_proxy (
435
522
proxy : Proxy ,
436
523
ws_uri : WebSocketURI ,
@@ -441,5 +528,7 @@ def connect_proxy(
441
528
# parse_proxy() validates proxy.scheme.
442
529
if proxy .scheme [:5 ] == "socks" :
443
530
return connect_socks_proxy (proxy , ws_uri , deadline , ** kwargs )
531
+ elif proxy .scheme [:4 ] == "http" :
532
+ return connect_http_proxy (proxy , ws_uri , deadline , ** kwargs )
444
533
else :
445
534
raise AssertionError ("unsupported proxy" )
0 commit comments