├── README.md ├── certs ├── ca.crt ├── ca.key └── cert.key ├── setup_https_intercept.sh └── websocket_http_proxy.py /README.md: -------------------------------------------------------------------------------- 1 | I needed to be able to intercept secure websocket traffic in python while also properly proxying normal HTTP and HTTPS traffic. 2 | I could not find any such python tool to do that, so I made one myself. 3 | -------------------------------------------------------------------------------- /certs/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDHzCCAgegAwIBAgIUK/LlbgZ96scOJfikW712tNI5i2YwDQYJKoZIhvcNAQEL 3 | BQAwHzEdMBsGA1UEAwwUQ3VzdG9tIFdTLUhUVFAgcHJveHkwHhcNMjAxMjE3MTMy 4 | ODI0WhcNMzAxMjE1MTMyODI0WjAfMR0wGwYDVQQDDBRDdXN0b20gV1MtSFRUUCBw 5 | cm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANPwVJ4DI90EWM3E 6 | KTVdtJ2r+/ctIM/PRz4Bt1JIhxcf1tDkW8Ki0FAIsdOe6IFZpUVeKMaA19Po2EM2 7 | ALuTfUihIec41JQyWntycFPHgCpjTf/fZl8BSOIgoeXtPhyR8nFrt8iKFvu0ErT3 8 | HxU6JsBnxUaML1Xn8y7L+sLZxCsanCdyjw3dJcx8gjQxS3tD6lJukbwtasIOhm42 9 | tQ55fz3sccSSsajrIPEZ+eVquuyI+VEtLGrppMyeKyueS8txw+JGTZ3YO9CGlQZg 10 | Yir+dB/gAqxsryfZdhvotr7w71QGv3mSMYT1GvQhZDno8tK2HR1lGJjimog2zHtw 11 | VZT9N90CAwEAAaNTMFEwHQYDVR0OBBYEFMpJV6LrLFBYQH5r+G3cDbq82sjEMB8G 12 | A1UdIwQYMBaAFMpJV6LrLFBYQH5r+G3cDbq82sjEMA8GA1UdEwEB/wQFMAMBAf8w 13 | DQYJKoZIhvcNAQELBQADggEBAEIei65TCp8mbIJiPOTGhUgkrv3c0CIR2tzdRrOT 14 | x2r0KgP4hVjnWi/2D+Q0lgowRVwlMAgxcjphhLaRVj0KDHw5jI/RfZjt3uEpb9As 15 | bWZthaMYU3aPhQY+JWDuH732utDmX8xlj1pANazardrPuBCykizYXyIylVV4weYT 16 | ase0OB/ibsnScAC7d9q6SPte1+62vfGmP5F3qQWhpRDKCstpdu8TvDMFABM5LBCb 17 | 4vXgvw8qUb/2OS0/CTAf6lSjWVdmKKe39rhjNpnlqFiL9Lce7VpfMFTJLMn3kjG4 18 | gjypg+s7uEk4YoT/BpSlAvVf6qIj2KxYWtU72G4Dt8cTB+g= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /certs/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA0/BUngMj3QRYzcQpNV20nav79y0gz89HPgG3UkiHFx/W0ORb 3 | wqLQUAix057ogVmlRV4oxoDX0+jYQzYAu5N9SKEh5zjUlDJae3JwU8eAKmNN/99m 4 | XwFI4iCh5e0+HJHycWu3yIoW+7QStPcfFTomwGfFRowvVefzLsv6wtnEKxqcJ3KP 5 | Dd0lzHyCNDFLe0PqUm6RvC1qwg6Gbja1Dnl/PexxxJKxqOsg8Rn55Wq67Ij5US0s 6 | aumkzJ4rK55Ly3HD4kZNndg70IaVBmBiKv50H+ACrGyvJ9l2G+i2vvDvVAa/eZIx 7 | hPUa9CFkOejy0rYdHWUYmOKaiDbMe3BVlP033QIDAQABAoIBAA3F6YnXRGZhmO/O 8 | Vqs8KrewbJB0o1Q98TBLZkF3qyfKjuhGXtw4PndlCUFqa0u0qrPmWZoE14HS+PLt 9 | OID4JcUpi41+OPpkh1LMbhZTubWHfJMrTnjQGY9wdXT+xPGQXoQWbvweVT4IRsrx 10 | 4Fg9zjTkyYI7K/xWNYyN/v7YrhHq63d26VQqwNHFbAdCLuLThE6PHFRwS7PT8Puj 11 | PijMePKqLk2CObpM24j/I/KGJRRoRAyjaWYM1XaEJZAREHi7L7QU8VMgLVtILeY2 12 | HBJvnWpsk2yUGWwPV3yYclejLdZ5jS+C0/Fw0yqqzI5GWFqk+Yt6CiZBkiaiR9HG 13 | ZM6ftsECgYEA9cB04QSPEDp/UyObgJU7Qp2RHnwFtMp3Qo7HaUIcbVcQj/9JJGAq 14 | ZR/b/Cn0bFZwMm1LM2OJIDOn8vW/1GDbMBLy2/WSgWeYlHowwhKuHqtvnodGmHA5 15 | u0TuXv9GOR7WYGdlmQe6hEef32f0PFz4s+A5/+xDVqpg5YJ5utBTCu0CgYEA3Mbm 16 | jPjj/pPWfIDLR66mbj0a08BEjd4gYgMgFf/HLQCT0HprzJs++Go8jUpVVN1N0yhw 17 | L6Ok1HXDEBsF7NHQb22r9Wjyz2uK0CMd4GCz4a85pFTIcL/feLmHa4To9IAex7cc 18 | GUjUIMXA2/jpb0rsWBS9miERciMsnGAJJJhGErECgYAyz13bAERMCKw2llAaX8Bm 19 | 34kXknDjllDeFAMqwh56hNvJyfBncvKRAetL0ajVlXGRG4PG1jeNzuBUnXbSBEyN 20 | Pf20eKuX0cF8QV8/YPlbIfrr/fAcqGdnTrMyNPlh6fxM5lPdNfncS6rEWAvpRxes 21 | qRmUzlaF3qg2C6n7dAMXaQKBgQCkrWgC9xN5LOTr8VWvnkJeMA32rI92Ep2s/g/M 22 | 2QDEPI4FD4uhDpulx/hqm2uS5Y/LVXp4zmOAZmadeMqunsIOm4uMfj+/H8RnBAqg 23 | 9wC18nHYdbUdBMG31wt/05+/4GEVLywyX/R6jYcRjVTxwr0P5kiW0tuke0AnVCr9 24 | tOdvwQKBgQDjXd8a43D16HbIthlSyJW+43dg/pTGiu9tHqJZooMFVxnyMKuXU8v3 25 | NjgOed11tYEm5VW7G8wQiztzHfDp/FKY0lnCJw+T4N+CiBDeBTej7Jg+uaMG1vyU 26 | BB1M8fkFdPF5DxsiGuuBsBMIgCYDcxX2TXeKhw2Hf9dNvrQcAIJ4BQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /certs/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA0UPVlP/9/RbOIyWJ+ZdnKhjudPK9uJ8OVj+YoosxV88isgXU 3 | TW2aOjEAgcFWZ9YpsfbdDbdH8cSiUQTlg/BB/WqTF33zKQ+j9zO2EEjxtsgfpEvg 4 | Gb5USbCegyHGkmjZNFlR9EMWP6HEC1FfXtThwiWH4LQDuEeFHws6OewwzmCC5bk1 5 | aI/DdIOkQ5z3juIzxQzkyUotGPcghJq1vGh9JL0ppBX8t97rjRhPJP5Ta/WHnSGQ 6 | O3MNtI9E6YK8mqb9UkGkddm8zQV/yVZDvP84KAyg2Xtal0ybhvPRvOgv+wMZ04TG 7 | /mf2fT5GTlUv/OuXgyjU2Y3O3RlbUydpK6q45QIDAQABAoIBAQDPqXwqhzp5zAa1 8 | tAvOhiNXEDSaE0SYECb/Cc8jBfPqSmAIv2Yli+0vb+8r1Ds1gb4Qn4RPlyCq19Gn 9 | iq9kFai2nOrothD2H1I7/rHeSTSsiL11oeH6SfiEw+MZCAxwv+FDZJvCREywyous 10 | G40Do0eBDRNgteK3HSoKW27lifAI6strQc42Teb8miw0ClzDW+99qEcH3g62HzqO 11 | cF1hwnWfNgr4BIPG5iGt2B0Ohd9lJQKTVNgMrWU4+0/dUUZKRRwT0EgP37hcYzqq 12 | 4R+zffu3om2S8wvABFGB4m7Ruvx63v9AINlqcAoYPDODgETwoSEzLSugvKQMXXmR 13 | ryTOB+XBAoGBAPAnJc6aPOTpeyb7DjEc8Fb2GiFNBmw1Io5SxCjNwA37mZXkvLn4 14 | DzUCeDHi/8DmB5qtaA0py4hAkCqEJ/BMWpaU35jEiy6NxNCJZceeRhjHmellScMj 15 | ul8cXqAuFZBeSkJS+pyyRG2bCT7ixDCtIrF1QuNCBgq4Q5hH3ImqX6uVAoGBAN8S 16 | 5zVZbPta6ikfpCJxQ9Vg0HWp0dFV1b3OhUMG5ghUOTrxfuql+0Es9gR42gAIOYMk 17 | VxAE+kR9fCM/C6/gHV5ZCU4mNY4V1AaLq+dS6JEEOhdtpCXxXltc8Y+evfj8xlfN 18 | gK8y8cX3eVpiOe986yhFLcJVIo9LZrbJJLRRmgQRAoGBAOgP3D/N2WQWnjOnzCn3 19 | XeOagtuFE9zCZ7cCEZ2gXKLmap5m31wRcZh233DNeviLD/QO9wopRg1O3kDHXdSd 20 | 47e4+mwkGJ6Ozg35h0mjDvdpAbiAcQvJXZIE1weQILRV+QooJxX+SZNkikWjWZPz 21 | 6h9zQYRbS31WW7MuVdUNts8VAoGAQuGHAp7CJwZWCGhdLJpq7RGuzmhQ6QNkJxlB 22 | KRxrYXnnAr4fADktgJf7VtHpAnN00tXVaI8lfd1ll6eyWFPIWl41hQG9stDmlePQ 23 | cXWRFtF+nUGZImsgkCHoptfAO2OGEBMkDuMmS+Vrs+aZWi1Iz/UVyBsAVpgTvp/F 24 | 6m5A0BECgYEAwkqPSPQZ7Eguj7VLY+6BwEKD0nlKGBxiDprx+9LStryW2a7JlRKV 25 | hPPIAiYao7lNf2CYvX+OqobAIJXIBo+YskNLkN4EeiXQUG+/0k7KVDRsr3cn5xhK 26 | 399Juf+AkdI3NRHdNw8etwC1IYttQz3Kpx0MJqe+oHkyGQJcjdyEytU= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /setup_https_intercept.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Save original dir 4 | pushd . >/dev/null 5 | # Go to script location 6 | cd "${0%/*}" 7 | 8 | mkdir certs 9 | cd certs 10 | openssl genrsa -out ca.key 2048 11 | openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=Custom WS-HTTP proxy" 12 | openssl genrsa -out cert.key 2048 13 | mkdir specific_certs 14 | 15 | # Back to orginal dir 16 | popd 17 | -------------------------------------------------------------------------------- /websocket_http_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | import struct 5 | import pathlib 6 | import sys 7 | import threading 8 | import socket 9 | import errno 10 | import ssl 11 | import time 12 | import select 13 | import json 14 | import re 15 | import gzip 16 | import brotli 17 | import http.client 18 | import html 19 | import queue 20 | from urllib.parse import urlparse, urlsplit, parse_qsl 21 | from base64 import b64encode 22 | from hashlib import sha1 23 | from io import StringIO, BytesIO 24 | from http.server import BaseHTTPRequestHandler 25 | from http.server import HTTPServer 26 | from socketserver import ThreadingMixIn 27 | from subprocess import Popen, PIPE 28 | 29 | # This is the websocket client we use to connect to the remote 30 | import websocket 31 | 32 | ########## 33 | # Config # 34 | ########## 35 | # Our local port we want to listen on 36 | proxy_port = 9999 37 | # Do we have SSL certs? 38 | secure = True 39 | 40 | # Nr of characters after which messages are truncated (0 to turn off message printing) 41 | trunc = 125 42 | #trunc = 0 43 | 44 | ################## 45 | # Implementation # 46 | ################## 47 | 48 | off='\033[0m' 49 | red='\033[0;91m' 50 | grn='\033[0;32m' 51 | yel='\033[0;33m' 52 | cya='\033[0;36m' 53 | 54 | class WebSocketError(Exception): 55 | pass 56 | 57 | class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): 58 | # Handle requests in a separate thread 59 | daemon_threads = True 60 | 61 | class HttpProxy(BaseHTTPRequestHandler): 62 | cakey = None 63 | cacert = None 64 | certkey = None 65 | certdir = None 66 | 67 | timeout = 100 68 | lock = threading.Lock() 69 | protocol_version = "HTTP/1.1" 70 | 71 | def __init__(self, *args, **kwargs): 72 | if secure: 73 | self.cakey = pathlib.Path(__file__).parent.joinpath("certs/ca.key") 74 | self.cacert = pathlib.Path(__file__).parent.joinpath("certs/ca.crt") 75 | self.certkey = pathlib.Path(__file__).parent.joinpath("certs/cert.key") 76 | self.certdir = pathlib.Path(__file__).parent.joinpath("certs/specific_certs/") 77 | self.extensionfile = pathlib.Path(__file__).parent.joinpath("certs/extfile.tmp") 78 | self.tls = threading.local() 79 | self.tls.conns = {} 80 | 81 | BaseHTTPRequestHandler.__init__(self, *args, **kwargs) 82 | 83 | def do_CONNECT(self): 84 | if secure: 85 | self.connect_intercept() 86 | else: 87 | self.connect_relay() 88 | 89 | def connect_intercept(self): 90 | hostname = self.path.split(':')[0] 91 | specific_cert = self.certdir.joinpath(f"{hostname}.crt") 92 | 93 | with self.lock: 94 | if not specific_cert.is_file(): 95 | epoch = "%d" % (time.time() * 1000) 96 | self.extensionfile.write_text("subjectAltName=DNS:%s" % hostname) 97 | p1 = Popen(["openssl", "req", "-new", "-key", self.certkey, "-subj", "/CN=%s" % hostname, "-addext", "subjectAltName = DNS:%s" % hostname], stdout=PIPE) 98 | p2 = Popen(["openssl", "x509", "-req", "-extfile", self.extensionfile, "-days", "365", "-CA", self.cacert, "-CAkey", self.cakey, "-set_serial", epoch, "-out", specific_cert], stdin=p1.stdout, stderr=PIPE) 99 | p2.communicate() 100 | 101 | self.wfile.write(("%s %d %s\r\n" % (self.protocol_version, 200, 'Connection Established')).encode()) 102 | if not hasattr(self, '_headers_buffer'): 103 | self._headers_buffer = [] 104 | self.end_headers() 105 | 106 | ssl_settings = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 107 | ssl_settings.check_hostname = False 108 | ssl_settings.load_cert_chain(certfile=specific_cert, keyfile=self.certkey) 109 | self.connection = ssl_settings.wrap_socket(self.connection, server_side=True) 110 | self.rfile = self.connection.makefile("rb", self.rbufsize) 111 | self.wfile = self.connection.makefile("wb", self.wbufsize) 112 | 113 | conntype = self.headers.get('Proxy-Connection', '') 114 | if self.protocol_version == "HTTP/1.1" and conntype.lower() != 'close': 115 | self.close_connection = 0 116 | else: 117 | self.close_connection = 1 118 | 119 | def connect_relay(self): 120 | address = self.path.split(':', 1) 121 | address[1] = int(address[1]) or 443 122 | try: 123 | s = socket.create_connection(address, timeout=self.timeout) 124 | except Exception as e: 125 | self.send_error(502) 126 | return 127 | self.send_response(200, 'Connection Established') 128 | self.end_headers() 129 | 130 | conns = [self.connection, s] 131 | self.close_connection = 0 132 | while not self.close_connection: 133 | rlist, wlist, xlist = select.select(conns, [], conns, self.timeout) 134 | if xlist or not rlist: 135 | break 136 | for r in rlist: 137 | other = conns[1] if r is conns[0] else conns[0] 138 | data = r.recv(8192) 139 | if not data: 140 | self.close_connection = 1 141 | break 142 | other.sendall(data) 143 | 144 | def do_GET(self): 145 | if self.path == 'http://ca.crt/': 146 | self.send_cacert() 147 | return 148 | 149 | req = self 150 | content_length = int(req.headers.get('Content-Length', 0)) 151 | req_body = self.rfile.read(content_length) if content_length else None 152 | 153 | if req.path[0] == '/': 154 | if isinstance(self.connection, ssl.SSLSocket): 155 | req.path = f"https://{req.headers['Host']}{req.path}" 156 | else: 157 | req.path = f"http://{req.headers['Host']}{req.path}" 158 | 159 | #print(f"REQ for {req.path}") 160 | req_body_modified = self.request_handler(req, req_body) 161 | if req_body_modified is False: 162 | self.send_error(403) 163 | return 164 | elif req_body_modified is not None: 165 | req_body = req_body_modified 166 | req.headers['Content-length'] = str(len(req_body)) 167 | 168 | u = urlsplit(req.path) 169 | scheme, netloc, path = u.scheme, u.netloc, (u.path + '?' + u.query if u.query else u.path) 170 | assert scheme in ('http', 'https') 171 | if netloc: 172 | req.headers['Host'] = netloc 173 | setattr(req, 'headers', self.filter_headers(req.headers)) 174 | 175 | try: 176 | origin = (scheme, netloc) 177 | if not origin in self.tls.conns: 178 | if scheme == 'https': 179 | self.tls.conns[origin] = http.client.HTTPSConnection(netloc, timeout=self.timeout) 180 | else: 181 | self.tls.conns[origin] = http.client.HTTPConnection(netloc, timeout=self.timeout) 182 | conn = self.tls.conns[origin] 183 | conn.request(self.command, path, req_body, dict(req.headers)) 184 | res = conn.getresponse() 185 | 186 | version_table = {10: 'HTTP/1.0', 11: 'HTTP/1.1'} 187 | setattr(res, 'headers', res.msg) 188 | setattr(res, 'response_version', version_table[res.version]) 189 | 190 | # support streaming 191 | if not 'Content-Length' in res.headers and 'no-store' in res.headers.get('Cache-Control', ''): 192 | self.response_handler(req, req_body, res, '') 193 | setattr(res, 'headers', self.filter_headers(res.headers)) 194 | self.relay_streaming(res) 195 | with self.lock: 196 | self.save_handler(req, req_body, res, '') 197 | return 198 | 199 | res_body = res.read() 200 | except Exception as e: 201 | print(e) 202 | if origin in self.tls.conns: 203 | del self.tls.conns[origin] 204 | self.send_error(502) 205 | return 206 | 207 | content_encoding = res.headers.get('Content-Encoding', 'identity') 208 | res_body_plain = self.decode_content_body(res_body, content_encoding) 209 | 210 | res_body_modified = self.response_handler(req, req_body, res, res_body_plain) 211 | if res_body_modified is False: 212 | self.send_error(403) 213 | return 214 | elif res_body_modified is not None: 215 | res_body_plain = res_body_modified 216 | res_body = self.encode_content_body(res_body_plain, content_encoding) 217 | res.headers['Content-Length'] = str(len(res_body)) 218 | 219 | setattr(res, 'headers', self.filter_headers(res.headers)) 220 | 221 | self.wfile.write(("%s %d %s\r\n" % (self.protocol_version, res.status, res.reason)).encode()) 222 | self.wfile.write(str(res.headers).encode()) 223 | if len(res_body) != 0: 224 | self.wfile.write(res_body) 225 | self.wfile.flush() 226 | 227 | with self.lock: 228 | self.save_handler(req, req_body, res, res_body_plain) 229 | 230 | do_HEAD = do_GET 231 | do_POST = do_GET 232 | do_PUT = do_GET 233 | do_DELETE = do_GET 234 | do_OPTIONS = do_GET 235 | 236 | def relay_streaming(self, res): 237 | self.wfile.write(("%s %d %s\r\n" % (self.protocol_version, res.status, res.reason)).encode()) 238 | self.wfile.write(str(res.headers).encode()) 239 | self.end_headers() 240 | try: 241 | while True: 242 | chunk = res.read(8192) 243 | if not chunk: 244 | break 245 | self.wfile.write(chunk) 246 | self.wfile.flush() 247 | except socket.error: 248 | # connection closed by client 249 | pass 250 | 251 | def filter_headers(self, headers): 252 | # http://tools.ietf.org/html/rfc2616#section-13.5.1 253 | hop_by_hop = ('connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade') 254 | #hop_by_hop = ('keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding') 255 | for k in hop_by_hop: 256 | del headers[k] 257 | 258 | # accept only supported encodings 259 | if 'Accept-Encoding' in headers: 260 | ae = headers['Accept-Encoding'] 261 | filtered_encodings = [x for x in re.split(r',\s*', ae) if x in ('identity', 'gzip', 'x-gzip', 'deflate', 'br')] 262 | headers['Accept-Encoding'] = ', '.join(filtered_encodings) 263 | 264 | return headers 265 | 266 | def encode_content_body(self, text, encoding): 267 | if encoding == 'identity': 268 | data = text 269 | elif encoding in ('gzip', 'x-gzip'): 270 | io = BytesIO() 271 | with gzip.GzipFile(fileobj=io, mode='wb') as f: 272 | f.write(text) 273 | data = io.getvalue() 274 | elif encoding == 'deflate': 275 | data = zlib.compress(text) 276 | elif encoding == 'br': 277 | data = brotli.compress(text) 278 | else: 279 | raise Exception("Unknown Content-Encoding: %s" % encoding) 280 | return data 281 | 282 | def decode_content_body(self, data, encoding): 283 | if encoding == 'identity': 284 | text = data 285 | elif encoding in ('gzip', 'x-gzip'): 286 | io = BytesIO(data) 287 | with gzip.GzipFile(fileobj=io) as f: 288 | text = f.read() 289 | elif encoding == 'deflate': 290 | try: 291 | text = zlib.decompress(data) 292 | except zlib.error: 293 | text = zlib.decompress(data, -zlib.MAX_WBITS) 294 | elif encoding == 'br': 295 | text = brotli.compress(data) 296 | else: 297 | raise Exception("Unknown Content-Encoding: %s" % encoding) 298 | return text 299 | 300 | def send_cacert(self): 301 | with open(self.cacert, 'rb') as f: 302 | data = f.read() 303 | 304 | self.wfile.write(("%s %d %s\r\n" % (self.protocol_version, 200, 'OK')).encode()) 305 | self.send_header('Content-Type', 'application/x-x509-ca-cert') 306 | self.send_header('Content-Length', len(data)) 307 | self.send_header('Connection', 'close') 308 | self.end_headers() 309 | self.wfile.write(data) 310 | 311 | def log_error(self, *args, **kwargs): 312 | # Hacky way to suppress timeout errors 313 | #if 'Request timed out' in args[0]: 314 | # return 315 | BaseHTTPRequestHandler.log_error(self, *args, **kwargs) 316 | 317 | 318 | def print_info(self, req, req_body, res, res_body): 319 | def _parse_qsl(s): 320 | return '\n'.join("%-20s %s" % (k, v) for k, v in parse_qsl(s, keep_blank_values=True)) 321 | 322 | req_header_text = "%s %s %s\n%s" % (req.command, req.path, req.request_version, req.headers) 323 | res_header_text = "%s %d %s\n%s" % (res.response_version, res.status, res.reason, res.headers) 324 | 325 | print(f"{yel}{req_header_text}{off}") 326 | 327 | u = urlsplit(req.path) 328 | if u.query: 329 | query_text = _parse_qsl(u.query) 330 | print(f"{grn}==== QUERY PARAMETERS ====\n{query_text}\n{off}") 331 | 332 | cookie = req.headers.get('Cookie', '') 333 | if cookie: 334 | cookie = _parse_qsl(re.sub(r';\s*', '&', cookie)) 335 | print(f"{grn}==== COOKIE ====\n{cookie}\n{off}") 336 | 337 | auth = req.headers.get('Authorization', '') 338 | if auth.lower().startswith('basic'): 339 | token = auth.split()[1].decode('base64') 340 | print(f"{red}==== BASIC AUTH ====\n{token}\n{off}") 341 | 342 | if req_body is not None: 343 | req_body_text = None 344 | content_type = req.headers.get('Content-Type', '') 345 | 346 | if content_type.startswith('application/x-www-form-urlencoded'): 347 | req_body_text = _parse_qsl(req_body) 348 | elif content_type.startswith('application/json'): 349 | try: 350 | json_obj = json.loads(req_body) 351 | json_str = json.dumps(json_obj, indent=2) 352 | if json_str.count('\n') < 50: 353 | req_body_text = json_str 354 | else: 355 | lines = json_str.splitlines() 356 | req_body_text = "%s\n(%d lines)" % ('\n'.join(lines[:50]), len(lines)) 357 | except ValueError: 358 | req_body_text = req_body 359 | elif len(req_body) < 1024: 360 | req_body_text = req_body 361 | 362 | if req_body_text: 363 | print(f"{grn}==== REQUEST BODY ====\n{req_body_text}\n{off}") 364 | 365 | print(f"{cya}{res_header_text}{off}") 366 | 367 | cookies = res.headers.get_all('Set-Cookie') 368 | if cookies: 369 | cookies = '\n'.join(cookies) 370 | print(f"{red}==== SET-COOKIE ====\n{cookies}\n{off}") 371 | 372 | if res_body is not None: 373 | res_body_text = None 374 | content_type = res.headers.get('Content-Type', '') 375 | 376 | if content_type.startswith('application/json'): 377 | try: 378 | json_obj = json.loads(res_body) 379 | json_str = json.dumps(json_obj, indent=2) 380 | if json_str.count('\n') < 50: 381 | res_body_text = json_str 382 | else: 383 | lines = json_str.splitlines() 384 | res_body_text = "%s\n(%d lines)" % ('\n'.join(lines[:50]), len(lines)) 385 | except ValueError: 386 | res_body_text = res_body 387 | elif content_type.startswith('text/html'): 388 | m = re.search(r'