diff options
| author | Maximilian Hils <git@maximilianhils.com> | 2016-09-22 03:06:39 -0700 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-09-22 03:06:39 -0700 | 
| commit | 9142da1a7d6e87147a8ed92ae027d55ac935eb85 (patch) | |
| tree | fe2eedd3367d4aac98f0e4b37ba69ddf172edc50 | |
| parent | d585236a820071938669d74b0069aab26f86ed5d (diff) | |
| parent | 77868434e7dcaf42a7b48f3c81e04d1a7e993240 (diff) | |
| download | mitmproxy-9142da1a7d6e87147a8ed92ae027d55ac935eb85.tar.gz mitmproxy-9142da1a7d6e87147a8ed92ae027d55ac935eb85.tar.bz2 mitmproxy-9142da1a7d6e87147a8ed92ae027d55ac935eb85.zip  | |
Merge pull request #1566 from mhils/issue-1546
add websocket on/off switch, improve logging (fix #1546, fix #1547)
| -rw-r--r-- | mitmproxy/cmdline.py | 14 | ||||
| -rw-r--r-- | mitmproxy/options.py | 2 | ||||
| -rw-r--r-- | mitmproxy/protocol/http.py | 45 | ||||
| -rw-r--r-- | mitmproxy/protocol/websockets.py | 24 | ||||
| -rw-r--r-- | mitmproxy/proxy/config.py | 5 | ||||
| -rw-r--r-- | mitmproxy/proxy/root_context.py | 19 | ||||
| -rw-r--r-- | test/mitmproxy/protocol/test_websockets.py | 3 | 
7 files changed, 66 insertions, 46 deletions
diff --git a/mitmproxy/cmdline.py b/mitmproxy/cmdline.py index 9fb4a561..ff431909 100644 --- a/mitmproxy/cmdline.py +++ b/mitmproxy/cmdline.py @@ -1,14 +1,12 @@  from __future__ import absolute_import, print_function, division +import configargparse  import os  import re - -import configargparse -  from mitmproxy import exceptions  from mitmproxy import filt -from mitmproxy import platform  from mitmproxy import options +from mitmproxy import platform  from netlib import human  from netlib import tcp  from netlib import version @@ -257,6 +255,7 @@ def get_common_options(args):          no_upstream_cert = args.no_upstream_cert,          spoof_source_address = args.spoof_source_address,          rawtcp = args.rawtcp, +        websockets = args.websockets,          upstream_server = upstream_server,          upstream_auth = args.upstream_auth,          ssl_version_client = args.ssl_version_client, @@ -475,6 +474,13 @@ def proxy_options(parser):                          "Disabled by default. "                          "Default value will change in a future version."                          ) +    websockets = group.add_mutually_exclusive_group() +    websockets.add_argument("--websockets", action="store_true", dest="websockets") +    websockets.add_argument("--no-websockets", action="store_false", dest="websockets", +                            help="Explicitly enable/disable experimental WebSocket support. " +                                 "Disabled by default as messages are only printed to the event log and not retained. " +                                 "Default value will change in a future version." +                            )      group.add_argument(          "--spoof-source-address",          action="store_true", dest="spoof_source_address", diff --git a/mitmproxy/options.py b/mitmproxy/options.py index ba4ed0c7..ccf06f75 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -70,6 +70,7 @@ class Options(optmanager.OptManager):              mode = "regular",  # type: str              no_upstream_cert = False,  # type: bool              rawtcp = False,  # type: bool +            websockets = False,  # type: bool              spoof_source_address = False,  # type: bool              upstream_server = "",  # type: str              upstream_auth = "",  # type: str @@ -129,6 +130,7 @@ class Options(optmanager.OptManager):          self.mode = mode          self.no_upstream_cert = no_upstream_cert          self.rawtcp = rawtcp +        self.websockets = websockets          self.spoof_source_address = spoof_source_address          self.upstream_server = upstream_server          self.upstream_auth = upstream_auth diff --git a/mitmproxy/protocol/http.py b/mitmproxy/protocol/http.py index 1632e66f..894ae465 100644 --- a/mitmproxy/protocol/http.py +++ b/mitmproxy/protocol/http.py @@ -1,17 +1,15 @@  from __future__ import absolute_import, print_function, division -import time -import sys -import traceback -  import h2.exceptions +import netlib.exceptions  import six - +import sys +import time +import traceback  from mitmproxy import exceptions  from mitmproxy import models  from mitmproxy.protocol import base - -import netlib.exceptions +from mitmproxy.protocol import websockets as pwebsockets  from netlib import http  from netlib import tcp  from netlib import websockets @@ -193,8 +191,8 @@ class HttpLayer(base.Layer):              try:                  if websockets.check_handshake(request.headers) and websockets.check_client_version(request.headers): -                    # we only support RFC6455 with WebSockets version 13 -                    # allow inline scripts to manupulate the client handshake +                    # We only support RFC6455 with WebSockets version 13 +                    # allow inline scripts to manipulate the client handshake                      self.channel.ask("websockets_handshake", flow)                  if not flow.response: @@ -217,12 +215,8 @@ class HttpLayer(base.Layer):                      return                  # Handle 101 Switching Protocols -                # It may be useful to pass additional args (such as the upgrade header) -                # to next_layer in the future                  if flow.response.status_code == 101: -                    layer = self.ctx.next_layer(self, flow) -                    layer() -                    return +                    return self.handle_101_switching_protocols(flow)                  # Upstream Proxy Mode: Handle CONNECT                  if flow.request.first_line_format == "authority" and flow.response.status_code == 200: @@ -438,3 +432,26 @@ class HttpLayer(base.Layer):                      ))                  return False          return True + +    def handle_101_switching_protocols(self, flow): +        """ +        Handle a successful HTTP 101 Switching Protocols Response, received after e.g. a WebSocket upgrade request. +        """ +        # Check for WebSockets handshake +        is_websockets = ( +            flow and +            websockets.check_handshake(flow.request.headers) and +            websockets.check_handshake(flow.response.headers) +        ) +        if is_websockets and not self.config.options.websockets: +            self.log( +                "Client requested WebSocket connection, but the protocol is currently disabled in mitmproxy.", +                "info" +            ) + +        if is_websockets and self.config.options.websockets: +            layer = pwebsockets.WebSocketsLayer(self, flow) +        else: +            layer = self.ctx.next_layer(self) + +        layer() diff --git a/mitmproxy/protocol/websockets.py b/mitmproxy/protocol/websockets.py index f15a38ef..7ac386f1 100644 --- a/mitmproxy/protocol/websockets.py +++ b/mitmproxy/protocol/websockets.py @@ -1,14 +1,12 @@  from __future__ import absolute_import, print_function, division +import netlib.exceptions  import socket  import struct -  from OpenSSL import SSL -  from mitmproxy import exceptions  from mitmproxy.protocol import base - -import netlib.exceptions +from netlib import strutils  from netlib import tcp  from netlib import websockets @@ -49,22 +47,28 @@ class WebSocketsLayer(base.Layer):          self.server_extensions = websockets.get_extensions(self._flow.response.headers)      def _handle_frame(self, frame, source_conn, other_conn, is_server): +        sender = "server" if is_server else "client"          self.log( -            "WebSockets Frame received from {}".format("server" if is_server else "client"), +            "WebSockets Frame received from {}".format(sender),              "debug",              [repr(frame)]          )          if frame.header.opcode & 0x8 == 0: +            self.log( +                "{direction} websocket {direction} {server}".format( +                    server=repr(self.server_conn.address), +                    direction="<-" if is_server else "->", +                ), +                "info", +                strutils.bytes_to_escaped_str(frame.payload, keep_spacing=True).splitlines() +            )              # forward the data frame to the other side              other_conn.send(bytes(frame)) -            self.log("WebSockets frame received by {}: {}".format(is_server, frame), "debug")          elif frame.header.opcode in (websockets.OPCODE.PING, websockets.OPCODE.PONG):              # just forward the ping/pong to the other side              other_conn.send(bytes(frame))          elif frame.header.opcode == websockets.OPCODE.CLOSE: -            other_conn.send(bytes(frame)) -              code = '(status code missing)'              msg = None              reason = '(message missing)' @@ -73,11 +77,13 @@ class WebSocketsLayer(base.Layer):                  msg = websockets.CLOSE_REASON.get_name(code, default='unknown status code')              if len(frame.payload) > 2:                  reason = frame.payload[2:] -            self.log("WebSockets connection closed: {} {}, {}".format(code, msg, reason), "info") +            self.log("WebSockets connection closed by {}: {} {}, {}".format(sender, code, msg, reason), "info") +            other_conn.send(bytes(frame))              # close the connection              return False          else: +            self.log("Unknown WebSockets frame received from {}".format(sender), "info", [repr(frame)])              # unknown frame - just forward it              other_conn.send(bytes(frame)) diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index 2cf8410a..df4dd4c8 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -10,6 +10,7 @@ import six  from OpenSSL import SSL, crypto  from mitmproxy import exceptions +from mitmproxy import options as moptions  # noqa  from netlib import certutils  from netlib import tcp  from netlib.http import authentication @@ -71,7 +72,7 @@ def parse_upstream_auth(auth):  class ProxyConfig:      def __init__(self, options): -        self.options = options +        self.options = options  # type: moptions.Options          self.authenticator = None          self.check_ignore = None @@ -83,7 +84,7 @@ class ProxyConfig:          options.changed.connect(self.configure)      def configure(self, options, updated): -        # type: (mitmproxy.options.Options, Any) -> None +        # type: (moptions.Options, Any) -> None          if options.add_upstream_certs_to_client_chain and not options.ssl_insecure:              raise exceptions.OptionsError(                  "The verify-upstream-cert requires certificate verification to be disabled. " diff --git a/mitmproxy/proxy/root_context.py b/mitmproxy/proxy/root_context.py index 95611362..81dd625c 100644 --- a/mitmproxy/proxy/root_context.py +++ b/mitmproxy/proxy/root_context.py @@ -4,7 +4,6 @@ import sys  import six -from netlib import websockets  import netlib.exceptions  from mitmproxy import exceptions  from mitmproxy import protocol @@ -33,7 +32,7 @@ class RootContext(object):          self.channel = channel          self.config = config -    def next_layer(self, top_layer, flow=None): +    def next_layer(self, top_layer):          """          This function determines the next layer in the protocol stack. @@ -43,22 +42,10 @@ class RootContext(object):          Returns:              The next layer          """ -        layer = self._next_layer(top_layer, flow) +        layer = self._next_layer(top_layer)          return self.channel.ask("next_layer", layer) -    def _next_layer(self, top_layer, flow): -        if flow is not None: -            # We already have a flow, try to derive the next information from it - -            # Check for WebSockets handshake -            is_websockets = ( -                flow and -                websockets.check_handshake(flow.request.headers) and -                websockets.check_handshake(flow.response.headers) -            ) -            if isinstance(top_layer, protocol.HttpLayer) and is_websockets: -                return protocol.WebSocketsLayer(top_layer, flow) - +    def _next_layer(self, top_layer):          try:              d = top_layer.client_conn.rfile.peek(3)          except netlib.exceptions.TcpException as e: diff --git a/test/mitmproxy/protocol/test_websockets.py b/test/mitmproxy/protocol/test_websockets.py index e7e2684f..e2361d89 100644 --- a/test/mitmproxy/protocol/test_websockets.py +++ b/test/mitmproxy/protocol/test_websockets.py @@ -65,7 +65,8 @@ class _WebSocketsTestBase(object):          opts = options.Options(              listen_port=0,              no_upstream_cert=False, -            ssl_insecure=True +            ssl_insecure=True, +            websockets=True,          )          opts.cadir = os.path.join(tempfile.gettempdir(), "mitmproxy")          return opts  | 
