├── 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 | 
6 | Eligible HTTP Proxies
7 |
8 | - The Proxomitron, for which ProxHTTPSProxy was created :)
9 | - Any that have the ability to forward all requests with a "Tagged:ProxHTTPSProxyMII FrontProxy/*" header to the ProxHTTPSProxyMII rear server.
10 | - Any that can be ran as two instances, one for true http and another for "tagged" http
11 | - Any that will only be used to monitor https traffic
12 |
13 | Install
14 |
15 | - ProxHTTPSProxy's "CA.crt" to the Client's store of trusted certificate authorities.
16 |
17 |
18 |
19 | - The Client to use the ProxHTTPSProxy front server at 127.0.0.1 on port 8079 for secure connections.
20 | - The HTTP proxy to receive requests at 127.0.0.1 on port 8080.
21 | - The HTTP proxy to forward requests to the ProxHTTPSProxy rear server at 127.0.0.1 on port 8081.
22 | - Edit "Config.ini" to change these requirements.
23 |
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 |
34 | - phoenix (aka whenever)
35 | - JJoe (test and doc)
36 |
37 | Proxomitron Tips
38 | To use
39 |
40 | Add the ProxHTTPSProxy rear server to the Proxomitron's list of external proxies
41 | 127.0.0.1:8081 ProxHTTPSProxy
42 |
43 | Add to Proxomitron's "Bypass URLs that match this expression" field if it is empty
44 | $OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)
45 |
46 | Add to the beginning of the entry in Proxomitron's "Bypass URLs that match this expression" field if it is not empty
47 | $OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)|
48 |
49 |
50 | Tips
51 |
52 | 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.
53 | This undocumented feature brings many possibilities but remember, an actual match triggers bypass of filtering!
54 |
55 | $OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)
checks for the header that indicates an https request.
56 | $SETPROXY(127.0.0.1:8081)
is executed when found.
57 | (^)
expression never matches.
58 |
59 |
60 | Identify https connections by testing for the "Tagged" request header that the ProxHTTPSProxy front server adds to the request.
61 | $OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)
62 |
63 | For local file requests, use an expression like
64 | $USEPROXY(false)$RDIR(http://local.ptron/killed.gif)
65 |
66 | Before redirecting "Tagged" connections to external resources consider removing the "Tagged" header.
67 |
68 | If needed, the Proxomitron can still do https. After adding the ssl files to the Proxomitron, use a header filter like
69 | [HTTP headers]
70 | In = FALSE
71 | Out = TRUE
72 | Key = "Tagged: Use Proxomitron for https://badcert.com"
73 | URL = "badcert.com$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$USEPROXY(false)$RDIR(https://badcert.com)"
74 |
This filter also removes the "Tagged" header.
75 |
76 |
77 | For the current sidki set
78 |
79 | 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 |
83 | Redirect connections to http resources with an expression like
84 | $USEPROXY(false)$SET(keyword=i_proxy:0.)$RDIR(http://local.ptron/killed.gif)
85 |
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 | 
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 | 
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
--------------------------------------------------------------------------------