diff options
-rw-r--r-- | mitmproxy/console/window.py | 2 | ||||
-rw-r--r-- | mitmproxy/protocol/http2.py | 76 | ||||
-rw-r--r-- | mitmproxy/protocol/tls.py | 34 | ||||
-rw-r--r-- | mitmproxy/proxy/config.py | 2 | ||||
-rw-r--r-- | test/mitmproxy/test_protocol_http2.py | 6 | ||||
-rw-r--r-- | test/mitmproxy/test_proxy.py | 26 | ||||
-rw-r--r-- | test/mitmproxy/test_server.py | 5 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/ui/flowSpec.js | 76 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/utils/listSpec.js | 1 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ShowFullContentButton.jsx | 2 | ||||
-rw-r--r-- | web/src/js/ducks/ui/flow.js | 21 |
11 files changed, 167 insertions, 84 deletions
diff --git a/mitmproxy/console/window.py b/mitmproxy/console/window.py index b24718be..35593643 100644 --- a/mitmproxy/console/window.py +++ b/mitmproxy/console/window.py @@ -27,7 +27,7 @@ class Window(urwid.Frame): if not k: if args[1] == "mouse drag": signals.status_message.send( - message = "Hold down shift, alt or ctrl to select text.", + message = "Hold down fn, shift, alt or ctrl to select text or use the --no-mouse parameter.", expire = 1 ) elif args[1] == "mouse press" and args[2] == 4: diff --git a/mitmproxy/protocol/http2.py b/mitmproxy/protocol/http2.py index eb5586cb..0e42d619 100644 --- a/mitmproxy/protocol/http2.py +++ b/mitmproxy/protocol/http2.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, print_function, division import threading import time import traceback +import functools import h2.exceptions import six @@ -54,21 +55,18 @@ class SafeH2Connection(connection.H2Connection): self.update_settings(new_settings) self.conn.send(self.data_to_send()) - def safe_send_headers(self, is_zombie, stream_id, headers, **kwargs): + def safe_send_headers(self, raise_zombie, stream_id, headers, **kwargs): with self.lock: - if is_zombie(): # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") + raise_zombie() self.send_headers(stream_id, headers.fields, **kwargs) self.conn.send(self.data_to_send()) - def safe_send_body(self, is_zombie, stream_id, chunks): + def safe_send_body(self, raise_zombie, stream_id, chunks): for chunk in chunks: position = 0 while position < len(chunk): self.lock.acquire() - if is_zombie(): # pragma: no cover - self.lock.release() - raise exceptions.Http2ProtocolException("Zombie Stream") + raise_zombie(self.lock.release) max_outbound_frame_size = self.max_outbound_frame_size frame_chunk = chunk[position:position + max_outbound_frame_size] if self.local_flow_control_window(stream_id) < len(frame_chunk): @@ -84,8 +82,7 @@ class SafeH2Connection(connection.H2Connection): self.lock.release() position += max_outbound_frame_size with self.lock: - if is_zombie(): # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") + raise_zombie() self.end_stream(stream_id) self.conn.send(self.data_to_send()) @@ -344,6 +341,17 @@ class Http2Layer(base.Layer): self._kill_all_streams() +def detect_zombie_stream(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + self.raise_zombie() + result = func(self, *args, **kwargs) + self.raise_zombie() + return result + + return wrapper + + class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread): def __init__(self, ctx, stream_id, request_headers): @@ -412,15 +420,16 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) def queued_data_length(self, v): self.request_queued_data_length = v - def is_zombie(self): - return self.zombie is not None + def raise_zombie(self, pre_command=None): + if self.zombie is not None: + if pre_command is not None: + pre_command() + raise exceptions.Http2ProtocolException("Zombie Stream") + @detect_zombie_stream def read_request(self): self.request_data_finished.wait() - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") - data = [] while self.request_data_queue.qsize() > 0: data.append(self.request_data_queue.get()) @@ -445,15 +454,14 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) def read_request_body(self, request): # pragma: no cover raise NotImplementedError() + @detect_zombie_stream def send_request(self, message): if self.pushed: # nothing to do here return while True: - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") - + self.raise_zombie() self.server_conn.h2.lock.acquire() max_streams = self.server_conn.h2.remote_settings.max_concurrent_streams @@ -467,8 +475,7 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) break # We must not assign a stream id if we are already a zombie. - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") + self.raise_zombie() self.server_stream_id = self.server_conn.h2.get_next_available_stream_id() self.server_to_client_stream_ids[self.server_stream_id] = self.client_stream_id @@ -490,7 +497,7 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) try: self.server_conn.h2.safe_send_headers( - self.is_zombie, + self.raise_zombie, self.server_stream_id, headers, end_stream=self.no_body, @@ -505,19 +512,16 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) if not self.no_body: self.server_conn.h2.safe_send_body( - self.is_zombie, + self.raise_zombie, self.server_stream_id, [message.body] ) - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") - + @detect_zombie_stream def read_response_headers(self): self.response_arrived.wait() - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") + self.raise_zombie() status_code = int(self.response_headers.get(':status', 502)) headers = self.response_headers.copy() @@ -533,6 +537,7 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) timestamp_end=self.timestamp_end, ) + @detect_zombie_stream def read_response_body(self, request, response): while True: try: @@ -540,14 +545,13 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) except queue.Empty: # pragma: no cover pass if self.response_data_finished.is_set(): - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") + self.raise_zombie() while self.response_data_queue.qsize() > 0: yield self.response_data_queue.get() break - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") + self.raise_zombie() + @detect_zombie_stream def send_response_headers(self, response): headers = response.headers.copy() headers.insert(0, ":status", str(response.status_code)) @@ -556,21 +560,21 @@ class Http2SingleStreamLayer(http._HttpTransmissionLayer, basethread.BaseThread) del headers[forbidden_header] with self.client_conn.h2.lock: self.client_conn.h2.safe_send_headers( - self.is_zombie, + self.raise_zombie, self.client_stream_id, headers ) - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") + @detect_zombie_stream def send_response_body(self, _response, chunks): self.client_conn.h2.safe_send_body( - self.is_zombie, + self.raise_zombie, self.client_stream_id, chunks ) - if self.zombie: # pragma: no cover - raise exceptions.Http2ProtocolException("Zombie Stream") + + def __call__(self): + raise EnvironmentError('Http2SingleStreamLayer must be run as thread') def run(self): layer = http.HttpLayer(self, self.mode) diff --git a/mitmproxy/protocol/tls.py b/mitmproxy/protocol/tls.py index d08e2e32..e41a9af0 100644 --- a/mitmproxy/protocol/tls.py +++ b/mitmproxy/protocol/tls.py @@ -369,8 +369,10 @@ class TlsLayer(base.Layer): not self.config.options.no_upstream_cert and ( self.config.options.add_upstream_certs_to_client_chain or - self._client_hello.alpn_protocols or - not self._client_hello.sni + self._client_tls and ( + self._client_hello.alpn_protocols or + not self._client_hello.sni + ) ) ) establish_server_tls_now = ( @@ -434,7 +436,7 @@ class TlsLayer(base.Layer): if self._custom_server_sni is False: return None else: - return self._custom_server_sni or self._client_hello.sni + return self._custom_server_sni or self._client_hello and self._client_hello.sni @property def alpn_for_client_connection(self): @@ -509,21 +511,18 @@ class TlsLayer(base.Layer): def _establish_tls_with_server(self): self.log("Establish TLS with server", "debug") try: - # We only support http/1.1 and h2. - # If the server only supports spdy (next to http/1.1), it may select that - # and mitmproxy would enter TCP passthrough mode, which we want to avoid. - def deprecated_http2_variant(x): - return x.startswith(b"h2-") or x.startswith(b"spdy") - - if self._client_hello.alpn_protocols: - alpn = [x for x in self._client_hello.alpn_protocols if not deprecated_http2_variant(x)] - else: - alpn = None - if alpn and b"h2" in alpn and not self.config.options.http2: - alpn.remove(b"h2") + alpn = None + if self._client_tls: + if self._client_hello.alpn_protocols: + # We only support http/1.1 and h2. + # If the server only supports spdy (next to http/1.1), it may select that + # and mitmproxy would enter TCP passthrough mode, which we want to avoid. + alpn = [x for x in self._client_hello.alpn_protocols if not (x.startswith(b"h2-") or x.startswith(b"spdy"))] + if alpn and b"h2" in alpn and not self.config.options.http2: + alpn.remove(b"h2") ciphers_server = self.config.options.ciphers_server - if not ciphers_server: + if not ciphers_server and self._client_tls: ciphers_server = [] for id in self._client_hello.cipher_suites: if id in CIPHER_ID_NAME_MAP.keys(): @@ -562,7 +561,8 @@ class TlsLayer(base.Layer): sys.exc_info()[2] ) - self.log("ALPN selected by server: %s" % self.alpn_for_client_connection, "debug") + proto = self.alpn_for_client_connection.decode() if self.alpn_for_client_connection else '-' + self.log("ALPN selected by server: {}".format(proto), "debug") def _find_cert(self): """ diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index 26f6a236..2cf8410a 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -188,4 +188,4 @@ class ProxyConfig: self.authenticator = authentication.BasicProxyAuth( password_manager, "mitmproxy" - )
\ No newline at end of file + ) diff --git a/test/mitmproxy/test_protocol_http2.py b/test/mitmproxy/test_protocol_http2.py index f0fa9a40..873c89c3 100644 --- a/test/mitmproxy/test_protocol_http2.py +++ b/test/mitmproxy/test_protocol_http2.py @@ -849,15 +849,15 @@ class TestMaxConcurrentStreams(_Http2Test): def test_max_concurrent_streams(self): client, h2_conn = self._setup_connection() new_streams = [1, 3, 5, 7, 9, 11] - for id in new_streams: + for stream_id in new_streams: # this will exceed MAX_CONCURRENT_STREAMS on the server connection # and cause mitmproxy to throttle stream creation to the server - self._send_request(client.wfile, h2_conn, stream_id=id, headers=[ + self._send_request(client.wfile, h2_conn, stream_id=stream_id, headers=[ (':authority', "127.0.0.1:{}".format(self.server.server.address.port)), (':method', 'GET'), (':scheme', 'https'), (':path', '/'), - ('X-Stream-ID', str(id)), + ('X-Stream-ID', str(stream_id)), ]) ended_streams = 0 diff --git a/test/mitmproxy/test_proxy.py b/test/mitmproxy/test_proxy.py index 84838018..f7c64e50 100644 --- a/test/mitmproxy/test_proxy.py +++ b/test/mitmproxy/test_proxy.py @@ -85,22 +85,22 @@ class TestProcessProxyOptions: @mock.patch("mitmproxy.platform.resolver") def test_modes(self, _): - # self.assert_noerr("-R", "http://localhost") - # self.assert_err("expected one argument", "-R") - # self.assert_err("Invalid server specification", "-R", "reverse") - # - # self.assert_noerr("-T") - # - # self.assert_noerr("-U", "http://localhost") - # self.assert_err("expected one argument", "-U") - # self.assert_err("Invalid server specification", "-U", "upstream") - # - # self.assert_noerr("--upstream-auth", "test:test") - # self.assert_err("expected one argument", "--upstream-auth") + self.assert_noerr("-R", "http://localhost") + self.assert_err("expected one argument", "-R") + self.assert_err("Invalid server specification", "-R", "reverse") + + self.assert_noerr("-T") + + self.assert_noerr("-U", "http://localhost") + self.assert_err("expected one argument", "-U") + self.assert_err("Invalid server specification", "-U", "upstream") + + self.assert_noerr("--upstream-auth", "test:test") + self.assert_err("expected one argument", "--upstream-auth") self.assert_err( "Invalid upstream auth specification", "--upstream-auth", "test" ) - # self.assert_err("mutually exclusive", "-R", "http://localhost", "-T") + self.assert_err("mutually exclusive", "-R", "http://localhost", "-T") def test_socks_auth(self): self.assert_err( diff --git a/test/mitmproxy/test_server.py b/test/mitmproxy/test_server.py index 78a97dc3..e0a8da47 100644 --- a/test/mitmproxy/test_server.py +++ b/test/mitmproxy/test_server.py @@ -472,6 +472,11 @@ class TestReverse(tservers.ReverseProxyTest, CommonMixin, TcpMixin): reverse = True +class TestReverseSSL(tservers.ReverseProxyTest, CommonMixin, TcpMixin): + reverse = True + ssl = True + + class TestSocks5(tservers.SocksModeTest): def test_simple(self): diff --git a/web/src/js/__tests__/ducks/ui/flowSpec.js b/web/src/js/__tests__/ducks/ui/flowSpec.js new file mode 100644 index 00000000..f838fbaa --- /dev/null +++ b/web/src/js/__tests__/ducks/ui/flowSpec.js @@ -0,0 +1,76 @@ +jest.unmock('../../../ducks/ui/flow') +jest.unmock('../../../ducks/flows') +jest.unmock('lodash') + +import _ from 'lodash' +import reducer, { + startEdit, + setContentViewDescription, + setShowFullContent, + setContent, + updateEdit + } from '../../../ducks/ui/flow' + +import { select, updateFlow } from '../../../ducks/flows' + +describe('flow reducer', () => { + it('should change to edit mode', () => { + let testFlow = {flow : 'foo'} + const newState = reducer(undefined, startEdit({ flow: 'foo' })) + expect(newState.contentView).toEqual('Edit') + expect(newState.modifiedFlow).toEqual(testFlow) + expect(newState.showFullContent).toEqual(true) + }) + it('should set the view description', () => { + expect(reducer(undefined, setContentViewDescription('description')).viewDescription) + .toEqual('description') + }) + + it('should set show full content', () => { + expect(reducer({showFullContent: false}, setShowFullContent()).showFullContent) + .toBeTruthy() + }) + + it('should set showFullContent to true', () => { + let maxLines = 10 + let content = _.range(maxLines) + const newState = reducer({maxContentLines: maxLines}, setContent(content) ) + expect(newState.showFullContent).toBeTruthy() + expect(newState.content).toEqual(content) + }) + + it('should set showFullContent to false', () => { + let maxLines = 5 + let content = _.range(maxLines+1); + const newState = reducer({maxContentLines: maxLines}, setContent(_.range(maxLines+1))) + expect(newState.showFullContent).toBeFalsy() + expect(newState.content).toEqual(content) + }) + + it('should not change the contentview mode', () => { + expect(reducer({contentView: 'foo'}, select(1)).contentView).toEqual('foo') + }) + + it('should change the contentview mode to auto after editing when a new flow will be selected', () => { + expect(reducer({contentView: 'foo', modifiedFlow : 'test_flow'}, select(1)).contentView).toEqual('Auto') + }) + + it('should set update and merge the modifiedflow with the update values', () => { + let modifiedFlow = {headers: []} + let updateValues = {content: 'bar'} + let result = {headers: [], content: 'bar'} + expect(reducer({modifiedFlow}, updateEdit(updateValues)).modifiedFlow).toEqual(result) + }) + + it('should not change the state when a flow is updated which is not selected', () => { + let modifiedFlow = {id: 1} + let updatedFlow = {id: 0} + expect(reducer({modifiedFlow}, updateFlow(updatedFlow)).modifiedFlow).toEqual(modifiedFlow) + }) + + it('should stop editing when the selected flow is updated', () => { + let modifiedFlow = {id: 1} + let updatedFlow = {id: 1} + expect(reducer({modifiedFlow}, updateFlow(updatedFlow)).modifiedFlow).toBeFalsy() + }) +}) diff --git a/web/src/js/__tests__/ducks/utils/listSpec.js b/web/src/js/__tests__/ducks/utils/listSpec.js index 72d162f2..0f5d0f34 100644 --- a/web/src/js/__tests__/ducks/utils/listSpec.js +++ b/web/src/js/__tests__/ducks/utils/listSpec.js @@ -2,6 +2,7 @@ jest.unmock('lodash') jest.unmock('../../../ducks/utils/list') import reduce, * as list from '../../../ducks/utils/list' +import _ from 'lodash' describe('list reduce', () => { diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx index 676068e9..cfd96dd8 100644 --- a/web/src/js/components/ContentView/ShowFullContentButton.jsx +++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx @@ -16,7 +16,7 @@ function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLi return ( !showFullContent && <div> - <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent(true)} text="Show full content"/> + <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()} text="Show full content"/> <span className="pull-right"> {visibleLines}/{contentLines} are visible </span> </div> ) diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index e65c39a3..4a6d64cd 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -16,7 +16,7 @@ export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW', const defaultState = { displayLarge: false, - contentViewDescription: '', + viewDescription: '', showFullContent: false, modifiedFlow: false, contentView: 'Auto', @@ -27,6 +27,10 @@ const defaultState = { export default function reducer(state = defaultState, action) { let wasInEditMode = !!(state.modifiedFlow) + + let content = action.content || state.content + let isFullContentShown = content && content.length <= state.maxContentLines + switch (action.type) { case START_EDIT: @@ -49,8 +53,7 @@ export default function reducer(state = defaultState, action) { modifiedFlow: false, displayLarge: false, contentView: (wasInEditMode ? 'Auto' : state.contentView), - viewDescription: '', - showFullContent: false, + showFullContent: isFullContentShown, } case flowsActions.UPDATE: @@ -63,7 +66,6 @@ export default function reducer(state = defaultState, action) { modifiedFlow: false, displayLarge: false, contentView: (wasInEditMode ? 'Auto' : state.contentView), - viewDescription: '', showFullContent: false } } else { @@ -79,7 +81,7 @@ export default function reducer(state = defaultState, action) { case SET_SHOW_FULL_CONTENT: return { ...state, - showFullContent: action.show + showFullContent: true } case SET_TAB: @@ -98,7 +100,6 @@ export default function reducer(state = defaultState, action) { } case SET_CONTENT: - let isFullContentShown = action.content.length < state.maxContentLines return { ...state, content: action.content, @@ -139,12 +140,8 @@ export function setContentViewDescription(description) { return { type: SET_CONTENT_VIEW_DESCRIPTION, description } } -export function setShowFullContent(show) { - return { type: SET_SHOW_FULL_CONTENT, show } -} - -export function updateEdit(update) { - return { type: UPDATE_EDIT, update } +export function setShowFullContent() { + return { type: SET_SHOW_FULL_CONTENT } } export function setContent(content){ |