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