diff --git a/libpathod/pathoc.py b/libpathod/pathoc.py index a2d89aafd..ae9edaf09 100644 --- a/libpathod/pathoc.py +++ b/libpathod/pathoc.py @@ -7,13 +7,14 @@ class PathocError(Exception): pass class Pathoc(tcp.TCPClient): - def __init__(self, host, port, ssl=None, sni=None): + def __init__(self, host, port, ssl=None, sni=None, clientcert=None): tcp.TCPClient.__init__(self, host, port) self.settings = dict( staticdir = os.getcwd(), unconstrained_file_access = True, ) self.ssl, self.sni = ssl, sni + self.clientcert = clientcert def http_connect(self, connect_to, wfile, rfile): wfile.write( @@ -34,7 +35,7 @@ class Pathoc(tcp.TCPClient): self.http_connect(connect_to, self.wfile, self.rfile) if self.ssl: try: - self.convert_to_ssl(sni=self.sni) + self.convert_to_ssl(sni=self.sni, clientcert=self.clientcert) except tcp.NetLibError, v: raise PathocError(str(v)) diff --git a/libpathod/pathod.py b/libpathod/pathod.py index ce64acf99..ac56619be 100644 --- a/libpathod/pathod.py +++ b/libpathod/pathod.py @@ -96,6 +96,17 @@ class PathodHandler(tcp.BaseHandler): self.info(s) return False, dict(type = "error", msg = s) + clientcert = None + if self.clientcert: + clientcert = dict( + cn = self.clientcert.cn, + subject = self.clientcert.subject, + serial = self.clientcert.serial, + notbefore = self.clientcert.notbefore.isoformat(), + notafter = self.clientcert.notafter.isoformat(), + keyinfo = self.clientcert.keyinfo, + ) + request_log = dict( path = path, method = method, @@ -103,6 +114,7 @@ class PathodHandler(tcp.BaseHandler): httpversion = httpversion, sni = self.sni, remote_address = self.client_address, + clientcert = clientcert ) try: diff --git a/libpathod/templates/log.html b/libpathod/templates/log.html index 22747e0ec..19468d661 100644 --- a/libpathod/templates/log.html +++ b/libpathod/templates/log.html @@ -17,9 +17,13 @@ {% for i in log %} - {{ i["id"] }} - {{ i["request"]["method"] }} - {{ i["request"]["path"] }} + {% if i["type"] == 'error' %} + ERROR: {{ i["msg"] }} + {% else %} + {{ i["id"] }} + {{ i["request"]["method"] }} + {{ i["request"]["path"] }} + {% endif %} {% endfor %} diff --git a/pathoc b/pathoc index 527b9a22d..aa3d5beda 100755 --- a/pathoc +++ b/pathoc @@ -22,10 +22,6 @@ if __name__ == "__main__": metavar = "HOST:PORT", help="Issue an HTTP CONNECT to connect to the specified host." ) - parser.add_argument( - "-i", dest="sni", type=str, default=False, - help="SSL Server Name Indication" - ) parser.add_argument( "-n", dest='repeat', default=1, type=int, metavar="N", help='Repeat requests N times' @@ -34,10 +30,6 @@ if __name__ == "__main__": "-p", dest="port", type=int, default=None, help="Port. Defaults to 80, or 443 if SSL is active" ) - parser.add_argument( - "-s", dest="ssl", action="store_true", default=False, - help="Connect with SSL" - ) parser.add_argument( "-t", dest="timeout", type=int, default=None, help="Connection timeout" @@ -51,6 +43,24 @@ if __name__ == "__main__": help='Request specification' ) + + group = parser.add_argument_group( + 'SSL', + ) + group.add_argument( + "-s", dest="ssl", action="store_true", default=False, + help="Connect with SSL" + ) + group.add_argument( + "-C", dest="clientcert", type=str, default=False, + help="Path to a file containing client certificate and private key" + ) + group.add_argument( + "-i", dest="sni", type=str, default=False, + help="SSL Server Name Indication" + ) + + group = parser.add_argument_group( 'Controlling Output', """ @@ -59,7 +69,7 @@ if __name__ == "__main__": """ ) group.add_argument( - "-C", dest="ignorecodes", type=str, default="", + "-I", dest="ignorecodes", type=str, default="", help="Comma-separated list of response codes to ignore" ) group.add_argument( @@ -113,10 +123,10 @@ if __name__ == "__main__": try: for i in range(args.repeat): - p = pathoc.Pathoc(args.host, port, args.ssl, args.sni) + p = pathoc.Pathoc(args.host, port, ssl=args.ssl, sni=args.sni, clientcert=args.clientcert) try: p.connect(connect_to) - except tcp.NetLibError, v: + except (tcp.NetLibError, pathoc.PathocError), v: print >> sys.stderr, str(v) sys.exit(1) if args.timeout: diff --git a/test/data/clientcert/.gitignore b/test/data/clientcert/.gitignore new file mode 100644 index 000000000..07bc53d25 --- /dev/null +++ b/test/data/clientcert/.gitignore @@ -0,0 +1,3 @@ +client.crt +client.key +client.req diff --git a/test/data/clientcert/client.cnf b/test/data/clientcert/client.cnf new file mode 100644 index 000000000..5046a9440 --- /dev/null +++ b/test/data/clientcert/client.cnf @@ -0,0 +1,5 @@ +[ ssl_client ] +basicConstraints = CA:FALSE +nsCertType = client +keyUsage = digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth diff --git a/test/data/clientcert/client.pem b/test/data/clientcert/client.pem new file mode 100644 index 000000000..4927bca28 --- /dev/null +++ b/test/data/clientcert/client.pem @@ -0,0 +1,42 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAzCpoRjSTfIN24kkNap/GYmP9zVWj0Gk8R5BB/PvvN0OB1Zk0 +EEYPsWCcuhEdK0ehiDZX030doF0DOncKKa6mop/d0x2o+ts42peDhZM6JNUrm6d+ +ZWQVtio33mpp77UMhR093vaA+ExDnmE26kBTVijJ1+fRAVDXG/cmQINEri91Kk/G +3YJ5e45UrohGI5seBZ4vV0xbHtmczFRhYFlGOvYsoIe4Lvz/eFS2pIrTIpYQ2VM/ +SQQl+JFy+NlQRsWG2NrxtKOzMnnDE7YN4I3z5D5eZFo1EtwZ48LNCeSwrEOdfuzP +G5q5qbs5KpE/x85H9umuRwSCIArbMwBYV8a8JwIDAQABAoIBAFE3FV/IDltbmHEP +iky93hbJm+6QgKepFReKpRVTyqb7LaygUvueQyPWQMIriKTsy675nxo8DQr7tQsO +y3YlSZgra/xNMikIB6e82c7K8DgyrDQw/rCqjZB3Xt4VCqsWJDLXnQMSn98lx0g7 +d7Lbf8soUpKWXqfdVpSDTi4fibSX6kshXyfSTpcz4AdoncEpViUfU1xkEEmZrjT8 +1GcCsDC41xdNmzCpqRuZX7DKSFRoB+0hUzsC1oiqM7FD5kixonRd4F5PbRXImIzt +6YCsT2okxTA04jX7yByis7LlOLTlkmLtKQYuc3erOFvwx89s4vW+AeFei+GGNitn +tHfSwbECgYEA7SzV+nN62hAERHlg8cEQT4TxnsWvbronYWcc/ev44eHSPDWL5tPi +GHfSbW6YAq5Wa0I9jMWfXyhOYEC3MZTC5EEeLOB71qVrTwcy/sY66rOrcgjFI76Q +5JFHQ4wy3SWU50KxE0oWJO9LIowprG+pW1vzqC3VF0T7q0FqESrY4LUCgYEA3F7Z +80ndnCUlooJAb+Hfotv7peFf1o6+m1PTRcz1lLnVt5R5lXj86kn+tXEpYZo1RiGR +2rE2N0seeznWCooakHcsBN7/qmFIhhooJNF7yW+JP2I4P2UV5+tJ+8bcs/voUkQD +1x+rGOuMn8nvHBd2+Vharft8eGL2mgooPVI2XusCgYEAlMZpO3+w8pTVeHaDP2MR +7i/AuQ3cbCLNjSX3Y7jgGCFllWspZRRIYXzYPNkA9b2SbBnTLjjRLgnEkFBIGgvs +7O2EFjaCuDRvydUEQhjq4ErwIsopj7B8h0QyZcbOKTbn3uFQ3n68wVJx2Sv/ADHT +FIHrp/WIE96r19Niy34LKXkCgYB2W59VsuOKnMz01l5DeR5C+0HSWxS9SReIl2IO +yEFSKullWyJeLIgyUaGy0990430feKI8whcrZXYumuah7IDN/KOwzhCk8vEfzWao +N7bzfqtJVrh9HA7C7DVlO+6H4JFrtcoWPZUIomJ549w/yz6EN3ckoMC+a/Ck1TW9 +ka1QFwKBgQCywG6TrZz0UmOjyLQZ+8Q4uvZklSW5NAKBkNnyuQ2kd5rzyYgMPE8C +Er8T88fdVIKvkhDyHhwcI7n58xE5Gr7wkwsrk/Hbd9/ZB2GgAPY3cATskK1v1McU +YeX38CU0fUS4aoy26hWQXkViB47IGQ3jWo3ZCtzIJl8DI9/RsBWTnw== +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICYDCCAckCAQEwDQYJKoZIhvcNAQEFBQAwKDESMBAGA1UEAxMJbWl0bXByb3h5 +MRIwEAYDVQQKEwltaXRtcHJveHkwHhcNMTMwMTIwMDEwODEzWhcNMTUxMDE3MDEw +ODEzWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UE +ChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAzCpoRjSTfIN24kkNap/GYmP9zVWj0Gk8R5BB/PvvN0OB1Zk0 +EEYPsWCcuhEdK0ehiDZX030doF0DOncKKa6mop/d0x2o+ts42peDhZM6JNUrm6d+ +ZWQVtio33mpp77UMhR093vaA+ExDnmE26kBTVijJ1+fRAVDXG/cmQINEri91Kk/G +3YJ5e45UrohGI5seBZ4vV0xbHtmczFRhYFlGOvYsoIe4Lvz/eFS2pIrTIpYQ2VM/ +SQQl+JFy+NlQRsWG2NrxtKOzMnnDE7YN4I3z5D5eZFo1EtwZ48LNCeSwrEOdfuzP +G5q5qbs5KpE/x85H9umuRwSCIArbMwBYV8a8JwIDAQABMA0GCSqGSIb3DQEBBQUA +A4GBAFvI+cd47B85PQ970n2dU/PlA2/Hb1ldrrXh2guR4hX6vYx/uuk5yRI/n0Rd +KOXJ3czO0bd2Fpe3ZoNpkW0pOSDej/Q+58ScuJd0gWCT/Sh1eRk6ZdC0kusOuWoY +bPOPMkG45LPgUMFOnZEsfJP6P5mZIxlbCvSMFC25nPHWlct7 +-----END CERTIFICATE----- diff --git a/test/data/clientcert/make b/test/data/clientcert/make new file mode 100755 index 000000000..d1caea813 --- /dev/null +++ b/test/data/clientcert/make @@ -0,0 +1,8 @@ +#!/bin/sh + +openssl genrsa -out client.key 2048 +openssl req -key client.key -new -out client.req +openssl x509 -req -days 365 -in client.req -signkey client.key -out client.crt -extfile client.cnf -extensions ssl_client +openssl x509 -req -days 1000 -in client.req -CA ~/.mitmproxy/mitmproxy-ca.pem -CAkey ~/.mitmproxy/mitmproxy-ca.pem -set_serial 00001 -out client.crt -extensions ssl_client +cat client.key client.crt > client.pem +openssl x509 -text -noout -in client.pem diff --git a/test/test_pathoc.py b/test/test_pathoc.py index 38d3754a0..2c86df118 100644 --- a/test/test_pathoc.py +++ b/test/test_pathoc.py @@ -45,6 +45,19 @@ class TestDaemonSSL(_TestDaemon): d = json.loads(content) assert d["log"][0]["request"]["sni"] == "foobar.com" + def test_clientcert(self): + c = pathoc.Pathoc( + "127.0.0.1", + self.d.port, + ssl = True, + clientcert = tutils.test_data.path("data/clientcert/client.pem") + ) + c.connect() + c.request("get:/p/200") + _, _, _, _, content = c.request("get:/api/log") + d = json.loads(content) + assert d["log"][0]["request"]["clientcert"]["keyinfo"] + class TestDaemon(_TestDaemon): ssl = False