├── CA.crt ├── CertTool.py ├── Docs ├── HowItWorks.gif ├── changelog.txt ├── readme.html └── readme.txt ├── LICENSE ├── ProxHTTPSProxy.py ├── ProxyTool.py ├── README.md ├── cacert.pem └── config.ini /CA.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+DCCAuCgAwIBAgIBADANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEX 3 | MBUGA1UECgwOUHJveEhUVFBTUHJveHkxEjAQBgNVBAsMCXB5T3BlblNTTDEaMBgG 4 | A1UEAwwRUHJveEhUVFBTUHJveHkgQ0EwHhcNMTUxMDAxMDc0MDI1WhcNMjUwOTMw 5 | MDc0MDI1WjBWMQswCQYDVQQGEwJDTjEXMBUGA1UECgwOUHJveEhUVFBTUHJveHkx 6 | EjAQBgNVBAsMCXB5T3BlblNTTDEaMBgGA1UEAwwRUHJveEhUVFBTUHJveHkgQ0Ew 7 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9mQTpJlPDjEqLnovcT/AL 8 | YwjoP2Siowor2yeEKaGKJjBamu3OkYhS+2kzJhcii705uTCal/f6gDIlnhYXlPEh 9 | L7Z0wsT9IePJSU9+yNtUrWYILfRg1+XkpZVqrPfjBk8usTjtC4kG9xRZno/TeZj/ 10 | 2Qror/C989Hl+bqZ4p31/l1Jcml/W01PDiGcqESS15bKk24azJ1w69Zhjwn8uZKc 11 | Mnq2myrJsl8fZ82gV2fV8yydhpDudPpHy8y/9U8FfsmODi75aH4A1NkK/2FZyBKE 12 | 1OEYd+JfL7QmBCCjIt9AREXA/77HSuj6OXoKWZ0AVuiHLA/psfcRL4+QXd1UtXbF 13 | AgMBAAGjgdAwgc0wDwYDVR0TAQH/BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAgQw 14 | ewYDVR0lAQH/BHEwbwYIKwYBBQUHAwEGCCsGAQUFBwMCBggrBgEFBQcDBAYIKwYB 15 | BQUHAwgGCisGAQQBgjcCARUGCisGAQQBgjcCARYGCisGAQQBgjcKAwEGCisGAQQB 16 | gjcKAwMGCisGAQQBgjcKAwQGCWCGSAGG+EIEATALBgNVHQ8EBAMCAQYwHQYDVR0O 17 | BBYEFKPBao+B+YH0tMNHNGoLv/3ncZyvMA0GCSqGSIb3DQEBCwUAA4IBAQCFZOPd 18 | SldrKkekP/tO/WnGgXEus8z4Ei7TEAm6qkSJ/r0ZaTKmGek370xvVG4myl0Hngr+ 19 | F6blIUzGi8e9mp/2vONhPYKTAg+Y4h5tKz9S6SyvbypBMa4YNZw8DNfd4uVLL/b6 20 | psQcYfMPMpRdM7GlLZbxY9AHyCaHZszc3bSBM/lIhLWJH0pR7QSZZ+cJUHYKODZ8 21 | Cs8goAcA/mJ4h1g63EP1Snlw4U3vMJ8ZQRAeg46FAZATwte9SaahAq1kLql/P8jg 22 | A4gM9xvfRgVOIrfxSHDlnw6gVK6u/WhD4SWIsS2JfNljgUmrcMWB37kNdT3i0yO7 23 | Vydw/UIJw1pqktqz 24 | -----END CERTIFICATE----- 25 | -----BEGIN PRIVATE KEY----- 26 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9mQTpJlPDjEqL 27 | novcT/ALYwjoP2Siowor2yeEKaGKJjBamu3OkYhS+2kzJhcii705uTCal/f6gDIl 28 | nhYXlPEhL7Z0wsT9IePJSU9+yNtUrWYILfRg1+XkpZVqrPfjBk8usTjtC4kG9xRZ 29 | no/TeZj/2Qror/C989Hl+bqZ4p31/l1Jcml/W01PDiGcqESS15bKk24azJ1w69Zh 30 | jwn8uZKcMnq2myrJsl8fZ82gV2fV8yydhpDudPpHy8y/9U8FfsmODi75aH4A1NkK 31 | /2FZyBKE1OEYd+JfL7QmBCCjIt9AREXA/77HSuj6OXoKWZ0AVuiHLA/psfcRL4+Q 32 | Xd1UtXbFAgMBAAECggEAK5AHEtLdmCaZ0i6hkANF8jfVChfWtY+kfKMkFzfBiA5y 33 | Ob8zOK0zl21wpHHyCtv0pFiqlDqqnYHrA72o8c4lAS0HTRibTlYFEnCntUfNLU2S 34 | DfsRFVdF2R06kYIgiqcedmn93Gk0GMeYg2btQPfFcbOa0A/szphA+AhDGax6AtUD 35 | gl7+QT4j5HE598ghtl5/DZ4tiw4cfuWjC6ph7tHbKKq9wCH6wQf9kcyIA4ozVBKV 36 | fejE9t4BfVPxzbxN+Quu0+S5SGnKzg1uY+/99Jo1IqtJGQq1OlPFLjVnxUF1N+Wp 37 | nJVBHorILQtGhYxW4QlWsHMdc7iB5r4eFSuKaivMGQKBgQDrCDviK35IuQylxKE8 38 | Xu/eSwPpOwF4ibASgaPmJt+t4O4JLX1GLZX899KDIeXaAFqcp2EF4QUhX2ioGIiO 39 | GGFFAmOHIDvCFfiNpM1m7F0Njj8gedFfT4Yhv73htUlh5zA8vfuv4PN4ZGfjK3L9 40 | sW9OEMUDTey5D/6Wq/IZ8ZGTwwKBgQDOgyJSJQk8K0n4AGLPyP/wmXL4w/xi8IOC 41 | kafs1XsQCn5OvKJZY5ZNyoSzhkKmlUQTO/tmZ5flOk6wVs34StSNSo+JQub5vEYi 42 | gXVNwYB6oPYMtdfPYLSy59h0REugNfkunRj5crPyVttJiVZpxBJHxgnIqJcBj+WT 43 | ehHNJpRK1wKBgFx4s97rj9ca/4/lCi8Phz6lsxc7gPuk6KKPYSX3W4A1BFKWFDjd 44 | TKrn8mpnluCrzPrfm/vNKdCUkj+4z1lg3DxjkTckBn75V/6avbnl+0KPGeU0KJ1g 45 | U3zJzPKV+hZL+J2dff4X+pL+piUp/ic0fX9wd6MyMJYrZdZwNmPguI8zAoGAARJF 46 | F1AB4EIJPDQkTxen3EOviQLbSFgfFopS6LOi0856IUZxQS13Fig60AOeTObxV3g0 47 | Ma/P5eyLg/avUt5wg9sjK38hW6JSatNpHGIonHpBTIeU+wpxZYw2X0QLcGVXSZqf 48 | CoxByrwQny0LObk+rwij/FqDjgqFEmLLvNi6ZDkCgYEA3xgeLNBGf5ghYhgX9PKO 49 | Y1Rg6y1ElqxMCoovkpNlA6bVkyxcYIItIW1npsSeM45x+6Blit74LuleE9UYoN8j 50 | BC8ADhYN7ywb0juCnpLrKuWl/3XNg3wREhvhHfEK1agEysVFUohFwdtfyW4gNWia 51 | wli1LGvTwY1aFj8K29VKvkE= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /CertTool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | "Cert Tools, pyOpenSSL version" 5 | 6 | __author__ = 'phoenix' 7 | __version__ = '0.1' 8 | 9 | CA = "CA.crt" 10 | CERTDIR = "Certs" 11 | # Temp list for generating certs 12 | workingList = set() 13 | 14 | import os 15 | import time 16 | import OpenSSL 17 | 18 | def create_CA(capath): 19 | key = OpenSSL.crypto.PKey() 20 | key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) 21 | ca = OpenSSL.crypto.X509() 22 | ca.set_serial_number(0) 23 | # Value 2 means v3 24 | ca.set_version(2) 25 | subj = ca.get_subject() 26 | subj.countryName = 'CN' 27 | subj.organizationName = 'ProxHTTPSProxy' 28 | subj.organizationalUnitName = 'pyOpenSSL' 29 | subj.commonName = 'ProxHTTPSProxy CA' 30 | ca.gmtime_adj_notBefore(0) 31 | ca.gmtime_adj_notAfter(24 * 60 * 60 * 3652) 32 | ca.set_issuer(ca.get_subject()) 33 | ca.set_pubkey(key) 34 | ca.add_extensions( 35 | [OpenSSL.crypto.X509Extension(b"basicConstraints", True, b"CA:TRUE"), 36 | # mozilla::pkix doesn't handle the Netscape Cert Type extension (which is problematic when it's marked critical) 37 | # https://bugzilla.mozilla.org/show_bug.cgi?id=1009161 38 | OpenSSL.crypto.X509Extension(b"nsCertType", False, b"sslCA"), 39 | OpenSSL.crypto.X509Extension(b"extendedKeyUsage", True, b"serverAuth,clientAuth,emailProtection,timeStamping,msCodeInd,msCodeCom,msCTLSign,msSGC,msEFS,nsSGC"), 40 | OpenSSL.crypto.X509Extension(b"keyUsage", False, b"keyCertSign, cRLSign"), 41 | OpenSSL.crypto.X509Extension(b"subjectKeyIdentifier", False, b"hash", subject=ca)]) 42 | ca.sign(key, 'sha256') 43 | with open(capath, 'wb') as fp: 44 | fp.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ca)) 45 | fp.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) 46 | 47 | def get_cert(name, cafile=CA, certdir=CERTDIR): 48 | """Return cert file path. Create it if it doesn't exist. 49 | 50 | cafile: the CA file to create dummpy cert files 51 | certdir: the path where cert files are looked for or created 52 | """ 53 | certfile = os.path.join(certdir, name + '.crt') 54 | if not os.path.exists(certfile): 55 | dummy_cert(cafile, certfile, name) 56 | return certfile 57 | 58 | def dummy_cert(cafile, certfile, commonname): 59 | """Generates and writes a certificate to certfile 60 | commonname: Common name for the generated certificate 61 | Ref: https://github.com/mitmproxy/netlib/blob/master/netlib/certutils.py 62 | """ 63 | if certfile in workingList: 64 | # Another thread is working on it, wait until it finish 65 | while True: 66 | time.sleep(0.2) 67 | if certfile not in workingList: break 68 | else: 69 | workingList.add(certfile) 70 | with open(cafile, "rb") as file: 71 | content = file.read() 72 | ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, content) 73 | key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, content) 74 | cert = OpenSSL.crypto.X509() 75 | # Value 2 means v3 76 | cert.set_version(2) 77 | cert.gmtime_adj_notBefore(0) 78 | cert.gmtime_adj_notAfter(60 * 60 * 24 * 3652) 79 | cert.set_issuer(ca.get_subject()) 80 | if commonname.startswith('.'): 81 | domain = '*' + commonname 82 | else: 83 | domain = commonname 84 | cert.get_subject().CN = domain 85 | cert.set_serial_number(int(time.time()*10000)) 86 | cert.set_pubkey(ca.get_pubkey()) 87 | cert.add_extensions( 88 | [OpenSSL.crypto.X509Extension(b"subjectAltName", False, str.encode("DNS:"+domain))]) 89 | cert.sign(key, "sha256") 90 | with open(certfile, 'wb') as fp: 91 | fp.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)) 92 | fp.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)) 93 | workingList.remove(certfile) 94 | 95 | def startup_check(): 96 | if not os.path.exists(CERTDIR): 97 | os.mkdir(CERTDIR) 98 | if not os.path.exists(CERTDIR): 99 | print("%s directory does not exist!") 100 | print("Please create it and restart the program!") 101 | input() 102 | raise SystemExit 103 | 104 | if not os.path.exists(CA): 105 | print("Creating CA ...") 106 | create_CA(CA) 107 | if not os.path.exists(CA): 108 | print("Failed to create CA :(") 109 | else: 110 | print("* Please import created %s to your client's store of trusted certificate authorities." % CA) 111 | print("* Please delete all files under %s directory!" % CERTDIR) 112 | print("* Then restart the program!") 113 | input() 114 | raise SystemExit 115 | 116 | startup_check() 117 | 118 | if __name__ == "__main__": 119 | print("All Good!") 120 | -------------------------------------------------------------------------------- /Docs/HowItWorks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wheever/ProxHTTPSProxyMII/da06c097fbe636d073ffc7a039b89d0e222072fc/Docs/HowItWorks.gif -------------------------------------------------------------------------------- /Docs/changelog.txt: -------------------------------------------------------------------------------- 1 | ProxHTTPSProxyMII 2 | ================= 3 | 4 | Version 1.4 (20160112) 5 | -------------- 6 | 7 | + Socks proxy support (needs urllib3 >= 1.14) 8 | * Certifications are now v3 instead of v1 9 | 10 | Version 1.3.1 (20151001) 11 | -------------- 12 | 13 | * Certifications are now signed via SHA256 instead of SHA1 14 | 15 | Version 1.3 (20150114) 16 | -------------- 17 | 18 | + Each request has a number ranged from 001 to 999 for reference. 000 is reserved for SSL requests not MITMed like those in [BLACKLIST] and [SSL Pass-Thru] sections. 19 | + Log window now displays the length of the bytes submitted in POST method 20 | 21 | Version 1.2 (20141221) 22 | -------------- 23 | 24 | + Content is streamed to client, while not cached before sending 25 | * Fix config auto reload 26 | * Less exception traceback dumped 27 | * Tagged header changed from "Tagged:Proxomitron FrontProxy/*" to "Tagged:ProxHTTPSProxyMII FrontProxy/*" 28 | 29 | Version 1.1 (20141024) 30 | -------------- 31 | 32 | + Support URL bypass 33 | + Handle both HTTP and HTTPS 34 | + Auto reload config upon chagnes 35 | 36 | Version 1.0 (20140729) 37 | -------------- 38 | 39 | Initial release -------------------------------------------------------------------------------- /Docs/readme.html: -------------------------------------------------------------------------------- 1 | ProxHTTPSProxyMII 2 |

ProxHTTPSProxyMII

3 |

Created to provide modern nag-free HTTPS connections for an HTTP proxy.

4 |

How it works

5 |

how it works

6 |

Eligible HTTP Proxies

7 | 13 |

Install

14 | 17 |

Configure

18 | 24 |

Execute

25 |

ProxHTTPSProxy.exe to start.

26 |

Remember

27 |

Be aware and careful! Use a direct connection when you don't want any mistakes made.

28 |

Use at your own risk!

29 |

Have fun!

30 |

Discuss

31 |

http://prxbx.com/forums/showthread.php?tid=2172

32 |

Author

33 | 37 |

Proxomitron Tips

38 |

To use

39 | 50 |

Tips

51 | 77 |

For the current sidki set

78 |
    79 |
  1. Add the following two lines to Exceptions-U

    80 |
    $OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^)
    81 | ~(^$TST(keyword=i_proxy:[03].))$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^)
    82 | 
  2. 83 |
  3. Redirect connections to http resources with an expression like

    84 |

    $USEPROXY(false)$SET(keyword=i_proxy:0.)$RDIR(http://local.ptron/killed.gif)

    85 |
  4. 86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /Docs/readme.txt: -------------------------------------------------------------------------------- 1 | ProxHTTPSProxyMII 2 | ================= 3 | 4 | Created to provide modern nag-free HTTPS connections for an HTTP proxy. 5 | 6 | How it works 7 | ---- 8 | 9 | ![how it works](http://www.proxfilter.net/proxhttpsproxy/HowItWorks.gif) 10 | 11 | Eligible HTTP Proxies 12 | ---- 13 | 14 | * The [Proxomitron](http://www.proxomitron.info), for which ProxHTTPSProxy was created :) 15 | * Any that have the ability to forward all requests with a "Tagged:ProxHTTPSProxyMII FrontProxy/*" header to the ProxHTTPSProxyMII rear server. 16 | * Any that can be ran as two instances, one for true http and another for "tagged" http 17 | * Any that will only be used to monitor https traffic 18 | 19 | Install 20 | ---- 21 | 22 | * ProxHTTPSProxy's "CA.crt" to the Client's store of trusted certificate authorities. 23 | 24 | Configure 25 | ---- 26 | 27 | * The Client to use the ProxHTTPSProxy front server at 127.0.0.1 on port 8079 for secure connections. 28 | * The HTTP proxy to receive requests at 127.0.0.1 on port 8080. 29 | * The HTTP proxy to forward requests to the ProxHTTPSProxy rear server at 127.0.0.1 on port 8081. 30 | * Edit "Config.ini" to change these requirements. 31 | 32 | Execute 33 | ---- 34 | 35 | ProxHTTPSProxy.exe to start. 36 | 37 | Remember 38 | ---- 39 | 40 | Be aware and careful! Use a direct connection when you don't want any mistakes made. 41 | 42 | Use at your own risk! 43 | 44 | Have fun! 45 | 46 | Discuss 47 | ---- 48 | 49 | 50 | 51 | Author 52 | ---- 53 | 54 | * phoenix (aka whenever) 55 | * JJoe (test and doc) 56 | 57 | Proxomitron Tips 58 | ================ 59 | 60 | To use 61 | ---- 62 | 63 | * Add the ProxHTTPSProxy rear server to the Proxomitron's list of external proxies 64 | 65 | `127.0.0.1:8081 ProxHTTPSProxyMII` 66 | 67 | * Add to Proxomitron's "Bypass URLs that match this expression" field if it is empty 68 | 69 | `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)` 70 | 71 | * Add to the beginning of the entry in Proxomitron's "Bypass URLs that match this expression" field if it is **not** empty 72 | 73 | `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)|` 74 | 75 | Tips 76 | ---- 77 | 78 | * Proxomitron always executes some commands in "Bypass URLs that match this expression" field. Adding the entry there allows the Proxomitron to use the rear server when in Bypass mode. 79 | 80 | This undocumented feature brings many possibilities but remember, an actual match triggers bypass of filtering! 81 | 82 | - `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)` checks for the header that indicates an https request. 83 | - `$SETPROXY(127.0.0.1:8081)` is executed when found. 84 | - `(^)` expression never matches. 85 | 86 | * Identify https connections by testing for the "Tagged" request header that the ProxHTTPSProxy front server adds to the request. 87 | 88 | `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)` 89 | 90 | * For local file requests, use an expression like 91 | 92 | `$USEPROXY(false)$RDIR(http://local.ptron/killed.gif)` 93 | 94 | * Before redirecting "Tagged" connections to external resources consider removing the "Tagged" header. 95 | 96 | * If needed, the Proxomitron can still do https. After adding the ssl files to the Proxomitron, use a header filter like 97 | 98 | ``` 99 | [HTTP headers] 100 | In = FALSE 101 | Out = TRUE 102 | Key = "Tagged: Use Proxomitron for https://badcert.com" 103 | URL = "badcert.com$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$USEPROXY(false)$RDIR(https://badcert.com)" 104 | ``` 105 | This filter also removes the "Tagged" header. 106 | 107 | For the current sidki set 108 | ---- 109 | 110 | 1. Add the following two lines to Exceptions-U 111 | 112 | ``` 113 | $OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^) 114 | ~(^$TST(keyword=i_proxy:[03].))$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^) 115 | ``` 116 | 117 | 2. Redirect connections to http resources with an expression like 118 | 119 | `$USEPROXY(false)$SET(keyword=i_proxy:0.)$RDIR(http://local.ptron/killed.gif)` 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 wheever 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ProxHTTPSProxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | "A Proxomitron Helper Program" 5 | 6 | _name = 'ProxHTTPSProxyMII' 7 | __author__ = 'phoenix' 8 | __version__ = 'v1.4' 9 | 10 | CONFIG = "config.ini" 11 | CA_CERTS = "cacert.pem" 12 | 13 | import os 14 | import time 15 | import configparser 16 | import fnmatch 17 | import logging 18 | import threading 19 | import ssl 20 | import urllib3 21 | from urllib3.contrib.socks import SOCKSProxyManager 22 | #https://urllib3.readthedocs.org/en/latest/security.html#insecurerequestwarning 23 | urllib3.disable_warnings() 24 | 25 | from socketserver import ThreadingMixIn 26 | from http.server import HTTPServer, BaseHTTPRequestHandler 27 | from urllib.parse import urlparse 28 | from ProxyTool import ProxyRequestHandler, get_cert, counter 29 | 30 | from colorama import init, Fore, Back, Style 31 | init(autoreset=True) 32 | 33 | class LoadConfig: 34 | def __init__(self, configfile): 35 | self.config = configparser.ConfigParser(allow_no_value=True, 36 | inline_comment_prefixes=('#',)) 37 | self.config.read(configfile) 38 | self.PROXADDR = self.config['GENERAL'].get('ProxAddr') 39 | self.FRONTPORT = int(self.config['GENERAL'].get('FrontPort')) 40 | self.REARPORT = int(self.config['GENERAL'].get('RearPort')) 41 | self.DEFAULTPROXY = self.config['GENERAL'].get('DefaultProxy') 42 | self.LOGLEVEL = self.config['GENERAL'].get('LogLevel') 43 | 44 | class ConnectionPools: 45 | """ 46 | self.pools is a list of {'proxy': 'http://127.0.0.1:8080', 47 | 'pool': urllib3.ProxyManager() object, 48 | 'patterns': ['ab.com', 'bc.net', ...]} 49 | self.getpool() is a method that returns pool based on host matching 50 | """ 51 | # Windows default CA certificates are incomplete 52 | # See: http://bugs.python.org/issue20916 53 | # cacert.pem sources: 54 | # - http://curl.haxx.se/docs/caextract.html 55 | # - http://certifi.io/en/latest/ 56 | 57 | # ssl_version="TLSv1" to specific version 58 | sslparams = dict(cert_reqs="REQUIRED", ca_certs=CA_CERTS) 59 | # IE: http://support2.microsoft.com/kb/181050/en-us 60 | # Firefox about:config 61 | # network.http.connection-timeout 90 62 | # network.http.response.timeout 300 63 | timeout = urllib3.util.timeout.Timeout(connect=90.0, read=300.0) 64 | 65 | def __init__(self, config): 66 | self.file = config 67 | self.file_timestamp = os.path.getmtime(config) 68 | self.loadConfig() 69 | 70 | def loadConfig(self): 71 | # self.conf has to be inited each time for reloading 72 | self.conf = configparser.ConfigParser(allow_no_value=True, delimiters=('=',), 73 | inline_comment_prefixes=('#',)) 74 | self.conf.read(self.file) 75 | self.pools = [] 76 | proxy_sections = [section for section in self.conf.sections() 77 | if section.startswith('PROXY')] 78 | for section in proxy_sections: 79 | proxy = section.split()[1] 80 | self.pools.append(dict(proxy=proxy, 81 | pool=self.setProxyPool(proxy), 82 | patterns=list(self.conf[section].keys()))) 83 | default_proxy = self.conf['GENERAL'].get('DefaultProxy') 84 | default_pool = (self.setProxyPool(default_proxy) if default_proxy else 85 | [urllib3.PoolManager(num_pools=10, maxsize=8, timeout=self.timeout, **self.sslparams), 86 | urllib3.PoolManager(num_pools=10, maxsize=8, timeout=self.timeout)]) 87 | self.pools.append({'proxy': default_proxy, 'pool': default_pool, 'patterns': '*'}) 88 | 89 | self.noverifylist = list(self.conf['SSL No-Verify'].keys()) 90 | self.blacklist = list(self.conf['BLACKLIST'].keys()) 91 | self.sslpasslist = list(self.conf['SSL Pass-Thru'].keys()) 92 | self.bypasslist = list(self.conf['BYPASS URL'].keys()) 93 | 94 | def reloadConfig(self): 95 | while True: 96 | mtime = os.path.getmtime(self.file) 97 | if mtime > self.file_timestamp: 98 | self.file_timestamp = mtime 99 | self.loadConfig() 100 | logger.info(Fore.RED + Style.BRIGHT 101 | + "*" * 20 + " CONFIG RELOADED " + "*" * 20) 102 | time.sleep(1) 103 | 104 | def getpool(self, host, httpmode=False): 105 | noverify = True if httpmode or any((fnmatch.fnmatch(host, pattern) for pattern in self.noverifylist)) else False 106 | for pool in self.pools: 107 | if any((fnmatch.fnmatch(host, pattern) for pattern in pool['patterns'])): 108 | return pool['proxy'], pool['pool'][noverify], noverify 109 | 110 | def setProxyPool(self, proxy): 111 | scheme = proxy.split(':')[0] 112 | if scheme in ('http', 'https'): 113 | ProxyManager = urllib3.ProxyManager 114 | elif scheme in ('socks4', 'socks5'): 115 | ProxyManager = SOCKSProxyManager 116 | else: 117 | print("Wrong Proxy Format: " + proxy) 118 | print("Proxy should start with http/https/socks4/socks5 .") 119 | input() 120 | raise SystemExit 121 | # maxsize is the max. number of connections to the same server 122 | return [ProxyManager(proxy, num_pools=10, maxsize=8, timeout=self.timeout, **self.sslparams), 123 | ProxyManager(proxy, num_pools=10, maxsize=8, timeout=self.timeout)] 124 | 125 | class FrontServer(ThreadingMixIn, HTTPServer): 126 | """Handle requests in a separate thread.""" 127 | pass 128 | 129 | class RearServer(ThreadingMixIn, HTTPServer): 130 | """Handle requests in a separate thread.""" 131 | pass 132 | 133 | class FrontRequestHandler(ProxyRequestHandler): 134 | """ 135 | Sit between the client and Proxomitron 136 | Convert https request to http 137 | """ 138 | server_version = "%s FrontProxy/%s" % (_name, __version__) 139 | 140 | def do_CONNECT(self): 141 | "Descrypt https request and dispatch to http handler" 142 | 143 | # request line: CONNECT www.example.com:443 HTTP/1.1 144 | self.host, self.port = self.path.split(":") 145 | self.proxy, self.pool, self.noverify = pools.getpool(self.host) 146 | if any((fnmatch.fnmatch(self.host, pattern) for pattern in pools.blacklist)): 147 | # BLACK LIST 148 | self.deny_request() 149 | logger.info("%03d " % self.reqNum + Fore.CYAN + 'Denied by blacklist: %s' % self.host) 150 | elif any((fnmatch.fnmatch(self.host, pattern) for pattern in pools.sslpasslist)): 151 | # SSL Pass-Thru 152 | if self.proxy and self.proxy.startswith('https'): 153 | self.forward_to_https_proxy() 154 | elif self.proxy and self.proxy.startswith('socks5'): 155 | self.forward_to_socks5_proxy() 156 | else: 157 | self.tunnel_traffic() 158 | # Upstream server or proxy of the tunnel is closed explictly, so we close the local connection too 159 | self.close_connection = 1 160 | else: 161 | # SSL MITM 162 | self.wfile.write(("HTTP/1.1 200 Connection established\r\n" + 163 | "Proxy-agent: %s\r\n" % self.version_string() + 164 | "\r\n").encode('ascii')) 165 | commonname = '.' + self.host.partition('.')[-1] if self.host.count('.') >= 2 else self.host 166 | dummycert = get_cert(commonname) 167 | # set a flag for do_METHOD 168 | self.ssltunnel = True 169 | 170 | ssl_sock = ssl.wrap_socket(self.connection, keyfile=dummycert, certfile=dummycert, server_side=True) 171 | # Ref: Lib/socketserver.py#StreamRequestHandler.setup() 172 | self.connection = ssl_sock 173 | self.rfile = self.connection.makefile('rb', self.rbufsize) 174 | self.wfile = self.connection.makefile('wb', self.wbufsize) 175 | # dispatch to do_METHOD() 176 | self.handle_one_request() 177 | 178 | def do_METHOD(self): 179 | "Forward request to Proxomitron" 180 | 181 | counter.increment_and_set(self, 'reqNum') 182 | 183 | if self.ssltunnel: 184 | # https request 185 | host = self.host if self.port == '443' else "%s:%s" % (self.host, self.port) 186 | url = "https://%s%s" % (host, self.path) 187 | self.bypass = any((fnmatch.fnmatch(url, pattern) for pattern in pools.bypasslist)) 188 | if not self.bypass: 189 | url = "http://%s%s" % (host, self.path) 190 | # Tag the request so Proxomitron can recognize it 191 | self.headers["Tagged"] = self.version_string() + ":%d" % self.reqNum 192 | else: 193 | # http request 194 | self.host = urlparse(self.path).hostname 195 | if any((fnmatch.fnmatch(self.host, pattern) for pattern in pools.blacklist)): 196 | # BLACK LIST 197 | self.deny_request() 198 | logger.info("%03d " % self.reqNum + Fore.CYAN + 'Denied by blacklist: %s' % self.host) 199 | return 200 | host = urlparse(self.path).netloc 201 | self.proxy, self.pool, self.noverify = pools.getpool(self.host, httpmode=True) 202 | self.bypass = any((fnmatch.fnmatch('http://' + host + urlparse(self.path).path, pattern) for pattern in pools.bypasslist)) 203 | url = self.path 204 | self.url = url 205 | pool = self.pool if self.bypass else proxpool 206 | data_length = self.headers.get("Content-Length") 207 | self.postdata = self.rfile.read(int(data_length)) if data_length and int(data_length) > 0 else None 208 | if self.command == "POST" and "Content-Length" not in self.headers: 209 | buffer = self.rfile.read() 210 | if buffer: 211 | logger.warning("%03d " % self.reqNum + Fore.RED + 212 | 'POST w/o "Content-Length" header (Bytes: %d | Transfer-Encoding: %s | HTTPS: %s', 213 | len(buffer), "Transfer-Encoding" in self.headers, self.ssltunnel) 214 | # Remove hop-by-hop headers 215 | self.purge_headers(self.headers) 216 | r = None 217 | 218 | # Below code in connectionpool.py expect the headers to has a copy() and update() method 219 | # That's why we can't use self.headers directly when call pool.urlopen() 220 | # 221 | # Merge the proxy headers. Only do this in HTTP. We have to copy the 222 | # headers dict so we can safely change it without those changes being 223 | # reflected in anyone else's copy. 224 | # if self.scheme == 'http': 225 | # headers = headers.copy() 226 | # headers.update(self.proxy_headers) 227 | headers = urllib3._collections.HTTPHeaderDict(self.headers) 228 | 229 | try: 230 | # Sometimes 302 redirect would fail with "BadStatusLine" exception, and IE11 doesn't restart the request. 231 | # retries=1 instead of retries=False fixes it. 232 | #! Retry may cause the requests with the same reqNum appear in the log window 233 | r = pool.urlopen(self.command, url, body=self.postdata, headers=headers, 234 | retries=1, redirect=False, preload_content=False, decode_content=False) 235 | if not self.ssltunnel: 236 | if self.bypass: 237 | prefix = '[BP]' if self.proxy else '[BD]' 238 | else: 239 | prefix = '[D]' 240 | if self.command in ("GET", "HEAD"): 241 | logger.info("%03d " % self.reqNum + Fore.MAGENTA + '%s "%s %s" %s %s' % 242 | (prefix, self.command, url, r.status, r.getheader('Content-Length', '-'))) 243 | else: 244 | logger.info("%03d " % self.reqNum + Fore.MAGENTA + '%s "%s %s %s" %s %s' % 245 | (prefix, self.command, url, data_length, r.status, r.getheader('Content-Length', '-'))) 246 | 247 | self.send_response_only(r.status, r.reason) 248 | # HTTPResponse.msg is easier to handle than urllib3._collections.HTTPHeaderDict 249 | r.headers = r._original_response.msg 250 | self.purge_write_headers(r.headers) 251 | 252 | if self.command == 'HEAD' or r.status in (100, 101, 204, 304) or r.getheader("Content-Length") == '0': 253 | written = None 254 | else: 255 | written = self.stream_to_client(r) 256 | if "Content-Length" not in r.headers and 'Transfer-Encoding' not in r.headers: 257 | self.close_connection = 1 258 | 259 | # Intend to catch regular http and bypass http/https requests exceptions 260 | # Regular https request exceptions should be handled by rear server 261 | except urllib3.exceptions.TimeoutError as e: 262 | self.sendout_error(url, 504, message="Timeout", explain=e) 263 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + '[F] %s on "%s %s"', e, self.command, url) 264 | except (urllib3.exceptions.HTTPError,) as e: 265 | self.sendout_error(url, 502, message="HTTPError", explain=e) 266 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + '[F] %s on "%s %s"', e, self.command, url) 267 | finally: 268 | if r: 269 | # Release the connection back into the pool 270 | r.release_conn() 271 | 272 | do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_OPTIONS = do_METHOD 273 | 274 | class RearRequestHandler(ProxyRequestHandler): 275 | """ 276 | Supposed to be the parent proxy for Proxomitron for tagged requests 277 | Convert http request to https 278 | 279 | """ 280 | server_version = "%s RearProxy/%s" % (_name, __version__) 281 | 282 | def do_METHOD(self): 283 | "Convert http request to https" 284 | 285 | if self.headers.get("Tagged") and self.headers["Tagged"].startswith(_name): 286 | self.reqNum = int(self.headers["Tagged"].split(":")[1]) 287 | # Remove the tag 288 | del self.headers["Tagged"] 289 | else: 290 | self.sendout_error(self.path, 400, 291 | explain="The proxy setting of the client is misconfigured.\n\n" + 292 | "Please set the HTTPS proxy port to %s " % config.FRONTPORT + 293 | "and check the Docs for other settings.") 294 | logger.error(Fore.RED + Style.BRIGHT + "[Misconfigured HTTPS proxy port] " + self.path) 295 | return 296 | 297 | # request line: GET http://somehost.com/path?attr=value HTTP/1.1 298 | url = "https" + self.path[4:] 299 | self.host = urlparse(self.path).hostname 300 | proxy, pool, noverify = pools.getpool(self.host) 301 | prefix = '[P]' if proxy else '[D]' 302 | data_length = self.headers.get("Content-Length") 303 | self.postdata = self.rfile.read(int(data_length)) if data_length else None 304 | self.purge_headers(self.headers) 305 | r = None 306 | 307 | # Below code in connectionpool.py expect the headers to has a copy() and update() method 308 | # That's why we can't use self.headers directly when call pool.urlopen() 309 | # 310 | # Merge the proxy headers. Only do this in HTTP. We have to copy the 311 | # headers dict so we can safely change it without those changes being 312 | # reflected in anyone else's copy. 313 | # if self.scheme == 'http': 314 | # headers = headers.copy() 315 | # headers.update(self.proxy_headers) 316 | headers = urllib3._collections.HTTPHeaderDict(self.headers) 317 | 318 | try: 319 | r = pool.urlopen(self.command, url, body=self.postdata, headers=headers, 320 | retries=1, redirect=False, preload_content=False, decode_content=False) 321 | if proxy: 322 | logger.debug('Using Proxy - %s' % proxy) 323 | color = Fore.RED if noverify else Fore.GREEN 324 | if self.command in ("GET", "HEAD"): 325 | logger.info("%03d " % self.reqNum + color + '%s "%s %s" %s %s' % 326 | (prefix, self.command, url, r.status, r.getheader('Content-Length', '-'))) 327 | else: 328 | logger.info("%03d " % self.reqNum + color + '%s "%s %s %s" %s %s' % 329 | (prefix, self.command, url, data_length, r.status, r.getheader('Content-Length', '-'))) 330 | 331 | self.send_response_only(r.status, r.reason) 332 | # HTTPResponse.msg is easier to handle than urllib3._collections.HTTPHeaderDict 333 | r.headers = r._original_response.msg 334 | self.purge_write_headers(r.headers) 335 | 336 | if self.command == 'HEAD' or r.status in (100, 101, 204, 304) or r.getheader("Content-Length") == '0': 337 | written = None 338 | else: 339 | written = self.stream_to_client(r) 340 | if "Content-Length" not in r.headers and 'Transfer-Encoding' not in r.headers: 341 | self.close_connection = 1 342 | 343 | except urllib3.exceptions.SSLError as e: 344 | self.sendout_error(url, 417, message="SSL Certificate Failed", explain=e) 345 | logger.error("%03d " % self.reqNum + Fore.RED + Style.BRIGHT + "[SSL Certificate Error] " + url) 346 | except urllib3.exceptions.TimeoutError as e: 347 | self.sendout_error(url, 504, message="Timeout", explain=e) 348 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + '[R]%s "%s %s" %s', prefix, self.command, url, e) 349 | except (urllib3.exceptions.HTTPError,) as e: 350 | self.sendout_error(url, 502, message="HTTPError", explain=e) 351 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + '[R]%s "%s %s" %s', prefix, self.command, url, e) 352 | 353 | finally: 354 | if r: 355 | # Release the connection back into the pool 356 | r.release_conn() 357 | 358 | do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_OPTIONS = do_METHOD 359 | 360 | """ 361 | #Information# 362 | 363 | * Python default ciphers: http://bugs.python.org/issue20995 364 | * SSL Cipher Suite Details of Your Browser: https://cc.dcsec.uni-hannover.de/ 365 | * https://wiki.mozilla.org/Security/Server_Side_TLS 366 | """ 367 | 368 | try: 369 | if os.name == 'nt': 370 | import ctypes 371 | ctypes.windll.kernel32.SetConsoleTitleW('%s %s' % (_name, __version__)) 372 | 373 | config = LoadConfig(CONFIG) 374 | 375 | logger = logging.getLogger(__name__) 376 | logger.setLevel(getattr(logging, config.LOGLEVEL, logging.INFO)) 377 | handler = logging.StreamHandler() 378 | formatter = logging.Formatter('%(asctime)s %(message)s', datefmt='[%H:%M]') 379 | handler.setFormatter(formatter) 380 | logger.addHandler(handler) 381 | 382 | pools = ConnectionPools(CONFIG) 383 | proxpool = urllib3.ProxyManager(config.PROXADDR, num_pools=10, maxsize=8, 384 | # A little longer than timeout of rear pool 385 | # to avoid trigger front server exception handler 386 | timeout=urllib3.util.timeout.Timeout(connect=90.0, read=310.0)) 387 | 388 | frontserver = FrontServer(('', config.FRONTPORT), FrontRequestHandler) 389 | rearserver = RearServer(('', config.REARPORT), RearRequestHandler) 390 | for worker in (frontserver.serve_forever, rearserver.serve_forever, 391 | pools.reloadConfig): 392 | thread = threading.Thread(target=worker) 393 | thread.daemon = True 394 | thread.start() 395 | 396 | print("=" * 76) 397 | print('%s %s (urllib3/%s)' % (_name, __version__, urllib3.__version__)) 398 | print() 399 | print(' FrontServer : localhost:%s' % config.FRONTPORT) 400 | print(' RearServer : localhost:%s' % config.REARPORT) 401 | print(' ParentServer : %s' % config.DEFAULTPROXY) 402 | print(' Proxomitron : ' + config.PROXADDR) 403 | print("=" * 76) 404 | while True: 405 | time.sleep(1) 406 | except KeyboardInterrupt: 407 | print("Quitting...") 408 | -------------------------------------------------------------------------------- /ProxyTool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | "HTTP Proxy Tools, pyOpenSSL version" 5 | 6 | _name = "ProxyTool" 7 | __author__ = 'phoenix' 8 | __version__ = '1.0' 9 | 10 | import time 11 | from datetime import datetime 12 | import logging 13 | import threading 14 | import cgi 15 | import socket 16 | import select 17 | import ssl 18 | from http.server import HTTPServer, BaseHTTPRequestHandler 19 | from socketserver import ThreadingMixIn 20 | from CertTool import get_cert 21 | 22 | from colorama import init, Fore, Back, Style 23 | init(autoreset=True) 24 | 25 | logger = logging.getLogger('__main__') 26 | 27 | message_format = """\ 28 | 29 | 30 | 31 | 32 | Proxy Error: %(code)d 33 | 34 | 35 |

%(code)d: %(message)s

36 |

The following error occurred while trying to access %(url)s

37 |

%(explain)s

38 |
Generated on %(now)s by %(server)s. 39 | 40 | 41 | """ 42 | 43 | def read_write(socket1, socket2, max_idling=10): 44 | "Read and Write contents between 2 sockets" 45 | iw = [socket1, socket2] 46 | ow = [] 47 | count = 0 48 | while True: 49 | count += 1 50 | (ins, _, exs) = select.select(iw, ow, iw, 1) 51 | if exs: break 52 | if ins: 53 | for reader in ins: 54 | writer = socket2 if reader is socket1 else socket1 55 | try: 56 | data = reader.recv(1024) 57 | if data: 58 | writer.send(data) 59 | count = 0 60 | except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError): 61 | pass 62 | if count == max_idling: break 63 | 64 | class Counter: 65 | reset_value = 999 66 | def __init__(self, start=0): 67 | self.lock = threading.Lock() 68 | self.value = start 69 | def increment_and_set(self, obj, attr): 70 | with self.lock: 71 | self.value = self.value + 1 if self.value < self.reset_value else 1 72 | setattr(obj, attr, self.value) 73 | 74 | counter = Counter() 75 | 76 | class ProxyRequestHandler(BaseHTTPRequestHandler): 77 | """RequestHandler with do_CONNECT method defined 78 | """ 79 | server_version = "%s/%s" % (_name, __version__) 80 | # do_CONNECT() will set self.ssltunnel to override this 81 | ssltunnel = False 82 | # Override default value 'HTTP/1.0' 83 | protocol_version = 'HTTP/1.1' 84 | # To be set in each request 85 | reqNum = 0 86 | 87 | def do_CONNECT(self): 88 | "Descrypt https request and dispatch to http handler" 89 | # request line: CONNECT www.example.com:443 HTTP/1.1 90 | self.host, self.port = self.path.split(":") 91 | # SSL MITM 92 | self.wfile.write(("HTTP/1.1 200 Connection established\r\n" + 93 | "Proxy-agent: %s\r\n" % self.version_string() + 94 | "\r\n").encode('ascii')) 95 | commonname = '.' + self.host.partition('.')[-1] if self.host.count('.') >= 2 else self.host 96 | dummycert = get_cert(commonname) 97 | # set a flag for do_METHOD 98 | self.ssltunnel = True 99 | 100 | ssl_sock = ssl.wrap_socket(self.connection, keyfile=dummycert, certfile=dummycert, server_side=True) 101 | # Ref: Lib/socketserver.py#StreamRequestHandler.setup() 102 | self.connection = ssl_sock 103 | self.rfile = self.connection.makefile('rb', self.rbufsize) 104 | self.wfile = self.connection.makefile('wb', self.wbufsize) 105 | # dispatch to do_METHOD() 106 | self.handle_one_request() 107 | 108 | def handle_one_request(self): 109 | """Catch more exceptions than default 110 | 111 | Intend to catch exceptions on local side 112 | Exceptions on remote side should be handled in do_*() 113 | """ 114 | try: 115 | BaseHTTPRequestHandler.handle_one_request(self) 116 | return 117 | except (ConnectionError, FileNotFoundError) as e: 118 | logger.warning("%03d " % self.reqNum + Fore.RED + "%s %s", self.server_version, e) 119 | except (ssl.SSLEOFError, ssl.SSLError) as e: 120 | if hasattr(self, 'url'): 121 | # Happens after the tunnel is established 122 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + '"%s" while operating on established local SSL tunnel for [%s]' % (e, self.url)) 123 | else: 124 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + '"%s" while trying to establish local SSL tunnel for [%s]' % (e, self.path)) 125 | self.close_connection = 1 126 | 127 | def sendout_error(self, url, code, message=None, explain=None): 128 | "Modified from http.server.send_error() for customized display" 129 | try: 130 | shortmsg, longmsg = self.responses[code] 131 | except KeyError: 132 | shortmsg, longmsg = '???', '???' 133 | if message is None: 134 | message = shortmsg 135 | if explain is None: 136 | explain = longmsg 137 | content = (message_format % 138 | {'code': code, 'message': message, 'explain': explain, 139 | 'url': url, 'now': datetime.today(), 'server': self.server_version}) 140 | body = content.encode('UTF-8', 'replace') 141 | self.send_response_only(code, message) 142 | self.send_header("Content-Type", self.error_content_type) 143 | self.send_header('Content-Length', int(len(body))) 144 | self.end_headers() 145 | if self.command != 'HEAD' and code >= 200 and code not in (204, 304): 146 | self.wfile.write(body) 147 | 148 | def deny_request(self): 149 | self.send_response_only(403) 150 | self.send_header('Content-Length', 0) 151 | self.end_headers() 152 | 153 | def redirect(self, url): 154 | self.send_response_only(302) 155 | self.send_header('Content-Length', 0) 156 | self.send_header('Location', url) 157 | self.end_headers() 158 | 159 | def forward_to_https_proxy(self): 160 | "Forward https request to upstream https proxy" 161 | logger.debug('Using Proxy - %s' % self.proxy) 162 | proxy_host, proxy_port = self.proxy.split('//')[1].split(':') 163 | server_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 164 | try: 165 | server_conn.connect((proxy_host, int(proxy_port))) 166 | server_conn.send(('CONNECT %s HTTP/1.1\r\n\r\n' % self.path).encode('ascii')) 167 | server_conn.settimeout(0.1) 168 | datas = b'' 169 | while True: 170 | try: 171 | data = server_conn.recv(4096) 172 | except socket.timeout: 173 | break 174 | if data: 175 | datas += data 176 | else: 177 | break 178 | server_conn.setblocking(True) 179 | if b'200' in datas and b'established' in datas.lower(): 180 | logger.info("%03d " % self.reqNum + Fore.CYAN + '[P] SSL Pass-Thru: https://%s/' % self.path) 181 | self.wfile.write(("HTTP/1.1 200 Connection established\r\n" + 182 | "Proxy-agent: %s\r\n\r\n" % self.version_string()).encode('ascii')) 183 | read_write(self.connection, server_conn) 184 | else: 185 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + 'Proxy %s failed.', self.proxy) 186 | if datas: 187 | logger.debug(datas) 188 | self.wfile.write(datas) 189 | finally: 190 | # We don't maintain a connection reuse pool, so close the connection anyway 191 | server_conn.close() 192 | 193 | def forward_to_socks5_proxy(self): 194 | "Forward https request to upstream socks5 proxy" 195 | logger.warning(Fore.YELLOW + 'Socks5 proxy not implemented yet, please use https proxy') 196 | 197 | def tunnel_traffic(self): 198 | "Tunnel traffic to remote host:port" 199 | logger.info("%03d " % self.reqNum + Fore.CYAN + '[D] SSL Pass-Thru: https://%s/' % self.path) 200 | server_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 201 | try: 202 | server_conn.connect((self.host, int(self.port))) 203 | self.wfile.write(("HTTP/1.1 200 Connection established\r\n" + 204 | "Proxy-agent: %s\r\n" % self.version_string() + 205 | "\r\n").encode('ascii')) 206 | read_write(self.connection, server_conn) 207 | except TimeoutError: 208 | self.wfile.write(b"HTTP/1.1 504 Gateway Timeout\r\n\r\n") 209 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + 'Timed Out: https://%s:%s/' % (self.host, self.port)) 210 | except socket.gaierror as e: 211 | self.wfile.write(b"HTTP/1.1 503 Service Unavailable\r\n\r\n") 212 | logger.warning("%03d " % self.reqNum + Fore.YELLOW + '%s: https://%s:%s/' % (e, self.host, self.port)) 213 | finally: 214 | # We don't maintain a connection reuse pool, so close the connection anyway 215 | server_conn.close() 216 | 217 | def ssl_get_response(self, conn): 218 | try: 219 | server_conn = ssl.wrap_socket(conn, cert_reqs=ssl.CERT_REQUIRED, ca_certs="cacert.pem", ssl_version=ssl.PROTOCOL_TLSv1) 220 | server_conn.sendall(('%s %s HTTP/1.1\r\n' % (self.command, self.path)).encode('ascii')) 221 | server_conn.sendall(self.headers.as_bytes()) 222 | if self.postdata: 223 | server_conn.sendall(self.postdata) 224 | while True: 225 | data = server_conn.recv(4096) 226 | if data: 227 | self.wfile.write(data) 228 | else: break 229 | except (ssl.SSLEOFError, ssl.SSLError) as e: 230 | logger.error(Fore.RED + Style.BRIGHT + "[SSLError]") 231 | self.send_error(417, message="Exception %s" % str(e.__class__), explain=str(e)) 232 | 233 | def purge_headers(self, headers): 234 | "Remove hop-by-hop headers that shouldn't pass through a Proxy" 235 | for name in ["Connection", "Keep-Alive", "Upgrade", 236 | "Proxy-Connection", "Proxy-Authenticate"]: 237 | del headers[name] 238 | 239 | def purge_write_headers(self, headers): 240 | self.purge_headers(headers) 241 | for key, value in headers.items(): 242 | self.send_header(key, value) 243 | self.end_headers() 244 | 245 | def stream_to_client(self, response): 246 | bufsize = 1024 * 64 247 | need_chunked = 'Transfer-Encoding' in response.headers 248 | written = 0 249 | while True: 250 | data = response.read(bufsize) 251 | if not data: 252 | if need_chunked: 253 | self.wfile.write(b'0\r\n\r\n') 254 | break 255 | if need_chunked: 256 | self.wfile.write(('%x\r\n' % len(data)).encode('ascii')) 257 | self.wfile.write(data) 258 | if need_chunked: 259 | self.wfile.write(b'\r\n') 260 | written += len(data) 261 | return written 262 | 263 | def http_request_info(self): 264 | """Return HTTP request information in bytes 265 | """ 266 | context = ["CLIENT VALUES:", 267 | "client_address = %s" % str(self.client_address), 268 | "requestline = %s" % self.requestline, 269 | "command = %s" % self.command, 270 | "path = %s" % self.path, 271 | "request_version = %s" % self.request_version, 272 | "", 273 | "SERVER VALUES:", 274 | "server_version = %s" % self.server_version, 275 | "sys_version = %s" % self.sys_version, 276 | "protocol_version = %s" % self.protocol_version, 277 | "", 278 | "HEADER RECEIVED:"] 279 | for name, value in sorted(self.headers.items()): 280 | context.append("%s = %s" % (name, value.rstrip())) 281 | 282 | if self.command == "POST": 283 | context.append("\r\nPOST VALUES:") 284 | form = cgi.FieldStorage(fp=self.rfile, 285 | headers=self.headers, 286 | environ={'REQUEST_METHOD': 'POST'}) 287 | for field in form.keys(): 288 | fielditem = form[field] 289 | if fielditem.filename: 290 | # The field contains an uploaded file 291 | file_data = fielditem.file.read() 292 | file_len = len(file_data) 293 | context.append('Uploaded %s as "%s" (%d bytes)' 294 | % (field, fielditem.filename, file_len)) 295 | else: 296 | # Regular form value 297 | context.append("%s = %s" % (field, fielditem.value)) 298 | 299 | return("\r\n".join(context).encode('ascii')) 300 | 301 | def demo(): 302 | PORT = 8000 303 | 304 | class ProxyServer(ThreadingMixIn, HTTPServer): 305 | """Handle requests in a separate thread.""" 306 | pass 307 | 308 | class RequestHandler(ProxyRequestHandler): 309 | "Displaying HTTP request information" 310 | server_version = "DemoProxy/0.1" 311 | 312 | def do_METHOD(self): 313 | "Universal method for GET, POST, HEAD, PUT and DELETE" 314 | message = self.http_request_info() 315 | self.send_response(200) 316 | # 'Content-Length' is important for HTTP/1.1 317 | self.send_header('Content-Length', len(message)) 318 | self.end_headers() 319 | self.wfile.write(message) 320 | 321 | do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_OPTIONS = do_METHOD 322 | 323 | print('%s serving now, to stop ...' % RequestHandler.server_version) 324 | print('Listen Addr : localhost:%s' % PORT) 325 | print("-" * 10) 326 | server = ProxyServer(('', PORT), RequestHandler) 327 | server.serve_forever() 328 | 329 | if __name__ == '__main__': 330 | try: 331 | demo() 332 | except KeyboardInterrupt: 333 | print("Quitting...") 334 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ProxHTTPSProxyMII 2 | ================= 3 | 4 | A Proxomitron SSL Helper Program, can also be used to make a HTTP proxy like Privoxy capable of filtering HTTPS. 5 | 6 | How it works 7 | ---- 8 | 9 | ![how it works](http://www.proxfilter.net/proxhttpsproxy/HowItWorks.gif) 10 | 11 | Eligible HTTP Proxies 12 | ---- 13 | 14 | * The [Proxomitron](http://www.proxomitron.info), for which ProxHTTPSProxy was created :) 15 | * Any that have the ability to forward all requests with a "Tagged:ProxHTTPSProxyMII FrontProxy/*" header to the ProxHTTPSProxyMII rear server. 16 | * Any that can be ran as two instances, one for true http and another for "tagged" http 17 | * Any that will only be used to monitor https traffic 18 | 19 | Install 20 | ---- 21 | 22 | * ProxHTTPSProxy's "CA.crt" to the Client's store of trusted certificate authorities. 23 | 24 | Configure 25 | ---- 26 | 27 | * The Client to use the ProxHTTPSProxy front server at 127.0.0.1 on port 8079 for secure connections. 28 | * The HTTP proxy to receive requests at 127.0.0.1 on port 8080. 29 | * The HTTP proxy to forward requests to the ProxHTTPSProxy rear server at 127.0.0.1 on port 8081. 30 | * Edit "Config.ini" to change these requirements. 31 | 32 | Execute 33 | ---- 34 | 35 | ProxHTTPSProxy.exe to start. 36 | 37 | Remember 38 | ---- 39 | 40 | Be aware and careful! Use a direct connection when you don't want any mistakes made. 41 | 42 | Use at your own risk! 43 | 44 | Have fun! 45 | 46 | Discuss 47 | ---- 48 | 49 | 50 | 51 | Author 52 | ---- 53 | 54 | * phoenix (aka whenever) 55 | * JJoe (test and doc) 56 | 57 | Proxomitron Tips 58 | ================ 59 | 60 | To use 61 | ---- 62 | 63 | * Add the ProxHTTPSProxy rear server to the Proxomitron's list of external proxies 64 | 65 | `127.0.0.1:8081 ProxHTTPSProxyMII` 66 | 67 | * Add to Proxomitron's "Bypass URLs that match this expression" field if it is empty 68 | 69 | `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)` 70 | 71 | * Add to the beginning of the entry in Proxomitron's "Bypass URLs that match this expression" field if it is **not** empty 72 | 73 | `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)|` 74 | 75 | Tips 76 | ---- 77 | 78 | * Proxomitron always executes some commands in "Bypass URLs that match this expression" field. Adding the entry there allows the Proxomitron to use the rear server when in Bypass mode. 79 | 80 | This undocumented feature brings many possibilities but remember, an actual match triggers bypass of filtering! 81 | 82 | - `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)` checks for the header that indicates an https request. 83 | - `$SETPROXY(127.0.0.1:8081)` is executed when found. 84 | - `(^)` expression never matches. 85 | 86 | * Identify https connections by testing for the "Tagged" request header that the ProxHTTPSProxy front server adds to the request. 87 | 88 | `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)` 89 | 90 | * For local file requests, use an expression like 91 | 92 | `$USEPROXY(false)$RDIR(http://local.ptron/killed.gif)` 93 | 94 | * Before redirecting "Tagged" connections to external resources consider removing the "Tagged" header. 95 | 96 | * If needed, the Proxomitron can still do https. After adding the ssl files to the Proxomitron, use a header filter like 97 | 98 | ``` 99 | [HTTP headers] 100 | In = FALSE 101 | Out = TRUE 102 | Key = "Tagged: Use Proxomitron for https://badcert.com" 103 | URL = "badcert.com$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$USEPROXY(false)$RDIR(https://badcert.com)" 104 | ``` 105 | This filter also removes the "Tagged" header. 106 | 107 | For the current sidki set 108 | ---- 109 | 110 | 1. Add the following two lines to Exceptions-U 111 | 112 | ``` 113 | $OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^) 114 | ~(^$TST(keyword=i_proxy:[03].))$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^) 115 | ``` 116 | 117 | 2. Redirect connections to http resources with an expression like 118 | 119 | `$USEPROXY(false)$SET(keyword=i_proxy:0.)$RDIR(http://local.ptron/killed.gif)` 120 | 121 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | ### The parent proxy has to support CONNECT method, if you want to proxy HTTPS requests 2 | ### 3 | ### Proxy setting applies to HTTPS requests only, as it is applied by the Rear Server 4 | ### HTTP requests are passed to and handled by Proxomitron, please set up Proxomitron for proxy 5 | 6 | [GENERAL] 7 | ProxAddr = http://localhost:8080 8 | FrontPort = 8079 9 | RearPort = 8081 10 | # DefaultProxy = http://127.0.0.1:8118 11 | 12 | # Proper values for LogLevel are ERROR, WARNING, INFO, DEBUG 13 | # Default is INFO if unset 14 | LogLevel = 15 | 16 | # * matches everything 17 | # ? matches any single character 18 | # [seq] matches any character in seq 19 | # [!seq] matches any character not in seq 20 | 21 | [PROXY http://192.168.178.1:8123] 22 | #duckduckgo.com 23 | #*.s3.amazonaws.com 24 | 25 | [PROXY socks5://192.168.178.3:1080] 26 | test.com 27 | 28 | ### Ignore SSL certificate verify, Use at your own risk!!! 29 | ### Proxy setting still effective 30 | [SSL No-Verify] 31 | *.12306.cn 32 | 33 | [BLACKLIST] 34 | *.doubleclick.net 35 | *.google-analytics.com 36 | 37 | ### Bypass Proxomitron and the Rear Server, Proxy setting still effective 38 | ### SSL certificate verify will be done by the browser 39 | [SSL Pass-Thru] 40 | pypi.python.org 41 | www.gstatic.com 42 | watson.telemetry.microsoft.com 43 | *.sync.services.mozilla.com 44 | *.mega.co.nz 45 | 46 | # Microsoft SmartScreen Filter Service 47 | *.smartscreen.microsoft.com 48 | urs.microsoft.com 49 | 50 | # NoScript uses https://secure.informaction.com/ipecho to detect the WAN IP 51 | # https://addons.mozilla.org/en-US/firefox/addon/noscript/privacy/ 52 | secure.informaction.com 53 | 54 | ### Bypass Proxomitron and the Rear Server, Proxy setting still effective 55 | ### This section supports URL matching 56 | [BYPASS URL] 57 | http://www.abc.com/* 58 | https://bcd.net/* 59 | *://feedly.com/* 60 | *.zip 61 | *.rar 62 | *.exe 63 | *.pdf --------------------------------------------------------------------------------