big refactor for privoxy
This commit is contained in:
parent
da06c097fb
commit
ef69565f31
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__
|
52
CA.crt
52
CA.crt
@ -1,52 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+DCCAuCgAwIBAgIBADANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEX
|
||||
MBUGA1UECgwOUHJveEhUVFBTUHJveHkxEjAQBgNVBAsMCXB5T3BlblNTTDEaMBgG
|
||||
A1UEAwwRUHJveEhUVFBTUHJveHkgQ0EwHhcNMTUxMDAxMDc0MDI1WhcNMjUwOTMw
|
||||
MDc0MDI1WjBWMQswCQYDVQQGEwJDTjEXMBUGA1UECgwOUHJveEhUVFBTUHJveHkx
|
||||
EjAQBgNVBAsMCXB5T3BlblNTTDEaMBgGA1UEAwwRUHJveEhUVFBTUHJveHkgQ0Ew
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9mQTpJlPDjEqLnovcT/AL
|
||||
YwjoP2Siowor2yeEKaGKJjBamu3OkYhS+2kzJhcii705uTCal/f6gDIlnhYXlPEh
|
||||
L7Z0wsT9IePJSU9+yNtUrWYILfRg1+XkpZVqrPfjBk8usTjtC4kG9xRZno/TeZj/
|
||||
2Qror/C989Hl+bqZ4p31/l1Jcml/W01PDiGcqESS15bKk24azJ1w69Zhjwn8uZKc
|
||||
Mnq2myrJsl8fZ82gV2fV8yydhpDudPpHy8y/9U8FfsmODi75aH4A1NkK/2FZyBKE
|
||||
1OEYd+JfL7QmBCCjIt9AREXA/77HSuj6OXoKWZ0AVuiHLA/psfcRL4+QXd1UtXbF
|
||||
AgMBAAGjgdAwgc0wDwYDVR0TAQH/BAUwAwEB/zARBglghkgBhvhCAQEEBAMCAgQw
|
||||
ewYDVR0lAQH/BHEwbwYIKwYBBQUHAwEGCCsGAQUFBwMCBggrBgEFBQcDBAYIKwYB
|
||||
BQUHAwgGCisGAQQBgjcCARUGCisGAQQBgjcCARYGCisGAQQBgjcKAwEGCisGAQQB
|
||||
gjcKAwMGCisGAQQBgjcKAwQGCWCGSAGG+EIEATALBgNVHQ8EBAMCAQYwHQYDVR0O
|
||||
BBYEFKPBao+B+YH0tMNHNGoLv/3ncZyvMA0GCSqGSIb3DQEBCwUAA4IBAQCFZOPd
|
||||
SldrKkekP/tO/WnGgXEus8z4Ei7TEAm6qkSJ/r0ZaTKmGek370xvVG4myl0Hngr+
|
||||
F6blIUzGi8e9mp/2vONhPYKTAg+Y4h5tKz9S6SyvbypBMa4YNZw8DNfd4uVLL/b6
|
||||
psQcYfMPMpRdM7GlLZbxY9AHyCaHZszc3bSBM/lIhLWJH0pR7QSZZ+cJUHYKODZ8
|
||||
Cs8goAcA/mJ4h1g63EP1Snlw4U3vMJ8ZQRAeg46FAZATwte9SaahAq1kLql/P8jg
|
||||
A4gM9xvfRgVOIrfxSHDlnw6gVK6u/WhD4SWIsS2JfNljgUmrcMWB37kNdT3i0yO7
|
||||
Vydw/UIJw1pqktqz
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9mQTpJlPDjEqL
|
||||
novcT/ALYwjoP2Siowor2yeEKaGKJjBamu3OkYhS+2kzJhcii705uTCal/f6gDIl
|
||||
nhYXlPEhL7Z0wsT9IePJSU9+yNtUrWYILfRg1+XkpZVqrPfjBk8usTjtC4kG9xRZ
|
||||
no/TeZj/2Qror/C989Hl+bqZ4p31/l1Jcml/W01PDiGcqESS15bKk24azJ1w69Zh
|
||||
jwn8uZKcMnq2myrJsl8fZ82gV2fV8yydhpDudPpHy8y/9U8FfsmODi75aH4A1NkK
|
||||
/2FZyBKE1OEYd+JfL7QmBCCjIt9AREXA/77HSuj6OXoKWZ0AVuiHLA/psfcRL4+Q
|
||||
Xd1UtXbFAgMBAAECggEAK5AHEtLdmCaZ0i6hkANF8jfVChfWtY+kfKMkFzfBiA5y
|
||||
Ob8zOK0zl21wpHHyCtv0pFiqlDqqnYHrA72o8c4lAS0HTRibTlYFEnCntUfNLU2S
|
||||
DfsRFVdF2R06kYIgiqcedmn93Gk0GMeYg2btQPfFcbOa0A/szphA+AhDGax6AtUD
|
||||
gl7+QT4j5HE598ghtl5/DZ4tiw4cfuWjC6ph7tHbKKq9wCH6wQf9kcyIA4ozVBKV
|
||||
fejE9t4BfVPxzbxN+Quu0+S5SGnKzg1uY+/99Jo1IqtJGQq1OlPFLjVnxUF1N+Wp
|
||||
nJVBHorILQtGhYxW4QlWsHMdc7iB5r4eFSuKaivMGQKBgQDrCDviK35IuQylxKE8
|
||||
Xu/eSwPpOwF4ibASgaPmJt+t4O4JLX1GLZX899KDIeXaAFqcp2EF4QUhX2ioGIiO
|
||||
GGFFAmOHIDvCFfiNpM1m7F0Njj8gedFfT4Yhv73htUlh5zA8vfuv4PN4ZGfjK3L9
|
||||
sW9OEMUDTey5D/6Wq/IZ8ZGTwwKBgQDOgyJSJQk8K0n4AGLPyP/wmXL4w/xi8IOC
|
||||
kafs1XsQCn5OvKJZY5ZNyoSzhkKmlUQTO/tmZ5flOk6wVs34StSNSo+JQub5vEYi
|
||||
gXVNwYB6oPYMtdfPYLSy59h0REugNfkunRj5crPyVttJiVZpxBJHxgnIqJcBj+WT
|
||||
ehHNJpRK1wKBgFx4s97rj9ca/4/lCi8Phz6lsxc7gPuk6KKPYSX3W4A1BFKWFDjd
|
||||
TKrn8mpnluCrzPrfm/vNKdCUkj+4z1lg3DxjkTckBn75V/6avbnl+0KPGeU0KJ1g
|
||||
U3zJzPKV+hZL+J2dff4X+pL+piUp/ic0fX9wd6MyMJYrZdZwNmPguI8zAoGAARJF
|
||||
F1AB4EIJPDQkTxen3EOviQLbSFgfFopS6LOi0856IUZxQS13Fig60AOeTObxV3g0
|
||||
Ma/P5eyLg/avUt5wg9sjK38hW6JSatNpHGIonHpBTIeU+wpxZYw2X0QLcGVXSZqf
|
||||
CoxByrwQny0LObk+rwij/FqDjgqFEmLLvNi6ZDkCgYEA3xgeLNBGf5ghYhgX9PKO
|
||||
Y1Rg6y1ElqxMCoovkpNlA6bVkyxcYIItIW1npsSeM45x+6Blit74LuleE9UYoN8j
|
||||
BC8ADhYN7ywb0juCnpLrKuWl/3XNg3wREhvhHfEK1agEysVFUohFwdtfyW4gNWia
|
||||
wli1LGvTwY1aFj8K29VKvkE=
|
||||
-----END PRIVATE KEY-----
|
119
CertTool.py
119
CertTool.py
@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"Cert Tools, pyOpenSSL version"
|
||||
|
||||
__author__ = 'phoenix'
|
||||
__version__ = '0.1'
|
||||
|
||||
CA = "CA.crt"
|
||||
CERTDIR = "Certs"
|
||||
# Temp list for generating certs
|
||||
workingList = set()
|
||||
|
||||
import os
|
||||
import time
|
||||
import OpenSSL
|
||||
|
||||
def create_CA(capath):
|
||||
key = OpenSSL.crypto.PKey()
|
||||
key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
|
||||
ca = OpenSSL.crypto.X509()
|
||||
ca.set_serial_number(0)
|
||||
# Value 2 means v3
|
||||
ca.set_version(2)
|
||||
subj = ca.get_subject()
|
||||
subj.countryName = 'CN'
|
||||
subj.organizationName = 'ProxHTTPSProxy'
|
||||
subj.organizationalUnitName = 'pyOpenSSL'
|
||||
subj.commonName = 'ProxHTTPSProxy CA'
|
||||
ca.gmtime_adj_notBefore(0)
|
||||
ca.gmtime_adj_notAfter(24 * 60 * 60 * 3652)
|
||||
ca.set_issuer(ca.get_subject())
|
||||
ca.set_pubkey(key)
|
||||
ca.add_extensions(
|
||||
[OpenSSL.crypto.X509Extension(b"basicConstraints", True, b"CA:TRUE"),
|
||||
# mozilla::pkix doesn't handle the Netscape Cert Type extension (which is problematic when it's marked critical)
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=1009161
|
||||
OpenSSL.crypto.X509Extension(b"nsCertType", False, b"sslCA"),
|
||||
OpenSSL.crypto.X509Extension(b"extendedKeyUsage", True, b"serverAuth,clientAuth,emailProtection,timeStamping,msCodeInd,msCodeCom,msCTLSign,msSGC,msEFS,nsSGC"),
|
||||
OpenSSL.crypto.X509Extension(b"keyUsage", False, b"keyCertSign, cRLSign"),
|
||||
OpenSSL.crypto.X509Extension(b"subjectKeyIdentifier", False, b"hash", subject=ca)])
|
||||
ca.sign(key, 'sha256')
|
||||
with open(capath, 'wb') as fp:
|
||||
fp.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ca))
|
||||
fp.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key))
|
||||
|
||||
def get_cert(name, cafile=CA, certdir=CERTDIR):
|
||||
"""Return cert file path. Create it if it doesn't exist.
|
||||
|
||||
cafile: the CA file to create dummpy cert files
|
||||
certdir: the path where cert files are looked for or created
|
||||
"""
|
||||
certfile = os.path.join(certdir, name + '.crt')
|
||||
if not os.path.exists(certfile):
|
||||
dummy_cert(cafile, certfile, name)
|
||||
return certfile
|
||||
|
||||
def dummy_cert(cafile, certfile, commonname):
|
||||
"""Generates and writes a certificate to certfile
|
||||
commonname: Common name for the generated certificate
|
||||
Ref: https://github.com/mitmproxy/netlib/blob/master/netlib/certutils.py
|
||||
"""
|
||||
if certfile in workingList:
|
||||
# Another thread is working on it, wait until it finish
|
||||
while True:
|
||||
time.sleep(0.2)
|
||||
if certfile not in workingList: break
|
||||
else:
|
||||
workingList.add(certfile)
|
||||
with open(cafile, "rb") as file:
|
||||
content = file.read()
|
||||
ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, content)
|
||||
key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, content)
|
||||
cert = OpenSSL.crypto.X509()
|
||||
# Value 2 means v3
|
||||
cert.set_version(2)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(60 * 60 * 24 * 3652)
|
||||
cert.set_issuer(ca.get_subject())
|
||||
if commonname.startswith('.'):
|
||||
domain = '*' + commonname
|
||||
else:
|
||||
domain = commonname
|
||||
cert.get_subject().CN = domain
|
||||
cert.set_serial_number(int(time.time()*10000))
|
||||
cert.set_pubkey(ca.get_pubkey())
|
||||
cert.add_extensions(
|
||||
[OpenSSL.crypto.X509Extension(b"subjectAltName", False, str.encode("DNS:"+domain))])
|
||||
cert.sign(key, "sha256")
|
||||
with open(certfile, 'wb') as fp:
|
||||
fp.write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
|
||||
fp.write(OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key))
|
||||
workingList.remove(certfile)
|
||||
|
||||
def startup_check():
|
||||
if not os.path.exists(CERTDIR):
|
||||
os.mkdir(CERTDIR)
|
||||
if not os.path.exists(CERTDIR):
|
||||
print("%s directory does not exist!")
|
||||
print("Please create it and restart the program!")
|
||||
input()
|
||||
raise SystemExit
|
||||
|
||||
if not os.path.exists(CA):
|
||||
print("Creating CA ...")
|
||||
create_CA(CA)
|
||||
if not os.path.exists(CA):
|
||||
print("Failed to create CA :(")
|
||||
else:
|
||||
print("* Please import created %s to your client's store of trusted certificate authorities." % CA)
|
||||
print("* Please delete all files under %s directory!" % CERTDIR)
|
||||
print("* Then restart the program!")
|
||||
input()
|
||||
raise SystemExit
|
||||
|
||||
startup_check()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("All Good!")
|
Binary file not shown.
Before Width: | Height: | Size: 29 KiB |
@ -1,39 +0,0 @@
|
||||
ProxHTTPSProxyMII
|
||||
=================
|
||||
|
||||
Version 1.4 (20160112)
|
||||
--------------
|
||||
|
||||
+ Socks proxy support (needs urllib3 >= 1.14)
|
||||
* Certifications are now v3 instead of v1
|
||||
|
||||
Version 1.3.1 (20151001)
|
||||
--------------
|
||||
|
||||
* Certifications are now signed via SHA256 instead of SHA1
|
||||
|
||||
Version 1.3 (20150114)
|
||||
--------------
|
||||
|
||||
+ 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.
|
||||
+ Log window now displays the length of the bytes submitted in POST method
|
||||
|
||||
Version 1.2 (20141221)
|
||||
--------------
|
||||
|
||||
+ Content is streamed to client, while not cached before sending
|
||||
* Fix config auto reload
|
||||
* Less exception traceback dumped
|
||||
* Tagged header changed from "Tagged:Proxomitron FrontProxy/*" to "Tagged:ProxHTTPSProxyMII FrontProxy/*"
|
||||
|
||||
Version 1.1 (20141024)
|
||||
--------------
|
||||
|
||||
+ Support URL bypass
|
||||
+ Handle both HTTP and HTTPS
|
||||
+ Auto reload config upon chagnes
|
||||
|
||||
Version 1.0 (20140729)
|
||||
--------------
|
||||
|
||||
Initial release
|
@ -1,88 +0,0 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>ProxHTTPSProxyMII</title><style></style></head><body>
|
||||
<h1 id="proxhttpsproxymii">ProxHTTPSProxyMII</h1>
|
||||
<p>Created to provide modern nag-free HTTPS connections for an HTTP proxy.</p>
|
||||
<h2 id="how-it-works">How it works</h2>
|
||||
<p><img src="http://www.proxfilter.net/proxhttpsproxy/HowItWorks.gif" alt="how it works"></p>
|
||||
<h2 id="eligible-http-proxies">Eligible HTTP Proxies</h2>
|
||||
<ul>
|
||||
<li>The <a href="http://www.proxomitron.info">Proxomitron</a>, for which ProxHTTPSProxy was created :)</li>
|
||||
<li>Any that have the ability to forward all requests with a "Tagged:ProxHTTPSProxyMII FrontProxy/*" header to the ProxHTTPSProxyMII rear server.</li>
|
||||
<li>Any that can be ran as two instances, one for true http and another for "tagged" http</li>
|
||||
<li>Any that will only be used to monitor https traffic </li>
|
||||
</ul>
|
||||
<h2 id="install">Install</h2>
|
||||
<ul>
|
||||
<li>ProxHTTPSProxy's "CA.crt" to the Client's store of trusted certificate authorities.</li>
|
||||
</ul>
|
||||
<h2 id="configure">Configure</h2>
|
||||
<ul>
|
||||
<li>The Client to use the ProxHTTPSProxy front server at 127.0.0.1 on port 8079 for secure connections.</li>
|
||||
<li>The HTTP proxy to receive requests at 127.0.0.1 on port 8080.</li>
|
||||
<li>The HTTP proxy to forward requests to the ProxHTTPSProxy rear server at 127.0.0.1 on port 8081.</li>
|
||||
<li>Edit "Config.ini" to change these requirements.</li>
|
||||
</ul>
|
||||
<h2 id="execute">Execute</h2>
|
||||
<p>ProxHTTPSProxy.exe to start.</p>
|
||||
<h2 id="remember">Remember</h2>
|
||||
<p>Be aware and careful! Use a direct connection when you don't want any mistakes made.</p>
|
||||
<p>Use at your own risk!</p>
|
||||
<p>Have fun!</p>
|
||||
<h2 id="discuss">Discuss</h2>
|
||||
<p><a href="http://prxbx.com/forums/showthread.php?tid=2172">http://prxbx.com/forums/showthread.php?tid=2172</a></p>
|
||||
<h2 id="author">Author</h2>
|
||||
<ul>
|
||||
<li>phoenix (aka whenever)</li>
|
||||
<li>JJoe (test and doc)</li>
|
||||
</ul>
|
||||
<h1 id="proxomitron-tips">Proxomitron Tips</h1>
|
||||
<h2 id="to-use">To use</h2>
|
||||
<ul>
|
||||
<li><p>Add the ProxHTTPSProxy rear server to the Proxomitron's list of external proxies</p>
|
||||
<p><code>127.0.0.1:8081 ProxHTTPSProxy</code></p>
|
||||
</li>
|
||||
<li><p>Add to Proxomitron's "Bypass URLs that match this expression" field if it is empty</p>
|
||||
<p><code>$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)</code></p>
|
||||
</li>
|
||||
<li><p>Add to the beginning of the entry in Proxomitron's "Bypass URLs that match this expression" field if it is <strong>not</strong> empty</p>
|
||||
<p><code>$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)|</code> </p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="tips">Tips</h2>
|
||||
<ul>
|
||||
<li><p>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.</p>
|
||||
<p>This undocumented feature brings many possibilities but remember, an actual match triggers bypass of filtering!</p>
|
||||
<ul>
|
||||
<li><code>$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)</code> checks for the header that indicates an https request.</li>
|
||||
<li><code>$SETPROXY(127.0.0.1:8081)</code> is executed when found.</li>
|
||||
<li><code>(^)</code> expression never matches. </li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><p>Identify https connections by testing for the "Tagged" request header that the ProxHTTPSProxy front server adds to the request. </p>
|
||||
<p><code>$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)</code></p>
|
||||
</li>
|
||||
<li><p>For local file requests, use an expression like </p>
|
||||
<p><code>$USEPROXY(false)$RDIR(http://local.ptron/killed.gif)</code></p>
|
||||
</li>
|
||||
<li><p>Before redirecting "Tagged" connections to external resources consider removing the "Tagged" header. </p>
|
||||
</li>
|
||||
<li><p>If needed, the Proxomitron can still do https. After adding the ssl files to the Proxomitron, use a header filter like</p>
|
||||
<pre><code>[HTTP headers]
|
||||
In = FALSE
|
||||
Out = TRUE
|
||||
Key = "Tagged: Use Proxomitron for https://badcert.com"
|
||||
URL = "badcert.com$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$USEPROXY(false)$RDIR(https://badcert.com)"
|
||||
</code></pre><p>This filter also removes the "Tagged" header. </p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 id="for-the-current-sidki-set">For the current sidki set</h2>
|
||||
<ol>
|
||||
<li><p>Add the following two lines to Exceptions-U</p>
|
||||
<pre><code>$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^)
|
||||
~(^$TST(keyword=i_proxy:[03].))$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^)
|
||||
</code></pre></li>
|
||||
<li><p>Redirect connections to http resources with an expression like</p>
|
||||
<p><code>$USEPROXY(false)$SET(keyword=i_proxy:0.)$RDIR(http://local.ptron/killed.gif)</code></p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
</body></html>
|
119
Docs/readme.txt
119
Docs/readme.txt
@ -1,119 +0,0 @@
|
||||
ProxHTTPSProxyMII
|
||||
=================
|
||||
|
||||
Created to provide modern nag-free HTTPS connections for an HTTP proxy.
|
||||
|
||||
How it works
|
||||
----
|
||||
|
||||
![how it works](http://www.proxfilter.net/proxhttpsproxy/HowItWorks.gif)
|
||||
|
||||
Eligible HTTP Proxies
|
||||
----
|
||||
|
||||
* The [Proxomitron](http://www.proxomitron.info), for which ProxHTTPSProxy was created :)
|
||||
* Any that have the ability to forward all requests with a "Tagged:ProxHTTPSProxyMII FrontProxy/*" header to the ProxHTTPSProxyMII rear server.
|
||||
* Any that can be ran as two instances, one for true http and another for "tagged" http
|
||||
* Any that will only be used to monitor https traffic
|
||||
|
||||
Install
|
||||
----
|
||||
|
||||
* ProxHTTPSProxy's "CA.crt" to the Client's store of trusted certificate authorities.
|
||||
|
||||
Configure
|
||||
----
|
||||
|
||||
* The Client to use the ProxHTTPSProxy front server at 127.0.0.1 on port 8079 for secure connections.
|
||||
* The HTTP proxy to receive requests at 127.0.0.1 on port 8080.
|
||||
* The HTTP proxy to forward requests to the ProxHTTPSProxy rear server at 127.0.0.1 on port 8081.
|
||||
* Edit "Config.ini" to change these requirements.
|
||||
|
||||
Execute
|
||||
----
|
||||
|
||||
ProxHTTPSProxy.exe to start.
|
||||
|
||||
Remember
|
||||
----
|
||||
|
||||
Be aware and careful! Use a direct connection when you don't want any mistakes made.
|
||||
|
||||
Use at your own risk!
|
||||
|
||||
Have fun!
|
||||
|
||||
Discuss
|
||||
----
|
||||
|
||||
<http://prxbx.com/forums/showthread.php?tid=2172>
|
||||
|
||||
Author
|
||||
----
|
||||
|
||||
* phoenix (aka whenever)
|
||||
* JJoe (test and doc)
|
||||
|
||||
Proxomitron Tips
|
||||
================
|
||||
|
||||
To use
|
||||
----
|
||||
|
||||
* Add the ProxHTTPSProxy rear server to the Proxomitron's list of external proxies
|
||||
|
||||
`127.0.0.1:8081 ProxHTTPSProxyMII`
|
||||
|
||||
* Add to Proxomitron's "Bypass URLs that match this expression" field if it is empty
|
||||
|
||||
`$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)`
|
||||
|
||||
* Add to the beginning of the entry in Proxomitron's "Bypass URLs that match this expression" field if it is **not** empty
|
||||
|
||||
`$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)|`
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
* 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.
|
||||
|
||||
This undocumented feature brings many possibilities but remember, an actual match triggers bypass of filtering!
|
||||
|
||||
- `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)` checks for the header that indicates an https request.
|
||||
- `$SETPROXY(127.0.0.1:8081)` is executed when found.
|
||||
- `(^)` expression never matches.
|
||||
|
||||
* Identify https connections by testing for the "Tagged" request header that the ProxHTTPSProxy front server adds to the request.
|
||||
|
||||
`$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)`
|
||||
|
||||
* For local file requests, use an expression like
|
||||
|
||||
`$USEPROXY(false)$RDIR(http://local.ptron/killed.gif)`
|
||||
|
||||
* Before redirecting "Tagged" connections to external resources consider removing the "Tagged" header.
|
||||
|
||||
* If needed, the Proxomitron can still do https. After adding the ssl files to the Proxomitron, use a header filter like
|
||||
|
||||
```
|
||||
[HTTP headers]
|
||||
In = FALSE
|
||||
Out = TRUE
|
||||
Key = "Tagged: Use Proxomitron for https://badcert.com"
|
||||
URL = "badcert.com$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$USEPROXY(false)$RDIR(https://badcert.com)"
|
||||
```
|
||||
This filter also removes the "Tagged" header.
|
||||
|
||||
For the current sidki set
|
||||
----
|
||||
|
||||
1. Add the following two lines to Exceptions-U
|
||||
|
||||
```
|
||||
$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^)
|
||||
~(^$TST(keyword=i_proxy:[03].))$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^)
|
||||
```
|
||||
|
||||
2. Redirect connections to http resources with an expression like
|
||||
|
||||
`$USEPROXY(false)$SET(keyword=i_proxy:0.)$RDIR(http://local.ptron/killed.gif)`
|
120
README.md
120
README.md
@ -1,120 +0,0 @@
|
||||
ProxHTTPSProxyMII
|
||||
=================
|
||||
|
||||
A Proxomitron SSL Helper Program, can also be used to make a HTTP proxy like Privoxy capable of filtering HTTPS.
|
||||
|
||||
How it works
|
||||
----
|
||||
|
||||
![how it works](http://www.proxfilter.net/proxhttpsproxy/HowItWorks.gif)
|
||||
|
||||
Eligible HTTP Proxies
|
||||
----
|
||||
|
||||
* The [Proxomitron](http://www.proxomitron.info), for which ProxHTTPSProxy was created :)
|
||||
* Any that have the ability to forward all requests with a "Tagged:ProxHTTPSProxyMII FrontProxy/*" header to the ProxHTTPSProxyMII rear server.
|
||||
* Any that can be ran as two instances, one for true http and another for "tagged" http
|
||||
* Any that will only be used to monitor https traffic
|
||||
|
||||
Install
|
||||
----
|
||||
|
||||
* ProxHTTPSProxy's "CA.crt" to the Client's store of trusted certificate authorities.
|
||||
|
||||
Configure
|
||||
----
|
||||
|
||||
* The Client to use the ProxHTTPSProxy front server at 127.0.0.1 on port 8079 for secure connections.
|
||||
* The HTTP proxy to receive requests at 127.0.0.1 on port 8080.
|
||||
* The HTTP proxy to forward requests to the ProxHTTPSProxy rear server at 127.0.0.1 on port 8081.
|
||||
* Edit "Config.ini" to change these requirements.
|
||||
|
||||
Execute
|
||||
----
|
||||
|
||||
ProxHTTPSProxy.exe to start.
|
||||
|
||||
Remember
|
||||
----
|
||||
|
||||
Be aware and careful! Use a direct connection when you don't want any mistakes made.
|
||||
|
||||
Use at your own risk!
|
||||
|
||||
Have fun!
|
||||
|
||||
Discuss
|
||||
----
|
||||
|
||||
<http://prxbx.com/forums/showthread.php?tid=2172>
|
||||
|
||||
Author
|
||||
----
|
||||
|
||||
* phoenix (aka whenever)
|
||||
* JJoe (test and doc)
|
||||
|
||||
Proxomitron Tips
|
||||
================
|
||||
|
||||
To use
|
||||
----
|
||||
|
||||
* Add the ProxHTTPSProxy rear server to the Proxomitron's list of external proxies
|
||||
|
||||
`127.0.0.1:8081 ProxHTTPSProxyMII`
|
||||
|
||||
* Add to Proxomitron's "Bypass URLs that match this expression" field if it is empty
|
||||
|
||||
`$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)`
|
||||
|
||||
* Add to the beginning of the entry in Proxomitron's "Bypass URLs that match this expression" field if it is **not** empty
|
||||
|
||||
`$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SETPROXY(127.0.0.1:8081)(^)|`
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
* 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.
|
||||
|
||||
This undocumented feature brings many possibilities but remember, an actual match triggers bypass of filtering!
|
||||
|
||||
- `$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)` checks for the header that indicates an https request.
|
||||
- `$SETPROXY(127.0.0.1:8081)` is executed when found.
|
||||
- `(^)` expression never matches.
|
||||
|
||||
* Identify https connections by testing for the "Tagged" request header that the ProxHTTPSProxy front server adds to the request.
|
||||
|
||||
`$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)`
|
||||
|
||||
* For local file requests, use an expression like
|
||||
|
||||
`$USEPROXY(false)$RDIR(http://local.ptron/killed.gif)`
|
||||
|
||||
* Before redirecting "Tagged" connections to external resources consider removing the "Tagged" header.
|
||||
|
||||
* If needed, the Proxomitron can still do https. After adding the ssl files to the Proxomitron, use a header filter like
|
||||
|
||||
```
|
||||
[HTTP headers]
|
||||
In = FALSE
|
||||
Out = TRUE
|
||||
Key = "Tagged: Use Proxomitron for https://badcert.com"
|
||||
URL = "badcert.com$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$USEPROXY(false)$RDIR(https://badcert.com)"
|
||||
```
|
||||
This filter also removes the "Tagged" header.
|
||||
|
||||
For the current sidki set
|
||||
----
|
||||
|
||||
1. Add the following two lines to Exceptions-U
|
||||
|
||||
```
|
||||
$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^)
|
||||
~(^$TST(keyword=i_proxy:[03].))$OHDR(Tagged:ProxHTTPSProxyMII FrontProxy/*)$SET(keyword=$GET(keyword)i_proxy:3.)(^)
|
||||
```
|
||||
|
||||
2. Redirect connections to http resources with an expression like
|
||||
|
||||
`$USEPROXY(false)$SET(keyword=i_proxy:0.)$RDIR(http://local.ptron/killed.gif)`
|
||||
|
5229
cacert.pem
5229
cacert.pem
File diff suppressed because it is too large
Load Diff
116
cert.py
Executable file
116
cert.py
Executable file
@ -0,0 +1,116 @@
|
||||
'Certificate generation utilities'
|
||||
|
||||
__author__ = 'phoenix'
|
||||
__version__ = '0.1'
|
||||
|
||||
import os
|
||||
import time
|
||||
import OpenSSL.crypto as crypto
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
# Temp list for generating certs
|
||||
workingList = set()
|
||||
|
||||
|
||||
def create_CA(file):
|
||||
'''
|
||||
Generate the CA root certificate.
|
||||
'''
|
||||
key = crypto.PKey()
|
||||
key.generate_key(crypto.TYPE_RSA, 2048)
|
||||
ca = crypto.X509()
|
||||
ca.set_serial_number(0)
|
||||
# Value 2 means v3
|
||||
ca.set_version(2)
|
||||
subj = ca.get_subject()
|
||||
subj.countryName = 'CN'
|
||||
subj.organizationName = 'Privoxy'
|
||||
subj.organizationalUnitName = 'pyOpenSSL'
|
||||
subj.commonName = 'Privoxy Fake CA'
|
||||
ca.gmtime_adj_notBefore(0)
|
||||
ca.gmtime_adj_notAfter(24 * 60 * 60 * 3652)
|
||||
ca.set_issuer(ca.get_subject())
|
||||
ca.set_pubkey(key)
|
||||
ca.add_extensions([
|
||||
crypto.X509Extension(b'basicConstraints', True, b'CA:TRUE'),
|
||||
crypto.X509Extension(b'nsCertType', False, b'sslCA'),
|
||||
crypto.X509Extension(b'extendedKeyUsage', True,
|
||||
b'serverAuth,clientAuth,emailProtection,'
|
||||
b'timeStamping,msCodeInd,msCodeCom,msCTLSign'
|
||||
b',msSGC,msEFS,nsSGC'),
|
||||
crypto.X509Extension(b'keyUsage', False, b'keyCertSign, cRLSign'),
|
||||
crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash',
|
||||
subject=ca)])
|
||||
ca.sign(key, 'sha256')
|
||||
file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca).decode())
|
||||
file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key).decode())
|
||||
|
||||
|
||||
def get_cert(name, config):
|
||||
'''
|
||||
Return cert file path. Create it if it doesn't exist.
|
||||
|
||||
Reads from config:
|
||||
cafile: the CA file to create dummpy cert files
|
||||
certdir: the path where cert files are looked for or created
|
||||
'''
|
||||
certfile = os.path.join(config.CERTDIR, name + '.crt')
|
||||
if not os.path.exists(certfile):
|
||||
dummy_cert(config.CA, certfile, name)
|
||||
return certfile
|
||||
|
||||
|
||||
def dummy_cert(cafile, certfile, commonname):
|
||||
'''
|
||||
Generates and writes a certificate to certfile
|
||||
commonname: Common name for the generated certificate
|
||||
Ref: https://github.com/mitmproxy/netlib/blob/master/netlib/certutils.py
|
||||
'''
|
||||
if certfile in workingList:
|
||||
# Another thread is working on it, wait until it finish
|
||||
while True:
|
||||
time.sleep(0.2)
|
||||
if certfile not in workingList:
|
||||
break
|
||||
else:
|
||||
workingList.add(certfile)
|
||||
with open(cafile, 'rb') as file:
|
||||
content = file.read()
|
||||
ca = crypto.load_certificate(crypto.FILETYPE_PEM, content)
|
||||
key = crypto.load_privatekey(crypto.FILETYPE_PEM, content)
|
||||
cert = crypto.X509()
|
||||
# Value 2 means v3
|
||||
cert.set_version(2)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(60 * 60 * 24 * 3652)
|
||||
cert.set_issuer(ca.get_subject())
|
||||
if commonname.startswith('.'):
|
||||
domain = '*' + commonname
|
||||
else:
|
||||
domain = commonname
|
||||
cert.get_subject().CN = domain
|
||||
cert.set_serial_number(int(time.time()*10000))
|
||||
cert.set_pubkey(ca.get_pubkey())
|
||||
cert.add_extensions([
|
||||
crypto.X509Extension(b'subjectAltName', False,
|
||||
str.encode('DNS:'+domain))])
|
||||
cert.sign(key, 'sha256')
|
||||
with open(certfile, 'wb') as fp:
|
||||
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
|
||||
workingList.remove(certfile)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generates CA Certificates.')
|
||||
parser.add_argument('-f', '--file', type=argparse.FileType('w'),
|
||||
default=sys.stdout,
|
||||
help='CA certificate output file')
|
||||
args = parser.parse_args()
|
||||
create_CA(args.file)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
63
config.ini
63
config.ini
@ -1,63 +0,0 @@
|
||||
### The parent proxy has to support CONNECT method, if you want to proxy HTTPS requests
|
||||
###
|
||||
### Proxy setting applies to HTTPS requests only, as it is applied by the Rear Server
|
||||
### HTTP requests are passed to and handled by Proxomitron, please set up Proxomitron for proxy
|
||||
|
||||
[GENERAL]
|
||||
ProxAddr = http://localhost:8080
|
||||
FrontPort = 8079
|
||||
RearPort = 8081
|
||||
# DefaultProxy = http://127.0.0.1:8118
|
||||
|
||||
# Proper values for LogLevel are ERROR, WARNING, INFO, DEBUG
|
||||
# Default is INFO if unset
|
||||
LogLevel =
|
||||
|
||||
# * matches everything
|
||||
# ? matches any single character
|
||||
# [seq] matches any character in seq
|
||||
# [!seq] matches any character not in seq
|
||||
|
||||
[PROXY http://192.168.178.1:8123]
|
||||
#duckduckgo.com
|
||||
#*.s3.amazonaws.com
|
||||
|
||||
[PROXY socks5://192.168.178.3:1080]
|
||||
test.com
|
||||
|
||||
### Ignore SSL certificate verify, Use at your own risk!!!
|
||||
### Proxy setting still effective
|
||||
[SSL No-Verify]
|
||||
*.12306.cn
|
||||
|
||||
[BLACKLIST]
|
||||
*.doubleclick.net
|
||||
*.google-analytics.com
|
||||
|
||||
### Bypass Proxomitron and the Rear Server, Proxy setting still effective
|
||||
### SSL certificate verify will be done by the browser
|
||||
[SSL Pass-Thru]
|
||||
pypi.python.org
|
||||
www.gstatic.com
|
||||
watson.telemetry.microsoft.com
|
||||
*.sync.services.mozilla.com
|
||||
*.mega.co.nz
|
||||
|
||||
# Microsoft SmartScreen Filter Service
|
||||
*.smartscreen.microsoft.com
|
||||
urs.microsoft.com
|
||||
|
||||
# NoScript uses https://secure.informaction.com/ipecho to detect the WAN IP
|
||||
# https://addons.mozilla.org/en-US/firefox/addon/noscript/privacy/
|
||||
secure.informaction.com
|
||||
|
||||
### Bypass Proxomitron and the Rear Server, Proxy setting still effective
|
||||
### This section supports URL matching
|
||||
[BYPASS URL]
|
||||
http://www.abc.com/*
|
||||
https://bcd.net/*
|
||||
*://feedly.com/*
|
||||
*.zip
|
||||
*.rar
|
||||
*.exe
|
||||
*.pdf
|
@ -1,14 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"A Proxomitron Helper Program"
|
||||
"A Privoxy Helper Program"
|
||||
|
||||
_name = 'ProxHTTPSProxyMII'
|
||||
__author__ = 'phoenix'
|
||||
__version__ = 'v1.4'
|
||||
|
||||
CONFIG = "config.ini"
|
||||
CA_CERTS = "cacert.pem"
|
||||
|
||||
from socketserver import ThreadingMixIn
|
||||
from http.server import HTTPServer
|
||||
from urllib.parse import urlparse
|
||||
from proxy import Counter, ProxyRequestHandler, get_cert
|
||||
from urllib3.contrib.socks import SOCKSProxyManager
|
||||
|
||||
import os
|
||||
import time
|
||||
@ -18,29 +20,27 @@ import logging
|
||||
import threading
|
||||
import ssl
|
||||
import urllib3
|
||||
from urllib3.contrib.socks import SOCKSProxyManager
|
||||
#https://urllib3.readthedocs.org/en/latest/security.html#insecurerequestwarning
|
||||
urllib3.disable_warnings()
|
||||
import argparse
|
||||
import collections
|
||||
|
||||
from socketserver import ThreadingMixIn
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse
|
||||
from ProxyTool import ProxyRequestHandler, get_cert, counter
|
||||
|
||||
from colorama import init, Fore, Back, Style
|
||||
init(autoreset=True)
|
||||
_name = 'privoxy-tls'
|
||||
|
||||
|
||||
class LoadConfig:
|
||||
def __init__(self, configfile):
|
||||
self.config = configparser.ConfigParser(allow_no_value=True,
|
||||
inline_comment_prefixes=('#',))
|
||||
self.config.read(configfile)
|
||||
self.PROXADDR = self.config['GENERAL'].get('ProxAddr')
|
||||
self.FRONTPORT = int(self.config['GENERAL'].get('FrontPort'))
|
||||
self.REARPORT = int(self.config['GENERAL'].get('RearPort'))
|
||||
self.DEFAULTPROXY = self.config['GENERAL'].get('DefaultProxy')
|
||||
self.LOGLEVEL = self.config['GENERAL'].get('LogLevel')
|
||||
self.PROXADDR = self.config['General'].get('ProxAddr')
|
||||
self.FRONTPORT = int(self.config['General'].get('FrontPort'))
|
||||
self.REARPORT = int(self.config['General'].get('RearPort'))
|
||||
self.GeneralPROXY = self.config['General'].get('DefaultProxy')
|
||||
self.LOGLEVEL = self.config['General'].get('LogLevel')
|
||||
self.CA = self.config['General'].get('CACert')
|
||||
self.CERTDIR = self.config['General'].get('CertDir')
|
||||
|
||||
|
||||
class ConnectionPools:
|
||||
"""
|
||||
self.pools is a list of {'proxy': 'http://127.0.0.1:8080',
|
||||
@ -48,19 +48,10 @@ class ConnectionPools:
|
||||
'patterns': ['ab.com', 'bc.net', ...]}
|
||||
self.getpool() is a method that returns pool based on host matching
|
||||
"""
|
||||
# Windows default CA certificates are incomplete
|
||||
# See: http://bugs.python.org/issue20916
|
||||
# cacert.pem sources:
|
||||
# - http://curl.haxx.se/docs/caextract.html
|
||||
# - http://certifi.io/en/latest/
|
||||
|
||||
# ssl_version="TLSv1" to specific version
|
||||
sslparams = dict(cert_reqs="REQUIRED", ca_certs=CA_CERTS)
|
||||
# IE: http://support2.microsoft.com/kb/181050/en-us
|
||||
# Firefox about:config
|
||||
# network.http.connection-timeout 90
|
||||
# network.http.response.timeout 300
|
||||
timeout = urllib3.util.timeout.Timeout(connect=90.0, read=300.0)
|
||||
sslparams = dict(
|
||||
cert_reqs="REQUIRED",
|
||||
ca_certs='/etc/ssl/certs/ca-bundle.crt')
|
||||
timeout = urllib3.util.timeout.Timeout(connect=90.0, read=90.0)
|
||||
|
||||
def __init__(self, config):
|
||||
self.file = config
|
||||
@ -69,27 +60,40 @@ class ConnectionPools:
|
||||
|
||||
def loadConfig(self):
|
||||
# self.conf has to be inited each time for reloading
|
||||
self.conf = configparser.ConfigParser(allow_no_value=True, delimiters=('=',),
|
||||
inline_comment_prefixes=('#',))
|
||||
self.conf = configparser.ConfigParser(
|
||||
allow_no_value=True, delimiters=('=',),
|
||||
inline_comment_prefixes=('#',))
|
||||
self.conf.read(self.file)
|
||||
self.pools = []
|
||||
proxy_sections = [section for section in self.conf.sections()
|
||||
if section.startswith('PROXY')]
|
||||
if section.startswith('Proxy')]
|
||||
for section in proxy_sections:
|
||||
proxy = section.split()[1]
|
||||
self.pools.append(dict(proxy=proxy,
|
||||
pool=self.setProxyPool(proxy),
|
||||
patterns=list(self.conf[section].keys())))
|
||||
default_proxy = self.conf['GENERAL'].get('DefaultProxy')
|
||||
default_pool = (self.setProxyPool(default_proxy) if default_proxy else
|
||||
[urllib3.PoolManager(num_pools=10, maxsize=8, timeout=self.timeout, **self.sslparams),
|
||||
urllib3.PoolManager(num_pools=10, maxsize=8, timeout=self.timeout)])
|
||||
self.pools.append({'proxy': default_proxy, 'pool': default_pool, 'patterns': '*'})
|
||||
default_proxy = self.conf['General'].get('DefaultProxy')
|
||||
|
||||
self.noverifylist = list(self.conf['SSL No-Verify'].keys())
|
||||
self.blacklist = list(self.conf['BLACKLIST'].keys())
|
||||
self.sslpasslist = list(self.conf['SSL Pass-Thru'].keys())
|
||||
self.bypasslist = list(self.conf['BYPASS URL'].keys())
|
||||
if default_proxy:
|
||||
default_pool = self.setProxyPool(default_proxy)
|
||||
else:
|
||||
default_pool = [
|
||||
urllib3.PoolManager(num_pools=10, maxsize=8,
|
||||
timeout=self.timeout, **self.sslparams),
|
||||
urllib3.PoolManager(num_pools=10, maxsize=8,
|
||||
timeout=self.timeout)]
|
||||
|
||||
self.pools.append(dict(proxy=default_proxy,
|
||||
pool=default_pool, patterns='*'))
|
||||
|
||||
# handle missing sections
|
||||
sections = collections.defaultdict(dict)
|
||||
for name in self.conf.sections():
|
||||
sections[name] = self.conf[name]
|
||||
self.noverifylist = list(sections['TLS NoVerify'].keys())
|
||||
self.sslpasslist = list(sections['TLS Passthru'].keys())
|
||||
self.blacklist = list(sections['Blacklist'].keys())
|
||||
self.bypasslist = list(sections['Bypass URL'].keys())
|
||||
|
||||
def reloadConfig(self):
|
||||
while True:
|
||||
@ -97,14 +101,18 @@ class ConnectionPools:
|
||||
if mtime > self.file_timestamp:
|
||||
self.file_timestamp = mtime
|
||||
self.loadConfig()
|
||||
logger.info(Fore.RED + Style.BRIGHT
|
||||
+ "*" * 20 + " CONFIG RELOADED " + "*" * 20)
|
||||
logger.info("*" * 20 + " CONFIG RELOADED " + "*" * 20)
|
||||
time.sleep(1)
|
||||
|
||||
def getpool(self, host, httpmode=False):
|
||||
noverify = True if httpmode or any((fnmatch.fnmatch(host, pattern) for pattern in self.noverifylist)) else False
|
||||
things = (fnmatch.fnmatch(host, pattern)
|
||||
for pattern in self.noverifylist)
|
||||
noverify = True if httpmode or any(things) else False
|
||||
logger.debug(f'host: {host}, noverify: {noverify}')
|
||||
for pool in self.pools:
|
||||
if any((fnmatch.fnmatch(host, pattern) for pattern in pool['patterns'])):
|
||||
things = (fnmatch.fnmatch(host, pattern)
|
||||
for pattern in pool['patterns'])
|
||||
if any(things):
|
||||
return pool['proxy'], pool['pool'][noverify], noverify
|
||||
|
||||
def setProxyPool(self, proxy):
|
||||
@ -119,23 +127,28 @@ class ConnectionPools:
|
||||
input()
|
||||
raise SystemExit
|
||||
# maxsize is the max. number of connections to the same server
|
||||
return [ProxyManager(proxy, num_pools=10, maxsize=8, timeout=self.timeout, **self.sslparams),
|
||||
ProxyManager(proxy, num_pools=10, maxsize=8, timeout=self.timeout)]
|
||||
return [
|
||||
ProxyManager(proxy, num_pools=10, maxsize=8, timeout=self.timeout,
|
||||
**self.sslparams),
|
||||
ProxyManager(proxy, num_pools=10, maxsize=8, timeout=self.timeout)]
|
||||
|
||||
|
||||
class FrontServer(ThreadingMixIn, HTTPServer):
|
||||
"""Handle requests in a separate thread."""
|
||||
pass
|
||||
|
||||
|
||||
class RearServer(ThreadingMixIn, HTTPServer):
|
||||
"""Handle requests in a separate thread."""
|
||||
pass
|
||||
|
||||
|
||||
class FrontRequestHandler(ProxyRequestHandler):
|
||||
"""
|
||||
Sit between the client and Proxomitron
|
||||
Sit between the client and Privoxy
|
||||
Convert https request to http
|
||||
"""
|
||||
server_version = "%s FrontProxy/%s" % (_name, __version__)
|
||||
server_version = "%s front/%s" % (_name, __version__)
|
||||
|
||||
def do_CONNECT(self):
|
||||
"Descrypt https request and dispatch to http handler"
|
||||
@ -143,11 +156,15 @@ class FrontRequestHandler(ProxyRequestHandler):
|
||||
# request line: CONNECT www.example.com:443 HTTP/1.1
|
||||
self.host, self.port = self.path.split(":")
|
||||
self.proxy, self.pool, self.noverify = pools.getpool(self.host)
|
||||
if any((fnmatch.fnmatch(self.host, pattern) for pattern in pools.blacklist)):
|
||||
things = (fnmatch.fnmatch(self.host, pattern)
|
||||
for pattern in pools.blacklist)
|
||||
if any(things):
|
||||
# BLACK LIST
|
||||
self.deny_request()
|
||||
logger.info("%03d " % self.reqNum + Fore.CYAN + 'Denied by blacklist: %s' % self.host)
|
||||
elif any((fnmatch.fnmatch(self.host, pattern) for pattern in pools.sslpasslist)):
|
||||
logger.info("%03d " % self.reqNum + 'Denied by blacklist: s'
|
||||
% self.host)
|
||||
elif any((fnmatch.fnmatch(self.host, pattern)
|
||||
for pattern in pools.sslpasslist)):
|
||||
# SSL Pass-Thru
|
||||
if self.proxy and self.proxy.startswith('https'):
|
||||
self.forward_to_https_proxy()
|
||||
@ -155,19 +172,24 @@ class FrontRequestHandler(ProxyRequestHandler):
|
||||
self.forward_to_socks5_proxy()
|
||||
else:
|
||||
self.tunnel_traffic()
|
||||
# Upstream server or proxy of the tunnel is closed explictly, so we close the local connection too
|
||||
# Upstream server or proxy of the tunnel is
|
||||
# closed explictly, so we close the local connection too
|
||||
self.close_connection = 1
|
||||
else:
|
||||
# SSL MITM
|
||||
self.wfile.write(("HTTP/1.1 200 Connection established\r\n" +
|
||||
"Proxy-agent: %s\r\n" % self.version_string() +
|
||||
"\r\n").encode('ascii'))
|
||||
commonname = '.' + self.host.partition('.')[-1] if self.host.count('.') >= 2 else self.host
|
||||
dummycert = get_cert(commonname)
|
||||
if self.host.count('.') >= 2:
|
||||
commonname = '.' + self.host.partition('.')[-1]
|
||||
else:
|
||||
commonname = self.host
|
||||
dummycert = get_cert(commonname, config)
|
||||
# set a flag for do_METHOD
|
||||
self.ssltunnel = True
|
||||
|
||||
ssl_sock = ssl.wrap_socket(self.connection, keyfile=dummycert, certfile=dummycert, server_side=True)
|
||||
ssl_sock = ssl.wrap_socket(self.connection, keyfile=dummycert,
|
||||
certfile=dummycert, server_side=True)
|
||||
# Ref: Lib/socketserver.py#StreamRequestHandler.setup()
|
||||
self.connection = ssl_sock
|
||||
self.rfile = self.connection.makefile('rb', self.rbufsize)
|
||||
@ -176,48 +198,57 @@ class FrontRequestHandler(ProxyRequestHandler):
|
||||
self.handle_one_request()
|
||||
|
||||
def do_METHOD(self):
|
||||
"Forward request to Proxomitron"
|
||||
"Forward request to Privoxy"
|
||||
|
||||
counter.increment_and_set(self, 'reqNum')
|
||||
|
||||
if self.ssltunnel:
|
||||
# https request
|
||||
host = self.host if self.port == '443' else "%s:%s" % (self.host, self.port)
|
||||
host = (self.host if self.port == '443'
|
||||
else "%s:%s" % (self.host, self.port))
|
||||
url = "https://%s%s" % (host, self.path)
|
||||
self.bypass = any((fnmatch.fnmatch(url, pattern) for pattern in pools.bypasslist))
|
||||
self.bypass = any((fnmatch.fnmatch(url, pattern)
|
||||
for pattern in pools.bypasslist))
|
||||
if not self.bypass:
|
||||
url = "http://%s%s" % (host, self.path)
|
||||
# Tag the request so Proxomitron can recognize it
|
||||
self.headers["Tagged"] = self.version_string() + ":%d" % self.reqNum
|
||||
# Tag the request so Privoxy can recognize it
|
||||
self.headers["Tagged"] = (self.version_string()
|
||||
+ ":%d" % self.reqNum)
|
||||
else:
|
||||
# http request
|
||||
self.host = urlparse(self.path).hostname
|
||||
if any((fnmatch.fnmatch(self.host, pattern) for pattern in pools.blacklist)):
|
||||
if any((fnmatch.fnmatch(self.host, pattern)
|
||||
for pattern in pools.blacklist)):
|
||||
# BLACK LIST
|
||||
self.deny_request()
|
||||
logger.info("%03d " % self.reqNum + Fore.CYAN + 'Denied by blacklist: %s' % self.host)
|
||||
logger.info("%03d " % self.reqNum
|
||||
+ 'Denied by blacklist: %s' % self.host)
|
||||
return
|
||||
host = urlparse(self.path).netloc
|
||||
self.proxy, self.pool, self.noverify = pools.getpool(self.host, httpmode=True)
|
||||
self.bypass = any((fnmatch.fnmatch('http://' + host + urlparse(self.path).path, pattern) for pattern in pools.bypasslist))
|
||||
self.proxy, self.pool, self.noverify = pools.getpool(
|
||||
self.host, httpmode=True)
|
||||
self.bypass = any((fnmatch.fnmatch('http://' + host +
|
||||
urlparse(self.path).path, pattern)
|
||||
for pattern in pools.bypasslist))
|
||||
url = self.path
|
||||
self.url = url
|
||||
pool = self.pool if self.bypass else proxpool
|
||||
data_length = self.headers.get("Content-Length")
|
||||
self.postdata = self.rfile.read(int(data_length)) if data_length and int(data_length) > 0 else None
|
||||
self.postdata = (self.rfile.read(int(data_length))
|
||||
if data_length and int(data_length) > 0 else None)
|
||||
if self.command == "POST" and "Content-Length" not in self.headers:
|
||||
buffer = self.rfile.read()
|
||||
if buffer:
|
||||
logger.warning("%03d " % self.reqNum + Fore.RED +
|
||||
'POST w/o "Content-Length" header (Bytes: %d | Transfer-Encoding: %s | HTTPS: %s',
|
||||
len(buffer), "Transfer-Encoding" in self.headers, self.ssltunnel)
|
||||
logger.warning(
|
||||
"%03d " % self.reqNum +
|
||||
'POST w/o "Content-Length" header (Bytes: %d |'
|
||||
' Transfer-Encoding: %s | HTTPS: %s',
|
||||
len(buffer), "Transfer-Encoding" in self.headers,
|
||||
self.ssltunnel)
|
||||
# Remove hop-by-hop headers
|
||||
self.purge_headers(self.headers)
|
||||
r = None
|
||||
|
||||
# Below code in connectionpool.py expect the headers to has a copy() and update() method
|
||||
# That's why we can't use self.headers directly when call pool.urlopen()
|
||||
#
|
||||
# Merge the proxy headers. Only do this in HTTP. We have to copy the
|
||||
# headers dict so we can safely change it without those changes being
|
||||
# reflected in anyone else's copy.
|
||||
@ -227,43 +258,55 @@ class FrontRequestHandler(ProxyRequestHandler):
|
||||
headers = urllib3._collections.HTTPHeaderDict(self.headers)
|
||||
|
||||
try:
|
||||
# Sometimes 302 redirect would fail with "BadStatusLine" exception, and IE11 doesn't restart the request.
|
||||
# Sometimes 302 redirect would fail with "BadStatusLine"
|
||||
# exception, and IE11 doesn't restart the request.
|
||||
# retries=1 instead of retries=False fixes it.
|
||||
#! Retry may cause the requests with the same reqNum appear in the log window
|
||||
r = pool.urlopen(self.command, url, body=self.postdata, headers=headers,
|
||||
retries=1, redirect=False, preload_content=False, decode_content=False)
|
||||
# ! Retry may cause the requests with the same reqNum appear
|
||||
# in the log window
|
||||
r = pool.urlopen(
|
||||
self.command, url, body=self.postdata, headers=headers,
|
||||
retries=1, redirect=False, preload_content=False,
|
||||
decode_content=False)
|
||||
if not self.ssltunnel:
|
||||
if self.bypass:
|
||||
prefix = '[BP]' if self.proxy else '[BD]'
|
||||
else:
|
||||
prefix = '[D]'
|
||||
if self.command in ("GET", "HEAD"):
|
||||
logger.info("%03d " % self.reqNum + Fore.MAGENTA + '%s "%s %s" %s %s' %
|
||||
(prefix, self.command, url, r.status, r.getheader('Content-Length', '-')))
|
||||
logger.info("%03d " % self.reqNum + '%s "%s %s" %s %s' %
|
||||
(prefix, self.command, url, r.status,
|
||||
r.getheader('Content-Length', '-')))
|
||||
else:
|
||||
logger.info("%03d " % self.reqNum + Fore.MAGENTA + '%s "%s %s %s" %s %s' %
|
||||
(prefix, self.command, url, data_length, r.status, r.getheader('Content-Length', '-')))
|
||||
logger.info("%03d " % self.reqNum + '%s "%s %s %s" %s %s' %
|
||||
(prefix, self.command, url, data_length,
|
||||
r.status, r.getheader('Content-Length', '-')))
|
||||
|
||||
self.send_response_only(r.status, r.reason)
|
||||
# HTTPResponse.msg is easier to handle than urllib3._collections.HTTPHeaderDict
|
||||
# HTTPResponse.msg is easier to handle
|
||||
# than urllib3._collections.HTTPHeaderDict
|
||||
r.headers = r._original_response.msg
|
||||
self.purge_write_headers(r.headers)
|
||||
|
||||
if self.command == 'HEAD' or r.status in (100, 101, 204, 304) or r.getheader("Content-Length") == '0':
|
||||
if (self.command == 'HEAD' or r.status in (100, 101, 204, 304)
|
||||
or r.getheader("Content-Length") == '0'):
|
||||
written = None
|
||||
else:
|
||||
written = self.stream_to_client(r)
|
||||
if "Content-Length" not in r.headers and 'Transfer-Encoding' not in r.headers:
|
||||
if ("Content-Length" not in r.headers
|
||||
and 'Transfer-Encoding' not in r.headers):
|
||||
self.close_connection = 1
|
||||
|
||||
# Intend to catch regular http and bypass http/https requests exceptions
|
||||
# Intend to catch regular http and bypass
|
||||
# http/https requests exceptions
|
||||
# Regular https request exceptions should be handled by rear server
|
||||
except urllib3.exceptions.TimeoutError as e:
|
||||
self.sendout_error(url, 504, message="Timeout", explain=e)
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + '[F] %s on "%s %s"', e, self.command, url)
|
||||
logger.warning("%03d " % self.reqNum + '[F] %s on "%s %s"',
|
||||
e, self.command, url)
|
||||
except (urllib3.exceptions.HTTPError,) as e:
|
||||
self.sendout_error(url, 502, message="HTTPError", explain=e)
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + '[F] %s on "%s %s"', e, self.command, url)
|
||||
logger.warning("%03d " % self.reqNum + '[F] %s on "%s %s"',
|
||||
e, self.command, url)
|
||||
finally:
|
||||
if r:
|
||||
# Release the connection back into the pool
|
||||
@ -271,27 +314,30 @@ class FrontRequestHandler(ProxyRequestHandler):
|
||||
|
||||
do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_OPTIONS = do_METHOD
|
||||
|
||||
|
||||
class RearRequestHandler(ProxyRequestHandler):
|
||||
"""
|
||||
Supposed to be the parent proxy for Proxomitron for tagged requests
|
||||
Supposed to be the parent proxy for Privoxy for tagged requests
|
||||
Convert http request to https
|
||||
|
||||
"""
|
||||
server_version = "%s RearProxy/%s" % (_name, __version__)
|
||||
|
||||
server_version = "%s rear/%s" % (_name, __version__)
|
||||
|
||||
def do_METHOD(self):
|
||||
"Convert http request to https"
|
||||
|
||||
if self.headers.get("Tagged") and self.headers["Tagged"].startswith(_name):
|
||||
if (self.headers.get("Tagged")
|
||||
and self.headers["Tagged"].startswith(_name)):
|
||||
self.reqNum = int(self.headers["Tagged"].split(":")[1])
|
||||
# Remove the tag
|
||||
del self.headers["Tagged"]
|
||||
else:
|
||||
self.sendout_error(self.path, 400,
|
||||
explain="The proxy setting of the client is misconfigured.\n\n" +
|
||||
"Please set the HTTPS proxy port to %s " % config.FRONTPORT +
|
||||
"and check the Docs for other settings.")
|
||||
logger.error(Fore.RED + Style.BRIGHT + "[Misconfigured HTTPS proxy port] " + self.path)
|
||||
self.sendout_error(
|
||||
self.path, 400,
|
||||
explain="The proxy setting of the client"
|
||||
" is misconfigured.\n\n" +
|
||||
"Please set the HTTPS proxy port to %s " % config.FRONTPORT +
|
||||
"and check the Docs for other settings.")
|
||||
logger.error("[Misconfigured HTTPS proxy port] " + self.path)
|
||||
return
|
||||
|
||||
# request line: GET http://somehost.com/path?attr=value HTTP/1.1
|
||||
@ -300,12 +346,15 @@ class RearRequestHandler(ProxyRequestHandler):
|
||||
proxy, pool, noverify = pools.getpool(self.host)
|
||||
prefix = '[P]' if proxy else '[D]'
|
||||
data_length = self.headers.get("Content-Length")
|
||||
self.postdata = self.rfile.read(int(data_length)) if data_length else None
|
||||
self.postdata = (self.rfile.read(int(data_length))
|
||||
if data_length else None)
|
||||
self.purge_headers(self.headers)
|
||||
r = None
|
||||
|
||||
# Below code in connectionpool.py expect the headers to has a copy() and update() method
|
||||
# That's why we can't use self.headers directly when call pool.urlopen()
|
||||
# Below code in connectionpool.py expect the headers
|
||||
# to has a copy() and update() method
|
||||
# That's why we can't use self.headers directly when
|
||||
# call pool.urlopen()
|
||||
#
|
||||
# Merge the proxy headers. Only do this in HTTP. We have to copy the
|
||||
# headers dict so we can safely change it without those changes being
|
||||
@ -316,39 +365,51 @@ class RearRequestHandler(ProxyRequestHandler):
|
||||
headers = urllib3._collections.HTTPHeaderDict(self.headers)
|
||||
|
||||
try:
|
||||
r = pool.urlopen(self.command, url, body=self.postdata, headers=headers,
|
||||
retries=1, redirect=False, preload_content=False, decode_content=False)
|
||||
r = pool.urlopen(
|
||||
self.command, url, body=self.postdata, headers=headers,
|
||||
retries=1, redirect=False, preload_content=False,
|
||||
decode_content=False)
|
||||
if proxy:
|
||||
logger.debug('Using Proxy - %s' % proxy)
|
||||
color = Fore.RED if noverify else Fore.GREEN
|
||||
if self.command in ("GET", "HEAD"):
|
||||
logger.info("%03d " % self.reqNum + color + '%s "%s %s" %s %s' %
|
||||
(prefix, self.command, url, r.status, r.getheader('Content-Length', '-')))
|
||||
logger.info(
|
||||
"%03d " % self.reqNum + '%s "%s %s" %s %s' %
|
||||
(prefix, self.command, url, r.status,
|
||||
r.getheader('Content-Length', '-')))
|
||||
else:
|
||||
logger.info("%03d " % self.reqNum + color + '%s "%s %s %s" %s %s' %
|
||||
(prefix, self.command, url, data_length, r.status, r.getheader('Content-Length', '-')))
|
||||
logger.info(
|
||||
"%03d " % self.reqNum + '%s "%s %s %s" %s %s' %
|
||||
(prefix, self.command, url, data_length, r.status,
|
||||
r.getheader('Content-Length', '-')))
|
||||
|
||||
self.send_response_only(r.status, r.reason)
|
||||
# HTTPResponse.msg is easier to handle than urllib3._collections.HTTPHeaderDict
|
||||
# HTTPResponse.msg is easier to handle than
|
||||
# urllib3._collections.HTTPHeaderDict
|
||||
r.headers = r._original_response.msg
|
||||
self.purge_write_headers(r.headers)
|
||||
|
||||
if self.command == 'HEAD' or r.status in (100, 101, 204, 304) or r.getheader("Content-Length") == '0':
|
||||
|
||||
if (self.command == 'HEAD' or r.status in (100, 101, 204, 304)
|
||||
or r.getheader("Content-Length") == '0'):
|
||||
written = None
|
||||
else:
|
||||
written = self.stream_to_client(r)
|
||||
if "Content-Length" not in r.headers and 'Transfer-Encoding' not in r.headers:
|
||||
if ("Content-Length" not in r.headers
|
||||
and 'Transfer-Encoding' not in r.headers):
|
||||
self.close_connection = 1
|
||||
|
||||
except urllib3.exceptions.SSLError as e:
|
||||
self.sendout_error(url, 417, message="SSL Certificate Failed", explain=e)
|
||||
logger.error("%03d " % self.reqNum + Fore.RED + Style.BRIGHT + "[SSL Certificate Error] " + url)
|
||||
self.sendout_error(url, 417, message="SSL Certificate Failed",
|
||||
explain=e)
|
||||
logger.error("%03d " % self.reqNum + "[SSL Certificate Error] "
|
||||
+ url)
|
||||
except urllib3.exceptions.TimeoutError as e:
|
||||
self.sendout_error(url, 504, message="Timeout", explain=e)
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + '[R]%s "%s %s" %s', prefix, self.command, url, e)
|
||||
logger.warning("%03d " % self.reqNum + '[R]%s "%s %s" %s',
|
||||
prefix, self.command, url, e)
|
||||
except (urllib3.exceptions.HTTPError,) as e:
|
||||
self.sendout_error(url, 502, message="HTTPError", explain=e)
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + '[R]%s "%s %s" %s', prefix, self.command, url, e)
|
||||
logger.warning("%03d " % self.reqNum + '[R]%s "%s %s" %s',
|
||||
prefix, self.command, url, e)
|
||||
|
||||
finally:
|
||||
if r:
|
||||
@ -357,51 +418,53 @@ class RearRequestHandler(ProxyRequestHandler):
|
||||
|
||||
do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_OPTIONS = do_METHOD
|
||||
|
||||
"""
|
||||
#Information#
|
||||
|
||||
* Python default ciphers: http://bugs.python.org/issue20995
|
||||
* SSL Cipher Suite Details of Your Browser: https://cc.dcsec.uni-hannover.de/
|
||||
* https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
"""
|
||||
|
||||
try:
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
ctypes.windll.kernel32.SetConsoleTitleW('%s %s' % (_name, __version__))
|
||||
|
||||
config = LoadConfig(CONFIG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
def main():
|
||||
urllib3.disable_warnings()
|
||||
logger.setLevel(getattr(logging, config.LOGLEVEL, logging.INFO))
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter('%(asctime)s %(message)s', datefmt='[%H:%M]')
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
pools = ConnectionPools(CONFIG)
|
||||
proxpool = urllib3.ProxyManager(config.PROXADDR, num_pools=10, maxsize=8,
|
||||
# A little longer than timeout of rear pool
|
||||
# to avoid trigger front server exception handler
|
||||
timeout=urllib3.util.timeout.Timeout(connect=90.0, read=310.0))
|
||||
|
||||
frontserver = FrontServer(('', config.FRONTPORT), FrontRequestHandler)
|
||||
rearserver = RearServer(('', config.REARPORT), RearRequestHandler)
|
||||
frontserver.config = config
|
||||
for worker in (frontserver.serve_forever, rearserver.serve_forever,
|
||||
pools.reloadConfig):
|
||||
thread = threading.Thread(target=worker)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
thread = threading.Thread(target=worker)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
print("=" * 76)
|
||||
print("=" * 40)
|
||||
print('%s %s (urllib3/%s)' % (_name, __version__, urllib3.__version__))
|
||||
print()
|
||||
print(' FrontServer : localhost:%s' % config.FRONTPORT)
|
||||
print(' RearServer : localhost:%s' % config.REARPORT)
|
||||
print(' ParentServer : %s' % config.DEFAULTPROXY)
|
||||
print(' Proxomitron : ' + config.PROXADDR)
|
||||
print("=" * 76)
|
||||
print('Front : localhost:%s' % config.FRONTPORT)
|
||||
print('Privoxy :', config.PROXADDR)
|
||||
print('Rear : localhost:%s' % config.REARPORT)
|
||||
print('Parent : %s' % config.GeneralPROXY)
|
||||
print("=" * 40)
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Quitting...")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser('Privoxy TLS proxy wrapper.')
|
||||
parser.add_argument('-c', '--config', type=argparse.FileType('r'),
|
||||
default='config.ini',
|
||||
help='Privoxy TLS configuration file.')
|
||||
args = parser.parse_args()
|
||||
|
||||
# globals
|
||||
CONFIG = args.config.name
|
||||
logger = logging.getLogger(__name__)
|
||||
config = LoadConfig(CONFIG)
|
||||
proxpool = urllib3.ProxyManager(
|
||||
config.PROXADDR, num_pools=10, maxsize=8,
|
||||
timeout=urllib3.util.timeout.Timeout(
|
||||
connect=90.0, read=310.0))
|
||||
pools = ConnectionPools(CONFIG)
|
||||
counter = Counter()
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("Quitting...")
|
@ -1,13 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"HTTP Proxy Tools, pyOpenSSL version"
|
||||
"HTTP Proxy utilities, pyOpenSSL version"
|
||||
|
||||
_name = "ProxyTool"
|
||||
__author__ = 'phoenix'
|
||||
__version__ = '1.0'
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import threading
|
||||
@ -15,40 +12,45 @@ import cgi
|
||||
import socket
|
||||
import select
|
||||
import ssl
|
||||
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
from CertTool import get_cert
|
||||
|
||||
from colorama import init, Fore, Back, Style
|
||||
init(autoreset=True)
|
||||
from cert import get_cert
|
||||
|
||||
_name = 'proxy'
|
||||
logger = logging.getLogger('__main__')
|
||||
|
||||
message_format = """\
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
message_format = '''\
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
<title>Proxy Error: %(code)d</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>%(code)d: %(message)s</h1>
|
||||
<p>The following error occurred while trying to access <strong>%(url)s</strong></p>
|
||||
<p><strong>%(explain)s</strong></p>
|
||||
<hr>Generated on %(now)s by %(server)s.
|
||||
</body>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
<title>Proxy Error: {code}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{code}: {message}</h1>
|
||||
<p>The following error occurred while
|
||||
trying to access <strong>{url}</strong>
|
||||
</p>
|
||||
<p><strong>{explain}</strong></p>
|
||||
<hr>Generated on {now} by {server}.
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
'''
|
||||
|
||||
|
||||
def read_write(socket1, socket2, max_idling=10):
|
||||
"Read and Write contents between 2 sockets"
|
||||
'''
|
||||
Read and Write contents between 2 sockets
|
||||
'''
|
||||
iw = [socket1, socket2]
|
||||
ow = []
|
||||
count = 0
|
||||
while True:
|
||||
count += 1
|
||||
(ins, _, exs) = select.select(iw, ow, iw, 1)
|
||||
if exs: break
|
||||
if exs:
|
||||
break
|
||||
if ins:
|
||||
for reader in ins:
|
||||
writer = socket2 if reader is socket1 else socket1
|
||||
@ -57,26 +59,32 @@ def read_write(socket1, socket2, max_idling=10):
|
||||
if data:
|
||||
writer.send(data)
|
||||
count = 0
|
||||
except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError):
|
||||
except (ConnectionAbortedError,
|
||||
ConnectionResetError,
|
||||
BrokenPipeError):
|
||||
pass
|
||||
if count == max_idling: break
|
||||
if count == max_idling:
|
||||
break
|
||||
|
||||
|
||||
class Counter:
|
||||
reset_value = 999
|
||||
|
||||
def __init__(self, start=0):
|
||||
self.lock = threading.Lock()
|
||||
self.value = start
|
||||
|
||||
def increment_and_set(self, obj, attr):
|
||||
with self.lock:
|
||||
self.value = self.value + 1 if self.value < self.reset_value else 1
|
||||
setattr(obj, attr, self.value)
|
||||
|
||||
counter = Counter()
|
||||
|
||||
class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
"""RequestHandler with do_CONNECT method defined
|
||||
"""
|
||||
server_version = "%s/%s" % (_name, __version__)
|
||||
'''
|
||||
RequestHandler with do_CONNECT method defined
|
||||
'''
|
||||
server_version = f'{_name}/{__version__}'
|
||||
# do_CONNECT() will set self.ssltunnel to override this
|
||||
ssltunnel = False
|
||||
# Override default value 'HTTP/1.0'
|
||||
@ -85,19 +93,27 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
reqNum = 0
|
||||
|
||||
def do_CONNECT(self):
|
||||
"Descrypt https request and dispatch to http handler"
|
||||
'''
|
||||
Descrypt https request and dispatch to http handler
|
||||
'''
|
||||
# request line: CONNECT www.example.com:443 HTTP/1.1
|
||||
self.host, self.port = self.path.split(":")
|
||||
self.host, self.port = self.path.split(':')
|
||||
# SSL MITM
|
||||
self.wfile.write(("HTTP/1.1 200 Connection established\r\n" +
|
||||
"Proxy-agent: %s\r\n" % self.version_string() +
|
||||
"\r\n").encode('ascii'))
|
||||
commonname = '.' + self.host.partition('.')[-1] if self.host.count('.') >= 2 else self.host
|
||||
dummycert = get_cert(commonname)
|
||||
self.wfile.write(
|
||||
('HTTP/1.1 200 Connection established\r\n'
|
||||
+ f'Proxy-agent: {self.version_string()}\r\n'
|
||||
+ '\r\n').encode('ascii'))
|
||||
|
||||
if self.host.count('.') >= 2:
|
||||
commonname = '.' + self.host.partition('.')[-1]
|
||||
else:
|
||||
commonname = self.host
|
||||
dummycert = get_cert(commonname, self.config)
|
||||
# set a flag for do_METHOD
|
||||
self.ssltunnel = True
|
||||
|
||||
ssl_sock = ssl.wrap_socket(self.connection, keyfile=dummycert, certfile=dummycert, server_side=True)
|
||||
ssl_sock = ssl.wrap_socket(self.connection, keyfile=dummycert,
|
||||
certfile=dummycert, server_side=True)
|
||||
# Ref: Lib/socketserver.py#StreamRequestHandler.setup()
|
||||
self.connection = ssl_sock
|
||||
self.rfile = self.connection.makefile('rb', self.rbufsize)
|
||||
@ -106,26 +122,33 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
self.handle_one_request()
|
||||
|
||||
def handle_one_request(self):
|
||||
"""Catch more exceptions than default
|
||||
'''Catch more exceptions than default
|
||||
|
||||
Intend to catch exceptions on local side
|
||||
Exceptions on remote side should be handled in do_*()
|
||||
"""
|
||||
'''
|
||||
try:
|
||||
BaseHTTPRequestHandler.handle_one_request(self)
|
||||
return
|
||||
except (ConnectionError, FileNotFoundError) as e:
|
||||
logger.warning("%03d " % self.reqNum + Fore.RED + "%s %s", self.server_version, e)
|
||||
logger.warning('{:03d} {} {}'.format(
|
||||
self.reqNum, self.server_version, e))
|
||||
except (ssl.SSLEOFError, ssl.SSLError) as e:
|
||||
if hasattr(self, 'url'):
|
||||
# Happens after the tunnel is established
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + '"%s" while operating on established local SSL tunnel for [%s]' % (e, self.url))
|
||||
logger.warning(f'{self.reqNum:03d} "{e}" while operating'
|
||||
f' on established local SSL tunnel for'
|
||||
f' [{self.url}]')
|
||||
else:
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + '"%s" while trying to establish local SSL tunnel for [%s]' % (e, self.path))
|
||||
logger.warning(f'{self.reqNum:03d} "{e}" while trying to'
|
||||
' establish local SSL tunnel for'
|
||||
f'[{self.path}]')
|
||||
self.close_connection = 1
|
||||
|
||||
def sendout_error(self, url, code, message=None, explain=None):
|
||||
"Modified from http.server.send_error() for customized display"
|
||||
'''
|
||||
Modified from http.server.send_error() for customized display
|
||||
'''
|
||||
try:
|
||||
shortmsg, longmsg = self.responses[code]
|
||||
except KeyError:
|
||||
@ -134,12 +157,14 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
message = shortmsg
|
||||
if explain is None:
|
||||
explain = longmsg
|
||||
content = (message_format %
|
||||
{'code': code, 'message': message, 'explain': explain,
|
||||
'url': url, 'now': datetime.today(), 'server': self.server_version})
|
||||
content = message_format.format(
|
||||
code=code, message=message,
|
||||
explain=explain, url=url,
|
||||
now=datetime.today(),
|
||||
server=self.server_version)
|
||||
body = content.encode('UTF-8', 'replace')
|
||||
self.send_response_only(code, message)
|
||||
self.send_header("Content-Type", self.error_content_type)
|
||||
self.send_header('Content-Type', self.error_content_type)
|
||||
self.send_header('Content-Length', int(len(body)))
|
||||
self.end_headers()
|
||||
if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
|
||||
@ -157,13 +182,16 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
def forward_to_https_proxy(self):
|
||||
"Forward https request to upstream https proxy"
|
||||
logger.debug('Using Proxy - %s' % self.proxy)
|
||||
'''
|
||||
Forward https request to upstream https proxy
|
||||
'''
|
||||
logger.debug(f'Using Proxy - {self.proxy}')
|
||||
proxy_host, proxy_port = self.proxy.split('//')[1].split(':')
|
||||
server_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
server_conn.connect((proxy_host, int(proxy_port)))
|
||||
server_conn.send(('CONNECT %s HTTP/1.1\r\n\r\n' % self.path).encode('ascii'))
|
||||
server_conn.send((
|
||||
f'CONNECT {self.path} HTTP/1.1\r\n\r\n').encode('ascii'))
|
||||
server_conn.settimeout(0.1)
|
||||
datas = b''
|
||||
while True:
|
||||
@ -177,47 +205,66 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
break
|
||||
server_conn.setblocking(True)
|
||||
if b'200' in datas and b'established' in datas.lower():
|
||||
logger.info("%03d " % self.reqNum + Fore.CYAN + '[P] SSL Pass-Thru: https://%s/' % self.path)
|
||||
self.wfile.write(("HTTP/1.1 200 Connection established\r\n" +
|
||||
"Proxy-agent: %s\r\n\r\n" % self.version_string()).encode('ascii'))
|
||||
logger.info('{:03d} [P] SSL Pass-Thru: https://{}/'.format(
|
||||
self.reqNum, self.path))
|
||||
self.wfile.write(('HTTP/1.1 200 Connection established\r\n' +
|
||||
'Proxy-agent: {}\r\n\r\n'.format(
|
||||
self.version_string()).encode('ascii')))
|
||||
read_write(self.connection, server_conn)
|
||||
else:
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + 'Proxy %s failed.', self.proxy)
|
||||
logger.warning('{:03d} Proxy {} failed.'.format(
|
||||
self.reqNum, self.proxy))
|
||||
if datas:
|
||||
logger.debug(datas)
|
||||
self.wfile.write(datas)
|
||||
finally:
|
||||
# We don't maintain a connection reuse pool, so close the connection anyway
|
||||
# We don't maintain a connection reuse pool,
|
||||
# so close the connection anyway
|
||||
server_conn.close()
|
||||
|
||||
def forward_to_socks5_proxy(self):
|
||||
"Forward https request to upstream socks5 proxy"
|
||||
logger.warning(Fore.YELLOW + 'Socks5 proxy not implemented yet, please use https proxy')
|
||||
'''
|
||||
Forward https request to upstream socks5 proxy
|
||||
'''
|
||||
logger.warning('Socks5 proxy not implemented yet, '
|
||||
'please use https proxy')
|
||||
|
||||
def tunnel_traffic(self):
|
||||
"Tunnel traffic to remote host:port"
|
||||
logger.info("%03d " % self.reqNum + Fore.CYAN + '[D] SSL Pass-Thru: https://%s/' % self.path)
|
||||
'''
|
||||
Tunnel traffic to remote host:port
|
||||
'''
|
||||
logger.info('{:03d} [D] SSL Pass-Thru: https://{}/'.format(
|
||||
self.reqNum, self.path))
|
||||
server_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
server_conn.connect((self.host, int(self.port)))
|
||||
self.wfile.write(("HTTP/1.1 200 Connection established\r\n" +
|
||||
"Proxy-agent: %s\r\n" % self.version_string() +
|
||||
"\r\n").encode('ascii'))
|
||||
self.wfile.write(('HTTP/1.1 200 Connection established\r\n'
|
||||
+ 'Proxy-agent: {}\r\n'.format(
|
||||
self.version_string())
|
||||
+ '\r\n').encode('ascii'))
|
||||
read_write(self.connection, server_conn)
|
||||
except TimeoutError:
|
||||
self.wfile.write(b"HTTP/1.1 504 Gateway Timeout\r\n\r\n")
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + 'Timed Out: https://%s:%s/' % (self.host, self.port))
|
||||
self.wfile.write(b'HTTP/1.1 504 Gateway Timeout\r\n\r\n')
|
||||
logger.warning('{:03d} Timed Out: https://{}:{}/'.format(
|
||||
self.reqNum, self.host, self.port))
|
||||
except socket.gaierror as e:
|
||||
self.wfile.write(b"HTTP/1.1 503 Service Unavailable\r\n\r\n")
|
||||
logger.warning("%03d " % self.reqNum + Fore.YELLOW + '%s: https://%s:%s/' % (e, self.host, self.port))
|
||||
self.wfile.write(b'HTTP/1.1 503 Service Unavailable\r\n\r\n')
|
||||
logger.warning('{:03d} {}: https://{}:{}/'.format(
|
||||
self.reqNum, e, self.host, self.port))
|
||||
finally:
|
||||
# We don't maintain a connection reuse pool, so close the connection anyway
|
||||
# We don't maintain a connection reuse pool,
|
||||
# so close the connection anyway
|
||||
server_conn.close()
|
||||
|
||||
def ssl_get_response(self, conn):
|
||||
try:
|
||||
server_conn = ssl.wrap_socket(conn, cert_reqs=ssl.CERT_REQUIRED, ca_certs="cacert.pem", ssl_version=ssl.PROTOCOL_TLSv1)
|
||||
server_conn.sendall(('%s %s HTTP/1.1\r\n' % (self.command, self.path)).encode('ascii'))
|
||||
server_conn = ssl.wrap_socket(
|
||||
conn, cert_reqs=ssl.CERT_REQUIRED,
|
||||
ca_certs="cacert.pem",
|
||||
ssl_version=ssl.PROTOCOL_TLSv1)
|
||||
server_conn.sendall(
|
||||
('{} {} HTTP/1.1\r\n'.format(
|
||||
self.command, self.path)).encode('ascii'))
|
||||
server_conn.sendall(self.headers.as_bytes())
|
||||
if self.postdata:
|
||||
server_conn.sendall(self.postdata)
|
||||
@ -225,15 +272,19 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
data = server_conn.recv(4096)
|
||||
if data:
|
||||
self.wfile.write(data)
|
||||
else: break
|
||||
else:
|
||||
break
|
||||
except (ssl.SSLEOFError, ssl.SSLError) as e:
|
||||
logger.error(Fore.RED + Style.BRIGHT + "[SSLError]")
|
||||
self.send_error(417, message="Exception %s" % str(e.__class__), explain=str(e))
|
||||
logger.error("[SSLError]")
|
||||
self.send_error(417, message="Exception "
|
||||
+ str(e.__class__), explain=str(e))
|
||||
|
||||
def purge_headers(self, headers):
|
||||
"Remove hop-by-hop headers that shouldn't pass through a Proxy"
|
||||
for name in ["Connection", "Keep-Alive", "Upgrade",
|
||||
"Proxy-Connection", "Proxy-Authenticate"]:
|
||||
'''
|
||||
Remove hop-by-hop headers that shouldn't pass through a Proxy
|
||||
'''
|
||||
for name in ['Connection', 'Keep-Alive', 'Upgrade',
|
||||
'Proxy-Connection', 'Proxy-Authenticate']:
|
||||
del headers[name]
|
||||
|
||||
def purge_write_headers(self, headers):
|
||||
@ -241,7 +292,7 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
for key, value in headers.items():
|
||||
self.send_header(key, value)
|
||||
self.end_headers()
|
||||
|
||||
|
||||
def stream_to_client(self, response):
|
||||
bufsize = 1024 * 64
|
||||
need_chunked = 'Transfer-Encoding' in response.headers
|
||||
@ -253,34 +304,35 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(b'0\r\n\r\n')
|
||||
break
|
||||
if need_chunked:
|
||||
self.wfile.write(('%x\r\n' % len(data)).encode('ascii'))
|
||||
self.wfile.write((f'{len(data):x}\r\n').encode('ascii'))
|
||||
self.wfile.write(data)
|
||||
if need_chunked:
|
||||
self.wfile.write(b'\r\n')
|
||||
written += len(data)
|
||||
return written
|
||||
|
||||
def http_request_info(self):
|
||||
"""Return HTTP request information in bytes
|
||||
"""
|
||||
context = ["CLIENT VALUES:",
|
||||
"client_address = %s" % str(self.client_address),
|
||||
"requestline = %s" % self.requestline,
|
||||
"command = %s" % self.command,
|
||||
"path = %s" % self.path,
|
||||
"request_version = %s" % self.request_version,
|
||||
"",
|
||||
"SERVER VALUES:",
|
||||
"server_version = %s" % self.server_version,
|
||||
"sys_version = %s" % self.sys_version,
|
||||
"protocol_version = %s" % self.protocol_version,
|
||||
"",
|
||||
"HEADER RECEIVED:"]
|
||||
for name, value in sorted(self.headers.items()):
|
||||
context.append("%s = %s" % (name, value.rstrip()))
|
||||
|
||||
if self.command == "POST":
|
||||
context.append("\r\nPOST VALUES:")
|
||||
def http_request_info(self):
|
||||
'''
|
||||
Return HTTP request information in bytes
|
||||
'''
|
||||
context = ['CLIENT VALUES:',
|
||||
f'client_address = {self.client_address}',
|
||||
f'requestline = {self.requestline}',
|
||||
f'command = {self.command}',
|
||||
f'path = {self.path}',
|
||||
f'request_version = {self.request_version}',
|
||||
'',
|
||||
'SERVER VALUES:',
|
||||
'server_version = {self.server_version}',
|
||||
'sys_version = {self.sys_version}',
|
||||
'protocol_version = {self.protocol_version}',
|
||||
'',
|
||||
'HEADER RECEIVED:']
|
||||
for name, value in sorted(self.headers.items()):
|
||||
context.append(f'{name} = {value.rstrip()}')
|
||||
|
||||
if self.command == 'POST':
|
||||
context.append('\r\nPOST VALUES:')
|
||||
form = cgi.FieldStorage(fp=self.rfile,
|
||||
headers=self.headers,
|
||||
environ={'REQUEST_METHOD': 'POST'})
|
||||
@ -290,27 +342,28 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
# The field contains an uploaded file
|
||||
file_data = fielditem.file.read()
|
||||
file_len = len(file_data)
|
||||
context.append('Uploaded %s as "%s" (%d bytes)'
|
||||
% (field, fielditem.filename, file_len))
|
||||
context.append('Uploaded {} as "{}" ({} bytes)'.format(
|
||||
field, fielditem.filename, file_len))
|
||||
else:
|
||||
# Regular form value
|
||||
context.append("%s = %s" % (field, fielditem.value))
|
||||
|
||||
return("\r\n".join(context).encode('ascii'))
|
||||
context.append(f'{field} = {fielditem.value}')
|
||||
|
||||
return('\r\n'.join(context).encode('ascii'))
|
||||
|
||||
|
||||
def demo():
|
||||
PORT = 8000
|
||||
|
||||
class ProxyServer(ThreadingMixIn, HTTPServer):
|
||||
"""Handle requests in a separate thread."""
|
||||
'''Handle requests in a separate thread.'''
|
||||
pass
|
||||
|
||||
class RequestHandler(ProxyRequestHandler):
|
||||
"Displaying HTTP request information"
|
||||
server_version = "DemoProxy/0.1"
|
||||
'Displaying HTTP request information'
|
||||
server_version = 'DemoProxy/0.1'
|
||||
|
||||
def do_METHOD(self):
|
||||
"Universal method for GET, POST, HEAD, PUT and DELETE"
|
||||
'Universal method for GET, POST, HEAD, PUT and DELETE'
|
||||
message = self.http_request_info()
|
||||
self.send_response(200)
|
||||
# 'Content-Length' is important for HTTP/1.1
|
||||
@ -318,16 +371,18 @@ def demo():
|
||||
self.end_headers()
|
||||
self.wfile.write(message)
|
||||
|
||||
do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = do_OPTIONS = do_METHOD
|
||||
do_GET = do_POST = do_HEAD = do_METHOD
|
||||
do_PUT = do_DELETE = do_OPTIONS = do_METHOD
|
||||
|
||||
print('%s serving now, <Ctrl-C> to stop ...' % RequestHandler.server_version)
|
||||
print('Listen Addr : localhost:%s' % PORT)
|
||||
print("-" * 10)
|
||||
print(RequestHandler.server_version, 'serving now, <Ctrl-C> to stop ...')
|
||||
print(f'Listen Addr : localhost:{PORT}')
|
||||
print('-' * 10)
|
||||
server = ProxyServer(('', PORT), RequestHandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
demo()
|
||||
except KeyboardInterrupt:
|
||||
print("Quitting...")
|
||||
print('Quitting...')
|
Loading…
Reference in New Issue
Block a user