8
8
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9
9
:license: BSD, see LICENSE for more details.
10
10
"""
11
+ import ssl
11
12
import random
12
- from typing import List , Tuple
13
+ import socket
14
+ import logging
15
+ import sysconfig
16
+
17
+ from pathlib import Path
18
+ from typing import List , Optional , Tuple , Any
13
19
from urllib import parse as urlparse
14
20
15
- from ..common .constants import DEFAULT_BUFFER_SIZE , DEFAULT_HTTP_PORT
16
- from ..common .utils import socket_connection , text_
21
+ from ..common .utils import text_
22
+ from ..common .constants import DEFAULT_HTTPS_PORT , DEFAULT_HTTP_PORT
23
+ from ..common .types import Readables , Writables
24
+ from ..core .connection import TcpServerConnection
25
+ from ..http .exception import HttpProtocolException
17
26
from ..http .parser import HttpParser
18
27
from ..http .websocket import WebsocketFrame
19
28
from ..http .server import HttpWebServerBasePlugin , httpProtocolTypes
20
29
30
+ logger = logging .getLogger (__name__ )
31
+
32
+ # We need CA bundle to verify TLS connection to upstream servers
33
+ PURE_LIB = sysconfig .get_path ('purelib' )
34
+ assert PURE_LIB
35
+ CACERT_PEM_PATH = Path (PURE_LIB ) / 'certifi' / 'cacert.pem'
21
36
37
+
38
+ # TODO: ReverseProxyPlugin and ProxyPoolPlugin are implementing
39
+ # a similar behavior. Abstract that particular logic out into its
40
+ # own class.
22
41
class ReverseProxyPlugin (HttpWebServerBasePlugin ):
23
42
"""Extend in-built Web Server to add Reverse Proxy capabilities.
24
43
@@ -39,35 +58,102 @@ class ReverseProxyPlugin(HttpWebServerBasePlugin):
39
58
"User-Agent": "curl/7.64.1"
40
59
},
41
60
"origin": "1.2.3.4, 5.6.7.8",
42
- "url": "https ://localhost/get"
61
+ "url": "http ://localhost/get"
43
62
}
44
63
"""
45
64
65
+ # TODO: We must use nginx python parser and
66
+ # make this plugin nginx.conf complaint.
46
67
REVERSE_PROXY_LOCATION : str = r'/get$'
68
+ # Randomly choose either http or https upstream endpoint.
69
+ #
70
+ # This is just to demonstrate that both http and https upstream
71
+ # reverse proxy works.
47
72
REVERSE_PROXY_PASS = [
48
73
b'http://httpbin.org/get' ,
74
+ b'https://httpbin.org/get' ,
49
75
]
50
76
77
+ def __init__ (self , * args : Any , ** kwargs : Any ):
78
+ super ().__init__ (* args , ** kwargs )
79
+ self .upstream : Optional [TcpServerConnection ] = None
80
+
51
81
def routes (self ) -> List [Tuple [int , str ]]:
52
82
return [
53
83
(httpProtocolTypes .HTTP , ReverseProxyPlugin .REVERSE_PROXY_LOCATION ),
54
84
(httpProtocolTypes .HTTPS , ReverseProxyPlugin .REVERSE_PROXY_LOCATION ),
55
85
]
56
86
57
- # TODO(abhinavsingh): Upgrade to use non-blocking get/read/write API.
87
+ def get_descriptors (self ) -> Tuple [List [socket .socket ], List [socket .socket ]]:
88
+ if not self .upstream :
89
+ return [], []
90
+ return [self .upstream .connection ], [self .upstream .connection ] if self .upstream .has_buffer () else []
91
+
92
+ def read_from_descriptors (self , r : Readables ) -> bool :
93
+ if self .upstream and self .upstream .connection in r :
94
+ try :
95
+ raw = self .upstream .recv (self .flags .server_recvbuf_size )
96
+ if raw is not None :
97
+ self .client .queue (raw )
98
+ else :
99
+ return True # Teardown because upstream server closed the connection
100
+ except ssl .SSLWantReadError :
101
+ logger .info ('Upstream server SSLWantReadError, will retry' )
102
+ return False
103
+ except ConnectionResetError :
104
+ logger .debug ('Connection reset by upstream server' )
105
+ return True
106
+ return super ().read_from_descriptors (r )
107
+
108
+ def write_to_descriptors (self , w : Writables ) -> bool :
109
+ if self .upstream and self .upstream .connection in w and self .upstream .has_buffer ():
110
+ try :
111
+ self .upstream .flush ()
112
+ except ssl .SSLWantWriteError :
113
+ logger .info ('Upstream server SSLWantWriteError, will retry' )
114
+ return False
115
+ except BrokenPipeError :
116
+ logger .debug (
117
+ 'BrokenPipeError when flushing to upstream server' ,
118
+ )
119
+ return True
120
+ return super ().write_to_descriptors (w )
121
+
58
122
def handle_request (self , request : HttpParser ) -> None :
59
- upstream = random .choice (ReverseProxyPlugin .REVERSE_PROXY_PASS )
60
- url = urlparse .urlsplit (upstream )
123
+ url = urlparse .urlsplit (
124
+ random .choice (ReverseProxyPlugin .REVERSE_PROXY_PASS ),
125
+ )
61
126
assert url .hostname
62
- with socket_connection ((text_ (url .hostname ), url .port if url .port else DEFAULT_HTTP_PORT )) as conn :
63
- conn .send (request .build ())
64
- self .client .queue (memoryview (conn .recv (DEFAULT_BUFFER_SIZE )))
127
+ port = url .port or (
128
+ DEFAULT_HTTP_PORT if url .scheme ==
129
+ b'http' else DEFAULT_HTTPS_PORT
130
+ )
131
+ self .upstream = TcpServerConnection (text_ (url .hostname ), port )
132
+ try :
133
+ self .upstream .connect ()
134
+ if url .scheme == b'https' :
135
+ self .upstream .wrap (
136
+ text_ (
137
+ url .hostname ,
138
+ ), ca_file = str (CACERT_PEM_PATH ),
139
+ )
140
+ self .upstream .queue (memoryview (request .build ()))
141
+ except ConnectionRefusedError :
142
+ logger .info (
143
+ 'Connection refused by upstream server {0}:{1}' .format (
144
+ text_ (url .hostname ), port ,
145
+ ),
146
+ )
147
+ raise HttpProtocolException ()
65
148
66
149
def on_websocket_open (self ) -> None :
67
150
pass
68
151
69
152
def on_websocket_message (self , frame : WebsocketFrame ) -> None :
70
153
pass
71
154
72
- def on_websocket_close (self ) -> None :
73
- pass
155
+ def on_client_connection_close (self ) -> None :
156
+ if self .upstream and not self .upstream .closed :
157
+ logger .debug ('Closing upstream server connection' )
158
+ self .upstream .close ()
159
+ self .upstream = None
0 commit comments