|
1 |
| - |
2 | 1 | # References used to code this file
|
3 |
| -# - https://github.com/senko/tornado-proxy/blob/master/tornado_proxy/proxy.py |
4 | 2 | # - https://raw.githubusercontent.com/tornadoweb/tornado/master/tornado/autoreload.py
|
5 | 3 |
|
6 |
| -import socket |
7 |
| -import tornado.httpserver |
8 |
| -import tornado.web |
9 |
| -import tornado.httpclient |
10 | 4 | import os
|
11 | 5 | import sys
|
12 | 6 | import types
|
13 | 7 | import fnmatch
|
14 |
| -import re |
| 8 | +import logging |
| 9 | +import socket, re |
| 10 | + |
| 11 | +import tornado.httpclient |
| 12 | +from tornado.iostream import IOStream |
| 13 | +from tornado.tcpserver import TCPServer |
| 14 | +from tornado.httputil import HTTPHeaders |
| 15 | + |
| 16 | +gen_log = logging.getLogger("tornado.general") |
15 | 17 |
|
16 | 18 | try:
|
17 | 19 | from urllib.parse import urlparse
|
@@ -43,13 +45,13 @@ def start(self, io_loop=None, check_time=2000):
|
43 | 45 |
|
44 | 46 | self.modify_times = {}
|
45 | 47 |
|
46 |
| - print("%d files to watch" % len(self._watched_files)) |
| 48 | + gen_log.info("%d files to watch" % len(self._watched_files)) |
| 49 | + |
47 | 50 | # tornado.ioloop.PeriodicCallback(self.build_watched_files, 5000, io_loop=self.io_loop).start()
|
48 | 51 | tornado.ioloop.PeriodicCallback(self._reload_on_update, check_time, io_loop=self.io_loop).start()
|
49 | 52 |
|
50 | 53 | def build_watched_files(self):
|
51 |
| - |
52 |
| - print("Reloading watched files") |
| 54 | + gen_log.info("Reloading watched files") |
53 | 55 | self._watched_files = set()
|
54 | 56 | for path in self.paths:
|
55 | 57 | for pattern in self.patterns:
|
@@ -120,140 +122,165 @@ def __init__(self, command):
|
120 | 122 | self.process = False
|
121 | 123 |
|
122 | 124 | def start(self):
|
| 125 | + gen_log.info("Start command: %s " % self.command) |
123 | 126 | self.process = tornado.process.Subprocess(self.command, shell=False)
|
124 | 127 | self.process.set_exit_callback(self._restart)
|
125 | 128 |
|
126 | 129 | def _restart(self, code):
|
127 | 130 | if self.reloading:
|
128 |
| - print("Create a new process") |
| 131 | + gen_log.info("Create a new process !!") |
129 | 132 | self.start()
|
130 | 133 | self.reloading = False
|
131 | 134 |
|
132 | 135 | def restart(self, *args, **kwargs):
|
133 | 136 | self.reloading = True
|
134 |
| - if self.process.proc.returncode != None: |
135 |
| - self.process.proc.terminate() |
136 | 137 |
|
137 |
| -class ProxyHandler(tornado.web.RequestHandler): |
138 |
| - SUPPORTED_METHODS = ['GET', 'POST', 'CONNECT'] |
| 138 | + if not self.process.proc.returncode: |
| 139 | + gen_log.info("Terminate process") |
| 140 | + self.process.proc.kill() |
| 141 | + |
| 142 | +def parse_headers(data): |
| 143 | + headers = HTTPHeaders() |
| 144 | + |
| 145 | + for line in data.splitlines(): |
| 146 | + if line: |
| 147 | + try: |
| 148 | + headers.parse_line(line) |
| 149 | + except Exception, e: |
| 150 | + break |
| 151 | + |
| 152 | + return headers |
| 153 | + |
| 154 | +def is_websocket(headers): |
| 155 | + """ |
| 156 | + Detect if the data is related to a websocket connection, should be called only once |
| 157 | +
|
| 158 | + http://en.wikipedia.org/wiki/WebSocket |
| 159 | + """ |
| 160 | + return "Upgrade" in headers and headers["Upgrade"] == "websocket" |
| 161 | + |
| 162 | +class StreamProxy(object): |
| 163 | + def __init__(self, io_source, io_target): |
| 164 | + self.public_io = io_source |
| 165 | + self.internal_io = io_target |
| 166 | + |
| 167 | + self.reset() |
| 168 | + |
| 169 | + def reset(self): |
| 170 | + self.init = False |
| 171 | + self.is_websocket = None |
| 172 | + self.is_html = None |
| 173 | + |
| 174 | + self.public_headers = None |
| 175 | + self.internal_headers = None |
| 176 | + |
| 177 | + self.content_length = 0 |
| 178 | + |
| 179 | + def handle(self): |
| 180 | + self.public_io.read_until_close(callback=self.end_public, streaming_callback=self.proxy_data_to_internal) |
| 181 | + self.internal_io.read_until_close(callback=self.end_internal, streaming_callback=self.proxy_data_to_public) |
| 182 | + |
| 183 | + def end_public(self, data): |
| 184 | + gen_log.debug("Proxy > callback end_public") |
| 185 | + self.internal_io.close() |
| 186 | + |
| 187 | + def end_internal(self, data): |
| 188 | + gen_log.debug("Proxy > callback end_internal") |
| 189 | + self.public_io.close() |
| 190 | + |
| 191 | + def proxy_data_to_internal(self, data): |
| 192 | + """ |
| 193 | + This method is used to send streamed data to the internal webserver |
| 194 | + """ |
| 195 | + if not self.init: |
| 196 | + self.init = True |
| 197 | + |
| 198 | + request, headers = data.split("\r\n", 1) |
| 199 | + gen_log.info("request: %s" % request) |
139 | 200 |
|
140 |
| - def initialize(self, state, proxy=None): |
141 |
| - self.state = state |
142 |
| - self.client = tornado.httpclient.AsyncHTTPClient() |
143 |
| - self.proxy = proxy or 'localhost:5001' |
| 201 | + self.public_headers = parse_headers(headers) |
| 202 | + self.is_websocket = is_websocket(self.public_headers) |
144 | 203 |
|
145 |
| - def handle_response(self, response): |
146 |
| - if response.error and not isinstance(response.error, tornado.httpclient.HTTPError): |
147 |
| - self.set_status(500) |
148 |
| - self.write('Internal server error:\n' + str(response.error)) |
149 |
| - else: |
150 |
| - self.set_status(response.code) |
| 204 | + if not self.is_websocket: |
| 205 | + # we don't want to deal with gzip content |
| 206 | + data = data.replace("Accept-Encoding: gzip, deflate\r\n", "") |
151 | 207 |
|
152 |
| - for header in ('Date', 'Cache-Control', 'Server', 'Content-Type', 'Location'): |
153 |
| - v = response.headers.get(header) |
| 208 | + self.internal_io.write(data) |
154 | 209 |
|
155 |
| - if v: |
156 |
| - self.set_header(header, v) |
| 210 | + def close_stream(self): |
| 211 | + if self.internal_headers and 'Content-Length' in self.internal_headers and self.content_length >= int(self.internal_headers['Content-Length']): |
| 212 | + if self.public_io.writing(): |
| 213 | + gen_log.debug(" > still data being written") |
157 | 214 |
|
158 |
| - if not response.body: |
159 | 215 | return
|
160 | 216 |
|
161 |
| - if response.headers['Content-Type'].startswith('text/html'): |
162 |
| - self.write(self.replace_tag(response)) |
163 |
| - else: |
164 |
| - self.write(response.body) |
| 217 | + gen_log.debug(" > closing all streams") |
165 | 218 |
|
166 |
| - self.finish() |
| 219 | + self.public_io.close() |
| 220 | + self.internal_io.close() |
167 | 221 |
|
168 |
| - def replace_tag(self, response): |
| 222 | + self.reset() |
| 223 | + |
| 224 | + def proxy_data_to_public(self, data): |
| 225 | + if self.is_websocket: |
| 226 | + self.public_io.write(data) |
| 227 | + return |
| 228 | + |
| 229 | + if not self.internal_headers: |
| 230 | + # we parse the response to replace esi tag |
| 231 | + self.internal_headers = parse_headers(data.split("\r\n", 1)[1]) |
| 232 | + |
| 233 | + self.is_html = 'Content-Type' in self.internal_headers and self.internal_headers['Content-Type'].startswith('text/html') |
| 234 | + |
| 235 | + self.content_length += len(data) |
| 236 | + |
| 237 | + if self.is_html and 'Content-Length' in self.internal_headers: |
| 238 | + data = data.replace('Content-Length:', 'X-Content-Length:') |
| 239 | + data = self.replace_tag(data) |
| 240 | + |
| 241 | + self.public_io.write(data, callback=self.close_stream) |
| 242 | + |
| 243 | + def replace_tag(self, data): |
169 | 244 | client = tornado.httpclient.HTTPClient()
|
170 |
| - headers = self.request.headers.copy() |
| 245 | + headers = self.public_headers.copy() |
171 | 246 | headers['Surrogate-Capability'] = 'abc=ESI/1.0'
|
172 | 247 |
|
173 |
| - if 'Set-Cookie' in response.headers: |
174 |
| - headers['Cookie'] = response.headers['Set-Cookie'] |
| 248 | + if 'Set-Cookie' in self.internal_headers: |
| 249 | + headers['Cookie'] = self.internal_headers['Set-Cookie'] |
| 250 | + |
| 251 | + if 'Accept-Encoding' in headers: |
| 252 | + # we don't want to deal with gzip content |
| 253 | + del(headers['Accept-Encoding']) |
175 | 254 |
|
176 | 255 | def get_contents(matcher):
|
177 | 256 | result = urlparse(matcher.group(2))
|
178 | 257 |
|
179 |
| - url = "http://%s%s?%s" % (self.proxy, result.path, result.query) |
| 258 | + url = "http://%s%s?%s" % ("localhost:5001", result.path, result.query) |
180 | 259 |
|
181 | 260 | try:
|
182 |
| - print("FETCHING ESI TAG: %s" % url) |
| 261 | + gen_log.info(" > start sub-request: %s" % url) |
183 | 262 |
|
184 | 263 | sub_response = client.fetch(tornado.httpclient.HTTPRequest(url, headers=headers))
|
185 | 264 |
|
| 265 | + gen_log.debug(" > end sub-request: %s" % url) |
| 266 | + |
186 | 267 | return sub_response.body
|
187 | 268 | except tornado.httpclient.HTTPError as e:
|
188 | 269 | return "<!-- error resolving : %s !-->" % url
|
189 | 270 |
|
190 |
| - content = re.sub(r"<esi:include\W[^>]*src=(\"|')([^\"']*)(\"|')[^>]*/>", get_contents, response.body, flags=re.IGNORECASE) |
| 271 | + content = re.sub(r"<esi:include\W[^>]*src=(\"|')([^\"']*)(\"|')[^>]*/>", get_contents, data, flags=re.IGNORECASE) |
191 | 272 |
|
192 | 273 | return content
|
193 | 274 |
|
194 |
| - @tornado.web.asynchronous |
195 |
| - def get(self): |
196 |
| - |
197 |
| - if self.state.reloading: |
198 |
| - self.write("reloading") |
| 275 | +class ProxyTCPServer(TCPServer): |
| 276 | + def __init__(self, sub_child_port, **kwargs): |
| 277 | + self.sub_child_port = sub_child_port |
199 | 278 |
|
200 |
| - self.finish() |
201 |
| - return |
202 |
| - |
203 |
| - url = "%s://%s%s" % ('http', self.proxy, self.request.uri) |
204 |
| - |
205 |
| - print("PROXY: Task: %s - %s %s" % (tornado.process.task_id(), self.request.method, url)) |
206 |
| - |
207 |
| - req = tornado.httpclient.HTTPRequest(url=url, |
208 |
| - method=self.request.method, body=self.request.body, |
209 |
| - headers=self.request.headers, follow_redirects=False, |
210 |
| - allow_nonstandard_methods=True |
211 |
| - ) |
212 |
| - |
213 |
| - try: |
214 |
| - self.client.fetch(req, self.handle_response) |
215 |
| - except tornado.httpclient.HTTPError as e: |
216 |
| - if hasattr(e, 'response') and e.response: |
217 |
| - self.handle_response(e.response) |
218 |
| - else: |
219 |
| - self.set_status(500) |
220 |
| - self.write('Internal server error:\n' + str(e)) |
221 |
| - self.finish() |
222 |
| - |
223 |
| - @tornado.web.asynchronous |
224 |
| - def post(self): |
225 |
| - return self.get() |
226 |
| - |
227 |
| - @tornado.web.asynchronous |
228 |
| - def connect(self): |
229 |
| - host, port = self.request.uri.split(':') |
230 |
| - client = self.request.connection.stream |
231 |
| - |
232 |
| - def read_from_client(data): |
233 |
| - upstream.write(data) |
234 |
| - |
235 |
| - def read_from_upstream(data): |
236 |
| - client.write(data) |
237 |
| - |
238 |
| - def client_close(data=None): |
239 |
| - if upstream.closed(): |
240 |
| - return |
241 |
| - if data: |
242 |
| - upstream.write(data) |
243 |
| - upstream.close() |
244 |
| - |
245 |
| - def upstream_close(data=None): |
246 |
| - if client.closed(): |
247 |
| - return |
248 |
| - if data: |
249 |
| - client.write(data) |
250 |
| - client.close() |
251 |
| - |
252 |
| - def start_tunnel(): |
253 |
| - client.read_until_close(client_close, read_from_client) |
254 |
| - upstream.read_until_close(upstream_close, read_from_upstream) |
255 |
| - client.write(b'HTTP/1.0 200 Connection established\r\n\r\n') |
| 279 | + super(ProxyTCPServer, self).__init__(**kwargs) |
256 | 280 |
|
| 281 | + def handle_stream(self, io_source, address): |
257 | 282 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
|
258 |
| - upstream = tornado.iostream.IOStream(s) |
259 |
| - upstream.connect((host, int(port)), start_tunnel) |
| 283 | + io_target = IOStream(s) |
| 284 | + |
| 285 | + proxy = StreamProxy(io_source, io_target) |
| 286 | + io_target.connect(("localhost", self.sub_child_port), proxy.handle) |
0 commit comments