From 3a3305b9acbb3ac3ac82f9b29c099699b5210fa5 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 24 Jul 2016 18:22:40 +0800 Subject: [PATCH 01/32] [web] fix: Flow update changes list order #36 --- web/src/js/ducks/utils/view.js | 35 ++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/web/src/js/ducks/utils/view.js b/web/src/js/ducks/utils/view.js index c00f00bd1..2f1e03fa8 100755 --- a/web/src/js/ducks/utils/view.js +++ b/web/src/js/ducks/utils/view.js @@ -57,16 +57,9 @@ export default function reduce(state = defaultState, action) { if (state.indexOf[action.item.id] == null) { return } - const nextState = { - ...state, - ...sortedRemove(state, action.item.id), - } - if (!action.filter(action.item)) { - return nextState - } return { - ...nextState, - ...sortedInsert(nextState, action.item, action.sort) + ...state, + ...sortedUpdate(state, action.item, action.sort), } case RECEIVE: @@ -110,7 +103,7 @@ export function receive(list, filter = defaultFilter, sort = defaultSort) { function sortedInsert(state, item, sort) { const index = sortedIndex(state.data, item, sort) - const data = [...state.data] + const data = [ ...state.data ] const indexOf = { ...state.indexOf } data.splice(index, 0, item) @@ -134,6 +127,28 @@ function sortedRemove(state, id) { return { data, indexOf } } +function sortedUpdate(state, item, sort) { + let data = [ ...state.data ] + let indexOf = { ...state.indexOf } + let index = indexOf[item.id] + data[index] = item + while (index + 1 < data.length && sort(data[index], data[index + 1]) > 0) { + data[index] = data[index + 1] + data[index + 1] = item + indexOf[item.id] = index + 1 + indexOf[data[index].id] = index + ++index + } + while (index > 0 && sort(data[index], data[index - 1]) < 0) { + data[index] = data[index - 1] + data[index - 1] = item + indexOf[item.id] = index - 1 + indexOf[data[index].id] = index + --index + } + return { data, indexOf } +} + function sortedIndex(list, item, sort) { let low = 0 let high = list.length From 7b51f12813ab145304c15f0d39222a2811e6ca4d Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 26 Jul 2016 02:09:54 +0800 Subject: [PATCH 02/32] [web] bug fix and add test --- web/src/js/__tests__/ducks/tutils.js | 6 ++++ .../__tests__/ducks/{ui.js => ui/header.js} | 10 +++---- web/src/js/__tests__/ducks/utils/view.js | 6 ++-- web/src/js/ducks/utils/view.js | 28 +++++++++++++++---- 4 files changed, 37 insertions(+), 13 deletions(-) rename web/src/js/__tests__/ducks/{ui.js => ui/header.js} (83%) diff --git a/web/src/js/__tests__/ducks/tutils.js b/web/src/js/__tests__/ducks/tutils.js index 90a21b78d..6a543434c 100644 --- a/web/src/js/__tests__/ducks/tutils.js +++ b/web/src/js/__tests__/ducks/tutils.js @@ -10,3 +10,9 @@ export function createStore(parts) { applyMiddleware(...[thunk]) ) } + +describe('tutils', () => { + it('do nothing', () => { + return + }) +}) \ No newline at end of file diff --git a/web/src/js/__tests__/ducks/ui.js b/web/src/js/__tests__/ducks/ui/header.js similarity index 83% rename from web/src/js/__tests__/ducks/ui.js rename to web/src/js/__tests__/ducks/ui/header.js index d32428151..8968e636a 100644 --- a/web/src/js/__tests__/ducks/ui.js +++ b/web/src/js/__tests__/ducks/ui/header.js @@ -1,10 +1,10 @@ -jest.unmock('../../ducks/ui') -jest.unmock('../../ducks/flows') +jest.unmock('../../../ducks/ui/header') +jest.unmock('../../../ducks/flows') -import reducer, { setActiveMenu } from '../../ducks/ui' -import * as flowActions from '../../ducks/flows' +import reducer, { setActiveMenu } from '../../../ducks/ui/header' +import * as flowActions from '../../../ducks/flows' -describe('ui reducer', () => { +describe('header reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {}).activeMenu).toEqual('Start') }) diff --git a/web/src/js/__tests__/ducks/utils/view.js b/web/src/js/__tests__/ducks/utils/view.js index f0b147da3..af3da1737 100644 --- a/web/src/js/__tests__/ducks/utils/view.js +++ b/web/src/js/__tests__/ducks/utils/view.js @@ -66,11 +66,13 @@ describe('view reduce', () => { it('should update item', () => { const state = createState([ { id: 1, val: 1 }, - { id: 2, val: 2 } + { id: 2, val: 2 }, + { id: 3, val: 3 } ]) const result = createState([ { id: 1, val: 1 }, - { id: 2, val: 3 } + { id: 2, val: 3 }, + { id: 3, val: 3 } ]) expect(reduce(state, view.update({ id: 2, val: 3 }))).toEqual(result) }) diff --git a/web/src/js/ducks/utils/view.js b/web/src/js/ducks/utils/view.js index 2f1e03fa8..fdddc3917 100755 --- a/web/src/js/ducks/utils/view.js +++ b/web/src/js/ducks/utils/view.js @@ -54,14 +54,30 @@ export default function reduce(state = defaultState, action) { } case UPDATE: - if (state.indexOf[action.item.id] == null) { - return + let hasOldItem = state.indexOf[action.item.id] !== null && state.indexOf[action.item.id] !== undefined + let hasNewItem = action.filter(action.item) + if (!hasNewItem && !hasOldItem) { + return state } - return { - ...state, - ...sortedUpdate(state, action.item, action.sort), + if (hasNewItem && !hasOldItem) { + return { + ...state, + ...sortedInsert(state, action.item, action.sort) + } } - + if (!hasNewItem && hasOldItem) { + return { + ...state, + ...sortedRemove(state, action.item.id) + } + } + if (hasNewItem && hasOldItem) { + return { + ...state, + ...sortedUpdate(state, action.item, action.sort), + } + } + break; case RECEIVE: { const data = action.list.filter(action.filter).sort(action.sort) From 2b9e5dcd1b93792bcfa1df07352cff1ecffbf36d Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 27 Jul 2016 00:39:01 +0800 Subject: [PATCH 03/32] [web] change test files --- web/package.json | 1 + web/src/js/__tests__/ducks/{flowView.js => flowViewSpec.js} | 0 web/src/js/__tests__/ducks/{flows.js => flowsSpec.js} | 0 web/src/js/__tests__/ducks/tutils.js | 6 ------ web/src/js/__tests__/ducks/ui/{header.js => headerSpec.js} | 0 web/src/js/__tests__/ducks/utils/{list.js => listSpec.js} | 0 web/src/js/__tests__/ducks/utils/{view.js => viewSpec.js} | 0 web/src/js/ducks/utils/view.js | 1 - 8 files changed, 1 insertion(+), 7 deletions(-) rename web/src/js/__tests__/ducks/{flowView.js => flowViewSpec.js} (100%) rename web/src/js/__tests__/ducks/{flows.js => flowsSpec.js} (100%) rename web/src/js/__tests__/ducks/ui/{header.js => headerSpec.js} (100%) rename web/src/js/__tests__/ducks/utils/{list.js => listSpec.js} (100%) rename web/src/js/__tests__/ducks/utils/{view.js => viewSpec.js} (100%) diff --git a/web/package.json b/web/package.json index 81b96adca..66a12501b 100644 --- a/web/package.json +++ b/web/package.json @@ -7,6 +7,7 @@ "start": "gulp" }, "jest": { + "testRegex": "__tests__/.*\\Spec.js$", "testPathDirs": [ "/src/js" ], diff --git a/web/src/js/__tests__/ducks/flowView.js b/web/src/js/__tests__/ducks/flowViewSpec.js similarity index 100% rename from web/src/js/__tests__/ducks/flowView.js rename to web/src/js/__tests__/ducks/flowViewSpec.js diff --git a/web/src/js/__tests__/ducks/flows.js b/web/src/js/__tests__/ducks/flowsSpec.js similarity index 100% rename from web/src/js/__tests__/ducks/flows.js rename to web/src/js/__tests__/ducks/flowsSpec.js diff --git a/web/src/js/__tests__/ducks/tutils.js b/web/src/js/__tests__/ducks/tutils.js index 6a543434c..90a21b78d 100644 --- a/web/src/js/__tests__/ducks/tutils.js +++ b/web/src/js/__tests__/ducks/tutils.js @@ -10,9 +10,3 @@ export function createStore(parts) { applyMiddleware(...[thunk]) ) } - -describe('tutils', () => { - it('do nothing', () => { - return - }) -}) \ No newline at end of file diff --git a/web/src/js/__tests__/ducks/ui/header.js b/web/src/js/__tests__/ducks/ui/headerSpec.js similarity index 100% rename from web/src/js/__tests__/ducks/ui/header.js rename to web/src/js/__tests__/ducks/ui/headerSpec.js diff --git a/web/src/js/__tests__/ducks/utils/list.js b/web/src/js/__tests__/ducks/utils/listSpec.js similarity index 100% rename from web/src/js/__tests__/ducks/utils/list.js rename to web/src/js/__tests__/ducks/utils/listSpec.js diff --git a/web/src/js/__tests__/ducks/utils/view.js b/web/src/js/__tests__/ducks/utils/viewSpec.js similarity index 100% rename from web/src/js/__tests__/ducks/utils/view.js rename to web/src/js/__tests__/ducks/utils/viewSpec.js diff --git a/web/src/js/ducks/utils/view.js b/web/src/js/ducks/utils/view.js index fdddc3917..6bf0a63ea 100755 --- a/web/src/js/ducks/utils/view.js +++ b/web/src/js/ducks/utils/view.js @@ -77,7 +77,6 @@ export default function reduce(state = defaultState, action) { ...sortedUpdate(state, action.item, action.sort), } } - break; case RECEIVE: { const data = action.list.filter(action.filter).sort(action.sort) From 303b6df447724eb051b65c7e93880a9a90d2b0aa Mon Sep 17 00:00:00 2001 From: dufferzafar Date: Tue, 26 Jul 2016 00:58:09 -0700 Subject: [PATCH 04/32] Use jsbeautifier from pip --- mitmproxy/contentviews.py | 4 ++-- setup.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mitmproxy/contentviews.py b/mitmproxy/contentviews.py index e155bc01e..3aedf08a3 100644 --- a/mitmproxy/contentviews.py +++ b/mitmproxy/contentviews.py @@ -31,7 +31,7 @@ from PIL import Image from six import BytesIO from mitmproxy import exceptions -from mitmproxy.contrib import jsbeautifier +import jsbeautifier from mitmproxy.contrib.wbxml import ASCommandResponse from netlib import http from netlib import multidict @@ -398,7 +398,7 @@ class ViewJavaScript(View): def __call__(self, data, **metadata): opts = jsbeautifier.default_options() opts.indent_size = 2 - res = jsbeautifier.beautify(data, opts) + res = jsbeautifier.beautify(data.decode(), opts) return "JavaScript", format_text(res) diff --git a/setup.py b/setup.py index 23eb3b268..1183d3e16 100644 --- a/setup.py +++ b/setup.py @@ -111,6 +111,7 @@ setup( ], 'contentviews': [ "cssutils>=1.0.1, <1.1", + "jsbeautifier>=1.6.3" # TODO: Find Python 3 replacements # "protobuf>=2.6.1, <2.7", # "pyamf>=0.8.0, <0.9", From 4ce2420545bc329397543afd4f8b3da2d8c089e8 Mon Sep 17 00:00:00 2001 From: dufferzafar Date: Wed, 27 Jul 2016 04:03:33 -0700 Subject: [PATCH 05/32] Make contentview requirements mandatory --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1183d3e16..2dca1a275 100644 --- a/setup.py +++ b/setup.py @@ -67,10 +67,12 @@ setup( "configargparse>=0.10, <0.11", "construct>=2.5.2, <2.6", "cryptography>=1.3, <1.5", + "cssutils>=1.0.1, <1.1", "Flask>=0.10.1, <0.12", "h2>=2.4.0, <3", "html2text>=2016.1.8, <=2016.5.29", "hyperframe>=4.0.1, <5", + "jsbeautifier>=1.6.3" "lxml>=3.5.0, <=3.6.0", # no wheels for 3.6.1 yet. "Pillow>=3.2, <3.4", "passlib>=1.6.5, <1.7", @@ -110,8 +112,6 @@ setup( "sphinx_rtd_theme>=0.1.9, <0.2", ], 'contentviews': [ - "cssutils>=1.0.1, <1.1", - "jsbeautifier>=1.6.3" # TODO: Find Python 3 replacements # "protobuf>=2.6.1, <2.7", # "pyamf>=0.8.0, <0.9", From 21e6ecb47deb9b372c8b102b92d79fa49a7e38ab Mon Sep 17 00:00:00 2001 From: dufferzafar Date: Wed, 27 Jul 2016 04:04:44 -0700 Subject: [PATCH 06/32] Remove jsbeautifier files --- mitmproxy/contrib/jsbeautifier/__init__.py | 1153 ----------------- .../jsbeautifier/unpackers/README.specs.mkd | 25 - .../jsbeautifier/unpackers/__init__.py | 66 - .../jsbeautifier/unpackers/evalbased.py | 39 - .../unpackers/javascriptobfuscator.py | 58 - .../jsbeautifier/unpackers/myobfuscate.py | 86 -- .../contrib/jsbeautifier/unpackers/packer.py | 103 -- .../jsbeautifier/unpackers/urlencode.py | 34 - 8 files changed, 1564 deletions(-) delete mode 100644 mitmproxy/contrib/jsbeautifier/__init__.py delete mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd delete mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/__init__.py delete mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py delete mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py delete mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py delete mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/packer.py delete mode 100644 mitmproxy/contrib/jsbeautifier/unpackers/urlencode.py diff --git a/mitmproxy/contrib/jsbeautifier/__init__.py b/mitmproxy/contrib/jsbeautifier/__init__.py deleted file mode 100644 index e319e8dd1..000000000 --- a/mitmproxy/contrib/jsbeautifier/__init__.py +++ /dev/null @@ -1,1153 +0,0 @@ -import sys -import getopt -import re -import string - -# -# Originally written by Einar Lielmanis et al., -# Conversion to python by Einar Lielmanis, einar@jsbeautifier.org, -# MIT licence, enjoy. -# -# Python is not my native language, feel free to push things around. -# -# Use either from command line (script displays its usage when run -# without any parameters), -# -# -# or, alternatively, use it as a module: -# -# import jsbeautifier -# res = jsbeautifier.beautify('your javascript string') -# res = jsbeautifier.beautify_file('some_file.js') -# -# you may specify some options: -# -# opts = jsbeautifier.default_options() -# opts.indent_size = 2 -# res = jsbeautifier.beautify('some javascript', opts) -# -# -# Here are the available options: (read source) - - -class BeautifierOptions: - def __init__(self): - self.indent_size = 4 - self.indent_char = ' ' - self.indent_with_tabs = False - self.preserve_newlines = True - self.max_preserve_newlines = 10. - self.jslint_happy = False - self.brace_style = 'collapse' - self.keep_array_indentation = False - self.keep_function_indentation = False - self.eval_code = False - - - - def __repr__(self): - return \ -"""indent_size = %d -indent_char = [%s] -preserve_newlines = %s -max_preserve_newlines = %d -jslint_happy = %s -indent_with_tabs = %s -brace_style = %s -keep_array_indentation = %s -eval_code = %s -""" % ( self.indent_size, - self.indent_char, - self.preserve_newlines, - self.max_preserve_newlines, - self.jslint_happy, - self.indent_with_tabs, - self.brace_style, - self.keep_array_indentation, - self.eval_code, - ) - - -class BeautifierFlags: - def __init__(self, mode): - self.previous_mode = 'BLOCK' - self.mode = mode - self.var_line = False - self.var_line_tainted = False - self.var_line_reindented = False - self.in_html_comment = False - self.if_line = False - self.in_case = False - self.eat_next_space = False - self.indentation_baseline = -1 - self.indentation_level = 0 - self.ternary_depth = 0 - - -def default_options(): - return BeautifierOptions() - - -def beautify(string, opts = default_options() ): - b = Beautifier() - return b.beautify(string, opts) - -def beautify_file(file_name, opts = default_options() ): - - if file_name == '-': # stdin - f = sys.stdin - else: - try: - f = open(file_name) - except Exception as ex: - return 'The file could not be opened' - - b = Beautifier() - return b.beautify(''.join(f.readlines()), opts) - - -def usage(): - - print("""Javascript beautifier (http://jsbeautifier.org/) - -Usage: jsbeautifier.py [options] - - can be "-", which means stdin. - defaults to stdout - -Input options: - - -i, --stdin read input from stdin - -Output options: - - -s, --indent-size=NUMBER indentation size. (default 4). - -c, --indent-char=CHAR character to indent with. (default space). - -t, --indent-with-tabs Indent with tabs, overrides -s and -c - -d, --disable-preserve-newlines do not preserve existing line breaks. - -j, --jslint-happy more jslint-compatible output - -b, --brace-style=collapse brace style (collapse, expand, end-expand) - -k, --keep-array-indentation keep array indentation. - -o, --outfile=FILE specify a file to output to (default stdout) - -f, --keep-function-indentation Do not re-indent function bodies defined in var lines. - -Rarely needed options: - - --eval-code evaluate code if a JS interpreter is - installed. May be useful with some obfuscated - script but poses a potential security issue. - - -l, --indent-level=NUMBER initial indentation level. (default 0). - - -h, --help, --usage prints this help statement. - -""") - - - - - - -class Beautifier: - - def __init__(self, opts = default_options() ): - - self.opts = opts - self.blank_state() - - def blank_state(self): - - # internal flags - self.flags = BeautifierFlags('BLOCK') - self.flag_store = [] - self.wanted_newline = False - self.just_added_newline = False - self.do_block_just_closed = False - - if self.opts.indent_with_tabs: - self.indent_string = "\t" - else: - self.indent_string = self.opts.indent_char * self.opts.indent_size - - self.preindent_string = '' - self.last_word = '' # last TK_WORD seen - self.last_type = 'TK_START_EXPR' # last token type - self.last_text = '' # last token text - self.last_last_text = '' # pre-last token text - - self.input = None - self.output = [] # formatted javascript gets built here - - self.whitespace = ["\n", "\r", "\t", " "] - self.wordchar = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$' - self.digits = '0123456789' - self.punct = '+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << >>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |= ::' - self.punct += ' <%= <% %>' - self.punct = self.punct.split(' ') - - - # Words which always should start on a new line - self.line_starters = 'continue,try,throw,return,var,if,switch,case,default,for,while,break,function'.split(',') - self.set_mode('BLOCK') - - global parser_pos - parser_pos = 0 - - - def beautify(self, s, opts = None ): - - if opts != None: - self.opts = opts - - - if self.opts.brace_style not in ['expand', 'collapse', 'end-expand']: - raise(Exception('opts.brace_style must be "expand", "collapse" or "end-expand".')) - - self.blank_state() - - while s and s[0] in [' ', '\t']: - self.preindent_string += s[0] - s = s[1:] - - #self.input = self.unpack(s, opts.eval_code) - # CORTESI - self.input = s - - parser_pos = 0 - while True: - token_text, token_type = self.get_next_token() - #print (token_text, token_type, self.flags.mode) - if token_type == 'TK_EOF': - break - - handlers = { - 'TK_START_EXPR': self.handle_start_expr, - 'TK_END_EXPR': self.handle_end_expr, - 'TK_START_BLOCK': self.handle_start_block, - 'TK_END_BLOCK': self.handle_end_block, - 'TK_WORD': self.handle_word, - 'TK_SEMICOLON': self.handle_semicolon, - 'TK_STRING': self.handle_string, - 'TK_EQUALS': self.handle_equals, - 'TK_OPERATOR': self.handle_operator, - 'TK_BLOCK_COMMENT': self.handle_block_comment, - 'TK_INLINE_COMMENT': self.handle_inline_comment, - 'TK_COMMENT': self.handle_comment, - 'TK_UNKNOWN': self.handle_unknown, - } - - handlers[token_type](token_text) - - self.last_last_text = self.last_text - self.last_type = token_type - self.last_text = token_text - - sweet_code = self.preindent_string + re.sub('[\n ]+$', '', ''.join(self.output)) - return sweet_code - - def unpack(self, source, evalcode=False): - import jsbeautifier.unpackers as unpackers - try: - return unpackers.run(source, evalcode) - except unpackers.UnpackingError as error: - print('error:', error) - return '' - - def trim_output(self, eat_newlines = False): - while len(self.output) \ - and ( - self.output[-1] == ' '\ - or self.output[-1] == self.indent_string \ - or self.output[-1] == self.preindent_string \ - or (eat_newlines and self.output[-1] in ['\n', '\r'])): - self.output.pop() - - def is_special_word(self, s): - return s in ['case', 'return', 'do', 'if', 'throw', 'else']; - - def is_array(self, mode): - return mode in ['[EXPRESSION]', '[INDENDED-EXPRESSION]'] - - - def is_expression(self, mode): - return mode in ['[EXPRESSION]', '[INDENDED-EXPRESSION]', '(EXPRESSION)', '(FOR-EXPRESSION)', '(COND-EXPRESSION)'] - - - def append_newline_forced(self): - old_array_indentation = self.opts.keep_array_indentation - self.opts.keep_array_indentation = False - self.append_newline() - self.opts.keep_array_indentation = old_array_indentation - - def append_newline(self, ignore_repeated = True): - - self.flags.eat_next_space = False - - if self.opts.keep_array_indentation and self.is_array(self.flags.mode): - return - - self.flags.if_line = False - self.trim_output() - - if len(self.output) == 0: - # no newline on start of file - return - - if self.output[-1] != '\n' or not ignore_repeated: - self.just_added_newline = True - self.output.append('\n') - - if self.preindent_string: - self.output.append(self.preindent_string) - - for i in range(self.flags.indentation_level): - self.output.append(self.indent_string) - - if self.flags.var_line and self.flags.var_line_reindented: - self.output.append(self.indent_string) - - - def append(self, s): - if s == ' ': - # do not add just a single space after the // comment, ever - if self.last_type == 'TK_COMMENT': - return self.append_newline() - - # make sure only single space gets drawn - if self.flags.eat_next_space: - self.flags.eat_next_space = False - elif len(self.output) and self.output[-1] not in [' ', '\n', self.indent_string]: - self.output.append(' ') - else: - self.just_added_newline = False - self.flags.eat_next_space = False - self.output.append(s) - - - def indent(self): - self.flags.indentation_level = self.flags.indentation_level + 1 - - - def remove_indent(self): - if len(self.output) and self.output[-1] in [self.indent_string, self.preindent_string]: - self.output.pop() - - - def set_mode(self, mode): - - prev = BeautifierFlags('BLOCK') - - if self.flags: - self.flag_store.append(self.flags) - prev = self.flags - - self.flags = BeautifierFlags(mode) - - if len(self.flag_store) == 1: - self.flags.indentation_level = 0 - else: - self.flags.indentation_level = prev.indentation_level - if prev.var_line and prev.var_line_reindented: - self.flags.indentation_level = self.flags.indentation_level + 1 - self.flags.previous_mode = prev.mode - - - def restore_mode(self): - self.do_block_just_closed = self.flags.mode == 'DO_BLOCK' - if len(self.flag_store) > 0: - mode = self.flags.mode - self.flags = self.flag_store.pop() - self.flags.previous_mode = mode - - - def get_next_token(self): - - global parser_pos - - self.n_newlines = 0 - - if parser_pos >= len(self.input): - return '', 'TK_EOF' - - self.wanted_newline = False - c = self.input[parser_pos] - parser_pos += 1 - - keep_whitespace = self.opts.keep_array_indentation and self.is_array(self.flags.mode) - - if keep_whitespace: - # slight mess to allow nice preservation of array indentation and reindent that correctly - # first time when we get to the arrays: - # var a = [ - # ....'something' - # we make note of whitespace_count = 4 into flags.indentation_baseline - # so we know that 4 whitespaces in original source match indent_level of reindented source - # - # and afterwards, when we get to - # 'something, - # .......'something else' - # we know that this should be indented to indent_level + (7 - indentation_baseline) spaces - - whitespace_count = 0 - while c in self.whitespace: - if c == '\n': - self.trim_output() - self.output.append('\n') - self.just_added_newline = True - whitespace_count = 0 - elif c == '\t': - whitespace_count += 4 - elif c == '\r': - pass - else: - whitespace_count += 1 - - if parser_pos >= len(self.input): - return '', 'TK_EOF' - - c = self.input[parser_pos] - parser_pos += 1 - - if self.flags.indentation_baseline == -1: - - self.flags.indentation_baseline = whitespace_count - - if self.just_added_newline: - for i in range(self.flags.indentation_level + 1): - self.output.append(self.indent_string) - - if self.flags.indentation_baseline != -1: - for i in range(whitespace_count - self.flags.indentation_baseline): - self.output.append(' ') - - else: # not keep_whitespace - while c in self.whitespace: - if c == '\n': - if self.opts.max_preserve_newlines == 0 or self.opts.max_preserve_newlines > self.n_newlines: - self.n_newlines += 1 - - if parser_pos >= len(self.input): - return '', 'TK_EOF' - - c = self.input[parser_pos] - parser_pos += 1 - - if self.opts.preserve_newlines and self.n_newlines > 1: - for i in range(self.n_newlines): - self.append_newline(i == 0) - self.just_added_newline = True - - self.wanted_newline = self.n_newlines > 0 - - - if c in self.wordchar: - if parser_pos < len(self.input): - while self.input[parser_pos] in self.wordchar: - c = c + self.input[parser_pos] - parser_pos += 1 - if parser_pos == len(self.input): - break - - # small and surprisingly unugly hack for 1E-10 representation - if parser_pos != len(self.input) and self.input[parser_pos] in '+-' \ - and re.match('^[0-9]+[Ee]$', c): - - sign = self.input[parser_pos] - parser_pos += 1 - t = self.get_next_token() - c += sign + t[0] - return c, 'TK_WORD' - - if c == 'in': # in is an operator, need to hack - return c, 'TK_OPERATOR' - - if self.wanted_newline and \ - self.last_type != 'TK_OPERATOR' and\ - self.last_type != 'TK_EQUALS' and\ - not self.flags.if_line and \ - (self.opts.preserve_newlines or self.last_text != 'var'): - self.append_newline() - - return c, 'TK_WORD' - - if c in '([': - return c, 'TK_START_EXPR' - - if c in ')]': - return c, 'TK_END_EXPR' - - if c == '{': - return c, 'TK_START_BLOCK' - - if c == '}': - return c, 'TK_END_BLOCK' - - if c == ';': - return c, 'TK_SEMICOLON' - - if c == '/': - comment = '' - inline_comment = True - comment_mode = 'TK_INLINE_COMMENT' - if self.input[parser_pos] == '*': # peek /* .. */ comment - parser_pos += 1 - if parser_pos < len(self.input): - while not (self.input[parser_pos] == '*' and \ - parser_pos + 1 < len(self.input) and \ - self.input[parser_pos + 1] == '/')\ - and parser_pos < len(self.input): - c = self.input[parser_pos] - comment += c - if c in '\r\n': - comment_mode = 'TK_BLOCK_COMMENT' - parser_pos += 1 - if parser_pos >= len(self.input): - break - parser_pos += 2 - return '/*' + comment + '*/', comment_mode - if self.input[parser_pos] == '/': # peek // comment - comment = c - while self.input[parser_pos] not in '\r\n': - comment += self.input[parser_pos] - parser_pos += 1 - if parser_pos >= len(self.input): - break - parser_pos += 1 - if self.wanted_newline: - self.append_newline() - return comment, 'TK_COMMENT' - - - - if c == "'" or c == '"' or \ - (c == '/' and ((self.last_type == 'TK_WORD' and self.is_special_word(self.last_text)) or \ - (self.last_type == 'TK_END_EXPR' and self.flags.previous_mode in ['(FOR-EXPRESSION)', '(COND-EXPRESSION)']) or \ - (self.last_type in ['TK_COMMENT', 'TK_START_EXPR', 'TK_START_BLOCK', 'TK_END_BLOCK', 'TK_OPERATOR', - 'TK_EQUALS', 'TK_EOF', 'TK_SEMICOLON']))): - sep = c - esc = False - resulting_string = c - in_char_class = False - - if parser_pos < len(self.input): - if sep == '/': - # handle regexp - in_char_class = False - while esc or in_char_class or self.input[parser_pos] != sep: - resulting_string += self.input[parser_pos] - if not esc: - esc = self.input[parser_pos] == '\\' - if self.input[parser_pos] == '[': - in_char_class = True - elif self.input[parser_pos] == ']': - in_char_class = False - else: - esc = False - parser_pos += 1 - if parser_pos >= len(self.input): - # incomplete regex when end-of-file reached - # bail out with what has received so far - return resulting_string, 'TK_STRING' - else: - # handle string - while esc or self.input[parser_pos] != sep: - resulting_string += self.input[parser_pos] - if not esc: - esc = self.input[parser_pos] == '\\' - else: - esc = False - parser_pos += 1 - if parser_pos >= len(self.input): - # incomplete string when end-of-file reached - # bail out with what has received so far - return resulting_string, 'TK_STRING' - - - parser_pos += 1 - resulting_string += sep - if sep == '/': - # regexps may have modifiers /regexp/MOD, so fetch those too - while parser_pos < len(self.input) and self.input[parser_pos] in self.wordchar: - resulting_string += self.input[parser_pos] - parser_pos += 1 - return resulting_string, 'TK_STRING' - - if c == '#': - - # she-bang - if len(self.output) == 0 and len(self.input) > 1 and self.input[parser_pos] == '!': - resulting_string = c - while parser_pos < len(self.input) and c != '\n': - c = self.input[parser_pos] - resulting_string += c - parser_pos += 1 - self.output.append(resulting_string.strip() + "\n") - self.append_newline() - return self.get_next_token() - - - # Spidermonkey-specific sharp variables for circular references - # https://developer.mozilla.org/En/Sharp_variables_in_JavaScript - # http://mxr.mozilla.org/mozilla-central/source/js/src/jsscan.cpp around line 1935 - sharp = '#' - if parser_pos < len(self.input) and self.input[parser_pos] in self.digits: - while True: - c = self.input[parser_pos] - sharp += c - parser_pos += 1 - if parser_pos >= len(self.input) or c == '#' or c == '=': - break - if c == '#' or parser_pos >= len(self.input): - pass - elif self.input[parser_pos] == '[' and self.input[parser_pos + 1] == ']': - sharp += '[]' - parser_pos += 2 - elif self.input[parser_pos] == '{' and self.input[parser_pos + 1] == '}': - sharp += '{}' - parser_pos += 2 - return sharp, 'TK_WORD' - - if c == '<' and self.input[parser_pos - 1 : parser_pos + 3] == '': - self.flags.in_html_comment = False - parser_pos += 2 - if self.wanted_newline: - self.append_newline() - return '-->', 'TK_COMMENT' - - if c in self.punct: - while parser_pos < len(self.input) and c + self.input[parser_pos] in self.punct: - c += self.input[parser_pos] - parser_pos += 1 - if parser_pos >= len(self.input): - break - if c == '=': - return c, 'TK_EQUALS' - else: - return c, 'TK_OPERATOR' - return c, 'TK_UNKNOWN' - - - - def handle_start_expr(self, token_text): - if token_text == '[': - if self.last_type == 'TK_WORD' or self.last_text == ')': - if self.last_text in self.line_starters: - self.append(' ') - self.set_mode('(EXPRESSION)') - self.append(token_text) - return - - if self.flags.mode in ['[EXPRESSION]', '[INDENTED-EXPRESSION]']: - if self.last_last_text == ']' and self.last_text == ',': - # ], [ goes to a new line - if self.flags.mode == '[EXPRESSION]': - self.flags.mode = '[INDENTED-EXPRESSION]' - if not self.opts.keep_array_indentation: - self.indent() - self.set_mode('[EXPRESSION]') - if not self.opts.keep_array_indentation: - self.append_newline() - elif self.last_text == '[': - if self.flags.mode == '[EXPRESSION]': - self.flags.mode = '[INDENTED-EXPRESSION]' - if not self.opts.keep_array_indentation: - self.indent() - self.set_mode('[EXPRESSION]') - - if not self.opts.keep_array_indentation: - self.append_newline() - else: - self.set_mode('[EXPRESSION]') - else: - self.set_mode('[EXPRESSION]') - else: - if self.last_text == 'for': - self.set_mode('(FOR-EXPRESSION)') - elif self.last_text in ['if', 'while']: - self.set_mode('(COND-EXPRESSION)') - else: - self.set_mode('(EXPRESSION)') - - - if self.last_text == ';' or self.last_type == 'TK_START_BLOCK': - self.append_newline() - elif self.last_type in ['TK_END_EXPR', 'TK_START_EXPR', 'TK_END_BLOCK'] or self.last_text == '.': - # do nothing on (( and )( and ][ and ]( and .( - if self.wanted_newline: - self.append_newline(); - elif self.last_type not in ['TK_WORD', 'TK_OPERATOR']: - self.append(' ') - elif self.last_word == 'function' or self.last_word == 'typeof': - # function() vs function (), typeof() vs typeof () - if self.opts.jslint_happy: - self.append(' ') - elif self.last_text in self.line_starters or self.last_text == 'catch': - self.append(' ') - - self.append(token_text) - - - def handle_end_expr(self, token_text): - if token_text == ']': - if self.opts.keep_array_indentation: - if self.last_text == '}': - self.remove_indent() - self.append(token_text) - self.restore_mode() - return - else: - if self.flags.mode == '[INDENTED-EXPRESSION]': - if self.last_text == ']': - self.restore_mode() - self.append_newline() - self.append(token_text) - return - self.restore_mode() - self.append(token_text) - - - def handle_start_block(self, token_text): - if self.last_word == 'do': - self.set_mode('DO_BLOCK') - else: - self.set_mode('BLOCK') - - if self.opts.brace_style == 'expand': - if self.last_type != 'TK_OPERATOR': - if self.last_text == '=' or (self.is_special_word(self.last_text) and self.last_text != 'else'): - self.append(' ') - else: - self.append_newline(True) - - self.append(token_text) - self.indent() - else: - if self.last_type not in ['TK_OPERATOR', 'TK_START_EXPR']: - if self.last_type == 'TK_START_BLOCK': - self.append_newline() - else: - self.append(' ') - else: - # if TK_OPERATOR or TK_START_EXPR - if self.is_array(self.flags.previous_mode) and self.last_text == ',': - if self.last_last_text == '}': - self.append(' ') - else: - self.append_newline() - self.indent() - self.append(token_text) - - - def handle_end_block(self, token_text): - self.restore_mode() - if self.opts.brace_style == 'expand': - if self.last_text != '{': - self.append_newline() - else: - if self.last_type == 'TK_START_BLOCK': - if self.just_added_newline: - self.remove_indent() - else: - # {} - self.trim_output() - else: - if self.is_array(self.flags.mode) and self.opts.keep_array_indentation: - self.opts.keep_array_indentation = False - self.append_newline() - self.opts.keep_array_indentation = True - else: - self.append_newline() - - self.append(token_text) - - - def handle_word(self, token_text): - if self.do_block_just_closed: - self.append(' ') - self.append(token_text) - self.append(' ') - self.do_block_just_closed = False - return - - if token_text == 'function': - - if self.flags.var_line: - self.flags.var_line_reindented = not self.opts.keep_function_indentation - if (self.just_added_newline or self.last_text == ';') and self.last_text != '{': - # make sure there is a nice clean space of at least one blank line - # before a new function definition - have_newlines = self.n_newlines - if not self.just_added_newline: - have_newlines = 0 - if not self.opts.preserve_newlines: - have_newlines = 1 - for i in range(2 - have_newlines): - self.append_newline(False) - - if token_text in ['case', 'default']: - if self.last_text == ':': - self.remove_indent() - else: - self.flags.indentation_level -= 1 - self.append_newline() - self.flags.indentation_level += 1 - self.append(token_text) - self.flags.in_case = True - return - - prefix = 'NONE' - - if self.last_type == 'TK_END_BLOCK': - if token_text not in ['else', 'catch', 'finally']: - prefix = 'NEWLINE' - else: - if self.opts.brace_style in ['expand', 'end-expand']: - prefix = 'NEWLINE' - else: - prefix = 'SPACE' - self.append(' ') - elif self.last_type == 'TK_SEMICOLON' and self.flags.mode in ['BLOCK', 'DO_BLOCK']: - prefix = 'NEWLINE' - elif self.last_type == 'TK_SEMICOLON' and self.is_expression(self.flags.mode): - prefix = 'SPACE' - elif self.last_type == 'TK_STRING': - prefix = 'NEWLINE' - elif self.last_type == 'TK_WORD': - if self.last_text == 'else': - # eat newlines between ...else *** some_op... - # won't preserve extra newlines in this place (if any), but don't care that much - self.trim_output(True) - prefix = 'SPACE' - elif self.last_type == 'TK_START_BLOCK': - prefix = 'NEWLINE' - elif self.last_type == 'TK_END_EXPR': - self.append(' ') - prefix = 'NEWLINE' - - if self.flags.if_line and self.last_type == 'TK_END_EXPR': - self.flags.if_line = False - - if token_text in self.line_starters: - if self.last_text == 'else': - prefix = 'SPACE' - else: - prefix = 'NEWLINE' - - if token_text == 'function' and self.last_text in ['get', 'set']: - prefix = 'SPACE' - - if token_text in ['else', 'catch', 'finally']: - if self.last_type != 'TK_END_BLOCK' \ - or self.opts.brace_style == 'expand' \ - or self.opts.brace_style == 'end-expand': - self.append_newline() - else: - self.trim_output(True) - self.append(' ') - elif prefix == 'NEWLINE': - if token_text == 'function' and (self.last_type == 'TK_START_EXPR' or self.last_text in '=,'): - # no need to force newline on "function" - - # (function... - pass - elif token_text == 'function' and self.last_text == 'new': - self.append(' ') - elif self.is_special_word(self.last_text): - # no newline between return nnn - self.append(' ') - elif self.last_type != 'TK_END_EXPR': - if (self.last_type != 'TK_START_EXPR' or token_text != 'var') and self.last_text != ':': - # no need to force newline on VAR - - # for (var x = 0... - if token_text == 'if' and self.last_word == 'else' and self.last_text != '{': - self.append(' ') - else: - self.flags.var_line = False - self.flags.var_line_reindented = False - self.append_newline() - elif token_text in self.line_starters and self.last_text != ')': - self.flags.var_line = False - self.flags.var_line_reindented = False - self.append_newline() - elif self.is_array(self.flags.mode) and self.last_text == ',' and self.last_last_text == '}': - self.append_newline() # }, in lists get a newline - elif prefix == 'SPACE': - self.append(' ') - - - self.append(token_text) - self.last_word = token_text - - if token_text == 'var': - self.flags.var_line = True - self.flags.var_line_reindented = False - self.flags.var_line_tainted = False - - - if token_text == 'if': - self.flags.if_line = True - - if token_text == 'else': - self.flags.if_line = False - - - def handle_semicolon(self, token_text): - self.append(token_text) - self.flags.var_line = False - self.flags.var_line_reindented = False - if self.flags.mode == 'OBJECT': - # OBJECT mode is weird and doesn't get reset too well. - self.flags.mode = 'BLOCK' - - - def handle_string(self, token_text): - if self.last_type == 'TK_END_EXPR' and self.flags.previous_mode in ['(COND-EXPRESSION)', '(FOR-EXPRESSION)']: - self.append(' ') - if self.last_type in ['TK_STRING', 'TK_START_BLOCK', 'TK_END_BLOCK', 'TK_SEMICOLON']: - self.append_newline() - elif self.last_type == 'TK_WORD': - self.append(' ') - - # Try to replace readable \x-encoded characters with their equivalent, - # if it is possible (e.g. '\x41\x42\x43\x01' becomes 'ABC\x01'). - def unescape(match): - block, code = match.group(0, 1) - char = chr(int(code, 16)) - if block.count('\\') == 1 and char in string.printable: - return char - return block - - token_text = re.sub(r'\\{1,2}x([a-fA-F0-9]{2})', unescape, token_text) - - self.append(token_text) - - def handle_equals(self, token_text): - if self.flags.var_line: - # just got an '=' in a var-line, different line breaking rules will apply - self.flags.var_line_tainted = True - - self.append(' ') - self.append(token_text) - self.append(' ') - - - def handle_operator(self, token_text): - space_before = True - space_after = True - - if self.flags.var_line and token_text == ',' and self.is_expression(self.flags.mode): - # do not break on comma, for ( var a = 1, b = 2 - self.flags.var_line_tainted = False - - if self.flags.var_line and token_text == ',': - if self.flags.var_line_tainted: - self.append(token_text) - self.flags.var_line_reindented = True - self.flags.var_line_tainted = False - self.append_newline() - return - else: - self.flags.var_line_tainted = False - - if self.is_special_word(self.last_text): - # return had a special handling in TK_WORD - self.append(' ') - self.append(token_text) - return - - if token_text == ':' and self.flags.in_case: - self.append(token_text) - self.append_newline() - self.flags.in_case = False - return - - if token_text == '::': - # no spaces around the exotic namespacing syntax operator - self.append(token_text) - return - - if token_text == ',': - if self.flags.var_line: - if self.flags.var_line_tainted: - # This never happens, as it's handled previously, right? - self.append(token_text) - self.append_newline() - self.flags.var_line_tainted = False - else: - self.append(token_text) - self.append(' ') - elif self.last_type == 'TK_END_BLOCK' and self.flags.mode != '(EXPRESSION)': - self.append(token_text) - if self.flags.mode == 'OBJECT' and self.last_text == '}': - self.append_newline() - else: - self.append(' ') - else: - if self.flags.mode == 'OBJECT': - self.append(token_text) - self.append_newline() - else: - # EXPR or DO_BLOCK - self.append(token_text) - self.append(' ') - # comma handled - return - elif token_text in ['--', '++', '!'] \ - or (token_text in ['+', '-'] \ - and self.last_type in ['TK_START_BLOCK', 'TK_START_EXPR', 'TK_EQUALS', 'TK_OPERATOR']) \ - or self.last_text in self.line_starters: - - space_before = False - space_after = False - - if self.last_text == ';' and self.is_expression(self.flags.mode): - # for (;; ++i) - # ^^ - space_before = True - - if self.last_type == 'TK_WORD' and self.last_text in self.line_starters: - space_before = True - - if self.flags.mode == 'BLOCK' and self.last_text in ['{', ';']: - # { foo: --i } - # foo(): --bar - self.append_newline() - - elif token_text == '.': - # decimal digits or object.property - space_before = False - - elif token_text == ':': - if self.flags.ternary_depth == 0: - self.flags.mode = 'OBJECT' - space_before = False - else: - self.flags.ternary_depth -= 1 - elif token_text == '?': - self.flags.ternary_depth += 1 - - if space_before: - self.append(' ') - - self.append(token_text) - - if space_after: - self.append(' ') - - - - def handle_block_comment(self, token_text): - - lines = token_text.replace('\x0d', '').split('\x0a') - # all lines start with an asterisk? that's a proper box comment - if not any(l for l in lines[1:] if ( l.strip() == '' or (l.lstrip())[0] != '*')): - self.append_newline() - self.append(lines[0]) - for line in lines[1:]: - self.append_newline() - self.append(' ' + line.strip()) - else: - # simple block comment: leave intact - if len(lines) > 1: - # multiline comment starts on a new line - self.append_newline() - else: - # single line /* ... */ comment stays on the same line - self.append(' ') - for line in lines: - self.append(line) - self.append('\n') - self.append_newline() - - - def handle_inline_comment(self, token_text): - self.append(' ') - self.append(token_text) - if self.is_expression(self.flags.mode): - self.append(' ') - else: - self.append_newline_forced() - - - def handle_comment(self, token_text): - if self.wanted_newline: - self.append_newline() - else: - self.append(' ') - - self.append(token_text) - self.append_newline_forced() - - - def handle_unknown(self, token_text): - if self.last_text in ['return', 'throw']: - self.append(' ') - - self.append(token_text) - - - - - -def main(): - - argv = sys.argv[1:] - - try: - opts, args = getopt.getopt(argv, "s:c:o:djbkil:htf", ['indent-size=','indent-char=','outfile=', 'disable-preserve-newlines', - 'jslint-happy', 'brace-style=', - 'keep-array-indentation', 'indent-level=', 'help', - 'usage', 'stdin', 'eval-code', 'indent-with-tabs', 'keep-function-indentation']) - except getopt.GetoptError: - return usage() - - js_options = default_options() - - file = None - outfile = 'stdout' - if len(args) == 1: - file = args[0] - - for opt, arg in opts: - if opt in ('--keep-array-indentation', '-k'): - js_options.keep_array_indentation = True - if opt in ('--keep-function-indentation','-f'): - js_options.keep_function_indentation = True - elif opt in ('--outfile', '-o'): - outfile = arg - elif opt in ('--indent-size', '-s'): - js_options.indent_size = int(arg) - elif opt in ('--indent-char', '-c'): - js_options.indent_char = arg - elif opt in ('--indent-with-tabs', '-t'): - js_options.indent_with_tabs = True - elif opt in ('--disable-preserve_newlines', '-d'): - js_options.preserve_newlines = False - elif opt in ('--jslint-happy', '-j'): - js_options.jslint_happy = True - elif opt in ('--eval-code'): - js_options.eval_code = True - elif opt in ('--brace-style', '-b'): - js_options.brace_style = arg - elif opt in ('--stdin', '-i'): - file = '-' - elif opt in ('--help', '--usage', '-h'): - return usage() - - if not file: - return usage() - else: - if outfile == 'stdout': - print(beautify_file(file, js_options)) - else: - with open(outfile, 'w') as f: - f.write(beautify_file(file, js_options) + '\n') - diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd b/mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd deleted file mode 100644 index e937b7621..000000000 --- a/mitmproxy/contrib/jsbeautifier/unpackers/README.specs.mkd +++ /dev/null @@ -1,25 +0,0 @@ -# UNPACKERS SPECIFICATIONS - -Nothing very difficult: an unpacker is a submodule placed in the directory -where this file was found. Each unpacker must define three symbols: - - * `PRIORITY` : integer number expressing the priority in applying this - unpacker. Lower number means higher priority. - Makes sense only if a source file has been packed with - more than one packer. - * `detect(source)` : returns `True` if source is packed, otherwise, `False`. - * `unpack(source)` : takes a `source` string and unpacks it. Must always return - valid JavaScript. That is to say, your code should look - like: - -``` -if detect(source): - return do_your_fancy_things_with(source) -else: - return source -``` - -*You can safely define any other symbol in your module, as it will be ignored.* - -`__init__` code will automatically load new unpackers, without any further step -to be accomplished. Simply drop it in this directory. diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/__init__.py b/mitmproxy/contrib/jsbeautifier/unpackers/__init__.py deleted file mode 100644 index fcb5b07a2..000000000 --- a/mitmproxy/contrib/jsbeautifier/unpackers/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -# -# General code for JSBeautifier unpackers infrastructure. See README.specs -# written by Stefano Sanfilippo -# - -"""General code for JSBeautifier unpackers infrastructure.""" - -import pkgutil -import re -from . import evalbased - -# NOTE: AT THE MOMENT, IT IS DEACTIVATED FOR YOUR SECURITY: it runs js! -BLACKLIST = ['jsbeautifier.unpackers.evalbased'] - -class UnpackingError(Exception): - """Badly packed source or general error. Argument is a - meaningful description.""" - -def getunpackers(): - """Scans the unpackers dir, finds unpackers and add them to UNPACKERS list. - An unpacker will be loaded only if it is a valid python module (name must - adhere to naming conventions) and it is not blacklisted (i.e. inserted - into BLACKLIST.""" - path = __path__ - prefix = __name__ + '.' - unpackers = [] - interface = ['unpack', 'detect', 'PRIORITY'] - for _importer, modname, _ispkg in pkgutil.iter_modules(path, prefix): - if 'tests' not in modname and modname not in BLACKLIST: - try: - module = __import__(modname, fromlist=interface) - except ImportError: - raise UnpackingError('Bad unpacker: %s' % modname) - else: - unpackers.append(module) - - return sorted(unpackers, key = lambda mod: mod.PRIORITY) - -UNPACKERS = getunpackers() - -def run(source, evalcode=False): - """Runs the applicable unpackers and return unpacked source as a string.""" - for unpacker in [mod for mod in UNPACKERS if mod.detect(source)]: - source = unpacker.unpack(source) - if evalcode and evalbased.detect(source): - source = evalbased.unpack(source) - return source - -def filtercomments(source): - """NOT USED: strips trailing comments and put them at the top.""" - trailing_comments = [] - comment = True - - while comment: - if re.search(r'^\s*\/\*', source): - comment = source[0, source.index('*/') + 2] - elif re.search(r'^\s*\/\/', source): - comment = re.search(r'^\s*\/\/', source).group(0) - else: - comment = None - - if comment: - source = re.sub(r'^\s+', '', source[len(comment):]) - trailing_comments.append(comment) - - return '\n'.join(trailing_comments) + source diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py b/mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py deleted file mode 100644 index b17d926ec..000000000 --- a/mitmproxy/contrib/jsbeautifier/unpackers/evalbased.py +++ /dev/null @@ -1,39 +0,0 @@ -# -# Unpacker for eval() based packers, a part of javascript beautifier -# by Einar Lielmanis -# -# written by Stefano Sanfilippo -# -# usage: -# -# if detect(some_string): -# unpacked = unpack(some_string) -# - -"""Unpacker for eval() based packers: runs JS code and returns result. -Works only if a JS interpreter (e.g. Mozilla's Rhino) is installed and -properly set up on host.""" - -from subprocess import PIPE, Popen - -PRIORITY = 3 - -def detect(source): - """Detects if source is likely to be eval() packed.""" - return source.strip().lower().startswith('eval(function(') - -def unpack(source): - """Runs source and return resulting code.""" - return jseval('print %s;' % source[4:]) if detect(source) else source - -# In case of failure, we'll just return the original, without crashing on user. -def jseval(script): - """Run code in the JS interpreter and return output.""" - try: - interpreter = Popen(['js'], stdin=PIPE, stdout=PIPE) - except OSError: - return script - result, errors = interpreter.communicate(script) - if interpreter.poll() or errors: - return script - return result diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py b/mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py deleted file mode 100644 index aa4344a30..000000000 --- a/mitmproxy/contrib/jsbeautifier/unpackers/javascriptobfuscator.py +++ /dev/null @@ -1,58 +0,0 @@ -# -# simple unpacker/deobfuscator for scripts messed up with -# javascriptobfuscator.com -# -# written by Einar Lielmanis -# rewritten in Python by Stefano Sanfilippo -# -# Will always return valid javascript: if `detect()` is false, `code` is -# returned, unmodified. -# -# usage: -# -# if javascriptobfuscator.detect(some_string): -# some_string = javascriptobfuscator.unpack(some_string) -# - -"""deobfuscator for scripts messed up with JavascriptObfuscator.com""" - -import re - -PRIORITY = 1 - -def smartsplit(code): - """Split `code` at " symbol, only if it is not escaped.""" - strings = [] - pos = 0 - while pos < len(code): - if code[pos] == '"': - word = '' # new word - pos += 1 - while pos < len(code): - if code[pos] == '"': - break - if code[pos] == '\\': - word += '\\' - pos += 1 - word += code[pos] - pos += 1 - strings.append('"%s"' % word) - pos += 1 - return strings - -def detect(code): - """Detects if `code` is JavascriptObfuscator.com packed.""" - # prefer `is not` idiom, so that a true boolean is returned - return (re.search(r'^var _0x[a-f0-9]+ ?\= ?\[', code) is not None) - -def unpack(code): - """Unpacks JavascriptObfuscator.com packed code.""" - if detect(code): - matches = re.search(r'var (_0x[a-f\d]+) ?\= ?\[(.*?)\];', code) - if matches: - variable = matches.group(1) - dictionary = smartsplit(matches.group(2)) - code = code[len(matches.group(0)):] - for key, value in enumerate(dictionary): - code = code.replace(r'%s[%s]' % (variable, key), value) - return code diff --git a/mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py b/mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py deleted file mode 100644 index 9893f95f3..000000000 --- a/mitmproxy/contrib/jsbeautifier/unpackers/myobfuscate.py +++ /dev/null @@ -1,86 +0,0 @@ -# -# deobfuscator for scripts messed up with myobfuscate.com -# by Einar Lielmanis -# -# written by Stefano Sanfilippo -# -# usage: -# -# if detect(some_string): -# unpacked = unpack(some_string) -# - -# CAVEAT by Einar Lielmanis - -# -# You really don't want to obfuscate your scripts there: they're tracking -# your unpackings, your script gets turned into something like this, -# as of 2011-08-26: -# -# var _escape = 'your_script_escaped'; -# var _111 = document.createElement('script'); -# _111.src = 'http://api.www.myobfuscate.com/?getsrc=ok' + -# '&ref=' + encodeURIComponent(document.referrer) + -# '&url=' + encodeURIComponent(document.URL); -# var 000 = document.getElementsByTagName('head')[0]; -# 000.appendChild(_111); -# document.write(unescape(_escape)); -# - -"""Deobfuscator for scripts messed up with MyObfuscate.com""" - -import re -import base64 - -# Python 2 retrocompatibility -# pylint: disable=F0401 -# pylint: disable=E0611 -try: - from urllib import unquote -except ImportError: - from urllib.parse import unquote - -from . import UnpackingError - -PRIORITY = 1 - -CAVEAT = """// -// Unpacker warning: be careful when using myobfuscate.com for your projects: -// scripts obfuscated by the free online version call back home. -// - -""" - -SIGNATURE = (r'["\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F' - r'\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x61\x62\x63\x64\x65' - r'\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F\x70\x71\x72\x73\x74\x75' - r'\x76\x77\x78\x79\x7A\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x2B' - r'\x2F\x3D","","\x63\x68\x61\x72\x41\x74","\x69\x6E\x64\x65\x78' - r'\x4F\x66","\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65","' - r'\x6C\x65\x6E\x67\x74\x68"]') - -def detect(source): - """Detects MyObfuscate.com packer.""" - return SIGNATURE in source - -def unpack(source): - """Unpacks js code packed with MyObfuscate.com""" - if not detect(source): - return source - payload = unquote(_filter(source)) - match = re.search(r"^var _escape\='
- \ No newline at end of file + diff --git a/web/src/images/favicon.ico b/web/src/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bfd2fde729addcf74dc3d55a0bbbb69d8349778b GIT binary patch literal 365133 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%0A+wL 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 +
- \ No newline at end of file + From 297cf5d565a1f86e7ffd0cabd2174c41925626e9 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 28 Jul 2016 09:33:20 +1200 Subject: [PATCH 10/32] mitmdump: send script .done when terminating after flow read Fixes #1439 --- mitmproxy/dump.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mitmproxy/dump.py b/mitmproxy/dump.py index 83f44d87a..e59fd23eb 100644 --- a/mitmproxy/dump.py +++ b/mitmproxy/dump.py @@ -118,5 +118,6 @@ class DumpMaster(flow.FlowMaster): def run(self): # pragma: no cover if self.options.rfile and not self.options.keepserving: + self.addons.done() return super(DumpMaster, self).run() From d6e4ef4cedb6e45e9890f44cc152cd8e90aedb01 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Thu, 28 Jul 2016 11:01:32 +1200 Subject: [PATCH 11/32] console: add replace_view_state, fix statusbar issues Fixes #1394 --- mitmproxy/console/master.py | 18 +++++++++++++++++- mitmproxy/console/signals.py | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/mitmproxy/console/master.py b/mitmproxy/console/master.py index ad46cbb4a..87c2cbf20 100644 --- a/mitmproxy/console/master.py +++ b/mitmproxy/console/master.py @@ -257,6 +257,7 @@ class ConsoleMaster(flow.FlowMaster): signals.call_in.connect(self.sig_call_in) signals.pop_view_state.connect(self.sig_pop_view_state) + signals.replace_view_state.connect(self.sig_replace_view_state) signals.push_view_state.connect(self.sig_push_view_state) signals.sig_add_log.connect(self.sig_add_log) self.addons.add(options, *builtins.default_addons()) @@ -295,7 +296,19 @@ class ConsoleMaster(flow.FlowMaster): return callback(*args) self.loop.set_alarm_in(seconds, cb) + def sig_replace_view_state(self, sender): + """ + A view has been pushed onto the stack, and is intended to replace + the current view rather tha creating a new stack entry. + """ + if len(self.view_stack) > 1: + del self.view_stack[1] + def sig_pop_view_state(self, sender): + """ + Pop the top view off the view stack. If no more views will be left + after this, prompt for exit. + """ if len(self.view_stack) > 1: self.view_stack.pop() self.loop.widget = self.view_stack[-1] @@ -311,6 +324,9 @@ class ConsoleMaster(flow.FlowMaster): ) def sig_push_view_state(self, sender, window): + """ + Push a new view onto the view stack. + """ self.view_stack.append(window) self.loop.widget = window self.loop.draw_screen() @@ -351,8 +367,8 @@ class ConsoleMaster(flow.FlowMaster): def toggle_eventlog(self): self.options.eventlog = not self.options.eventlog - signals.pop_view_state.send(self) self.view_flowlist() + signals.replace_view_state.send(self) def _readflows(self, path): """ diff --git a/mitmproxy/console/signals.py b/mitmproxy/console/signals.py index 975078343..93eb399f4 100644 --- a/mitmproxy/console/signals.py +++ b/mitmproxy/console/signals.py @@ -43,3 +43,4 @@ flowlist_change = blinker.Signal() # Pop and push view state onto a stack pop_view_state = blinker.Signal() push_view_state = blinker.Signal() +replace_view_state = blinker.Signal() From 17fdb841f023a546ebb56bc8ae81fb6f74b224cc Mon Sep 17 00:00:00 2001 From: Sachin Kelkar Date: Wed, 27 Jul 2016 17:57:38 -0700 Subject: [PATCH 12/32] verify upstream certificates by default (#1111) squashed and merged by @mhils --- mitmproxy/cmdline.py | 9 +++---- mitmproxy/console/options.py | 7 +++++ mitmproxy/options.py | 4 +-- mitmproxy/proxy/config.py | 22 ++++++--------- test/mitmproxy/test_protocol_http2.py | 6 ++++- test/mitmproxy/test_proxy.py | 6 ++--- test/mitmproxy/test_server.py | 39 ++++++++++++++------------- test/mitmproxy/tservers.py | 3 ++- 8 files changed, 52 insertions(+), 44 deletions(-) diff --git a/mitmproxy/cmdline.py b/mitmproxy/cmdline.py index a6844241c..4eadce116 100644 --- a/mitmproxy/cmdline.py +++ b/mitmproxy/cmdline.py @@ -260,7 +260,7 @@ def get_common_options(args): upstream_auth = args.upstream_auth, ssl_version_client = args.ssl_version_client, ssl_version_server = args.ssl_version_server, - ssl_verify_upstream_cert = args.ssl_verify_upstream_cert, + ssl_insecure = args.ssl_insecure, ssl_verify_upstream_trusted_cadir = args.ssl_verify_upstream_trusted_cadir, ssl_verify_upstream_trusted_ca = args.ssl_verify_upstream_trusted_ca, tcp_hosts = args.tcp_hosts, @@ -519,10 +519,9 @@ def proxy_ssl_options(parser): "that will be served to the proxy client, as extras." ) group.add_argument( - "--verify-upstream-cert", default=False, - action="store_true", dest="ssl_verify_upstream_cert", - help="Verify upstream server SSL/TLS certificates and fail if invalid " - "or not present." + "--insecure", default=False, + action="store_true", dest="ssl_insecure", + help="Do not verify upstream server SSL/TLS certificates." ) group.add_argument( "--upstream-trusted-cadir", default=None, action="store", diff --git a/mitmproxy/console/options.py b/mitmproxy/console/options.py index f9fc3764a..d34672d37 100644 --- a/mitmproxy/console/options.py +++ b/mitmproxy/console/options.py @@ -8,6 +8,7 @@ from mitmproxy.console import grideditor from mitmproxy.console import palettes from mitmproxy.console import select from mitmproxy.console import signals +from OpenSSL import SSL footer = [ ('heading_key', "enter/space"), ":toggle ", @@ -91,6 +92,12 @@ class Options(urwid.WidgetWrap): lambda: master.options.tcp_hosts, self.tcp_hosts ), + select.Option( + "Don't Verify SSL/TLS Certificates", + "V", + lambda: master.server.config.ssl_insecure, + master.options.toggler("ssl_insecure") + ), select.Heading("Utility"), select.Option( diff --git a/mitmproxy/options.py b/mitmproxy/options.py index bdc0db4ed..75798381d 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -73,7 +73,7 @@ class Options(optmanager.OptManager): upstream_auth = "", # type: str ssl_version_client="secure", # type: str ssl_version_server="secure", # type: str - ssl_verify_upstream_cert=False, # type: bool + ssl_insecure=False, # type: bool ssl_verify_upstream_trusted_cadir=None, # type: str ssl_verify_upstream_trusted_ca=None, # type: str tcp_hosts = (), # type: Sequence[str] @@ -130,7 +130,7 @@ class Options(optmanager.OptManager): self.upstream_auth = upstream_auth self.ssl_version_client = ssl_version_client self.ssl_version_server = ssl_version_server - self.ssl_verify_upstream_cert = ssl_verify_upstream_cert + self.ssl_insecure = ssl_insecure self.ssl_verify_upstream_trusted_cadir = ssl_verify_upstream_trusted_cadir self.ssl_verify_upstream_trusted_ca = ssl_verify_upstream_trusted_ca self.tcp_hosts = tcp_hosts diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index a74ba7e29..cf75830a5 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -83,24 +83,18 @@ class ProxyConfig: options.changed.connect(self.configure) def configure(self, options, updated): - conflict = all( - [ - options.add_upstream_certs_to_client_chain, - options.ssl_verify_upstream_cert - ] - ) - if conflict: + # type: (mitmproxy.options.Options, Any) -> None + if options.add_upstream_certs_to_client_chain and not options.ssl_insecure: raise exceptions.OptionsError( - "The verify-upstream-cert and add-upstream-certs-to-client-chain " - "options are mutually exclusive. If upstream certificates are verified " - "then extra upstream certificates are not available for inclusion " - "to the client chain." + "The verify-upstream-cert requires certificate verification to be disabled. " + "If upstream certificates are verified then extra upstream certificates are " + "not available for inclusion to the client chain." ) - if options.ssl_verify_upstream_cert: - self.openssl_verification_mode_server = SSL.VERIFY_PEER - else: + if options.ssl_insecure: self.openssl_verification_mode_server = SSL.VERIFY_NONE + else: + self.openssl_verification_mode_server = SSL.VERIFY_PEER self.check_ignore = HostMatcher(options.ignore_hosts) self.check_tcp = HostMatcher(options.tcp_hosts) diff --git a/test/mitmproxy/test_protocol_http2.py b/test/mitmproxy/test_protocol_http2.py index aa096a721..f0fa9a404 100644 --- a/test/mitmproxy/test_protocol_http2.py +++ b/test/mitmproxy/test_protocol_http2.py @@ -102,7 +102,11 @@ class _Http2TestBase(object): @classmethod def get_options(cls): - opts = options.Options(listen_port=0, no_upstream_cert=False) + opts = options.Options( + listen_port=0, + no_upstream_cert=False, + ssl_insecure=True + ) opts.cadir = os.path.join(tempfile.gettempdir(), "mitmproxy") return opts diff --git a/test/mitmproxy/test_proxy.py b/test/mitmproxy/test_proxy.py index 6e790e284..848380189 100644 --- a/test/mitmproxy/test_proxy.py +++ b/test/mitmproxy/test_proxy.py @@ -146,9 +146,9 @@ class TestProcessProxyOptions: "--singleuser", "test") - def test_verify_upstream_cert(self): - p = self.assert_noerr("--verify-upstream-cert") - assert p.openssl_verification_mode_server == SSL.VERIFY_PEER + def test_insecure(self): + p = self.assert_noerr("--insecure") + assert p.openssl_verification_mode_server == SSL.VERIFY_NONE def test_upstream_trusted_cadir(self): expected_dir = "/path/to/a/ca/dir" diff --git a/test/mitmproxy/test_server.py b/test/mitmproxy/test_server.py index 6230fc1ff..a6dffb695 100644 --- a/test/mitmproxy/test_server.py +++ b/test/mitmproxy/test_server.py @@ -2,7 +2,6 @@ import os import socket import time import types -from OpenSSL import SSL from netlib.exceptions import HttpReadDisconnect, HttpException from netlib.tcp import Address @@ -15,6 +14,7 @@ from pathod import pathoc, pathod from mitmproxy.builtins import script from mitmproxy import controller +from mitmproxy import options from mitmproxy.proxy.config import HostMatcher, parse_server_spec from mitmproxy.models import Error, HTTPResponse, HTTPFlow @@ -350,6 +350,15 @@ class TestHTTPSCertfile(tservers.HTTPProxyTest, CommonMixin): assert self.pathod("304") +class TestHTTPSSecureByDefault: + def test_secure_by_default(self): + """ + Certificate verification should be turned on by default. + """ + default_opts = options.Options() + assert not default_opts.ssl_insecure + + class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxyTest): """ @@ -360,11 +369,12 @@ class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxyTest): cn=b"trusted-cert", certs=[ ("trusted-cert", tutils.test_data.path("data/trusted-server.crt")) - ]) + ] + ) def test_verification_w_cadir(self): self.config.options.update( - ssl_verify_upstream_cert = True, + ssl_insecure = False, ssl_verify_upstream_trusted_cadir = tutils.test_data.path( "data/trusted-cadir/" ) @@ -372,10 +382,12 @@ class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxyTest): self.pathoc() def test_verification_w_pemfile(self): - self.config.openssl_verification_mode_server = SSL.VERIFY_PEER - self.config.options.ssl_verify_upstream_trusted_ca = tutils.test_data.path( - "data/trusted-cadir/trusted-ca.pem") - + self.config.options.update( + ssl_insecure = False, + ssl_verify_upstream_trusted_ca = tutils.test_data.path( + "data/trusted-cadir/trusted-ca.pem" + ), + ) self.pathoc() @@ -396,18 +408,9 @@ class TestHTTPSUpstreamServerVerificationWBadCert(tservers.HTTPProxyTest): # We need to make an actual request because the upstream connection is lazy-loaded. return p.request("get:/p/242") - def test_default_verification_w_bad_cert(self): - """Should use no verification.""" - self.config.options.update( - ssl_verify_upstream_trusted_ca = tutils.test_data.path( - "data/trusted-cadir/trusted-ca.pem" - ) - ) - assert self._request().status_code == 242 - def test_no_verification_w_bad_cert(self): self.config.options.update( - ssl_verify_upstream_cert = False, + ssl_insecure = True, ssl_verify_upstream_trusted_ca = tutils.test_data.path( "data/trusted-cadir/trusted-ca.pem" ) @@ -416,7 +419,7 @@ class TestHTTPSUpstreamServerVerificationWBadCert(tservers.HTTPProxyTest): def test_verification_w_bad_cert(self): self.config.options.update( - ssl_verify_upstream_cert = True, + ssl_insecure = False, ssl_verify_upstream_trusted_ca = tutils.test_data.path( "data/trusted-cadir/trusted-ca.pem" ) diff --git a/test/mitmproxy/tservers.py b/test/mitmproxy/tservers.py index d364162c6..1597f59cf 100644 --- a/test/mitmproxy/tservers.py +++ b/test/mitmproxy/tservers.py @@ -120,7 +120,8 @@ class ProxyTestBase(object): return options.Options( listen_port=0, cadir=cls.cadir, - add_upstream_certs_to_client_chain=cls.add_upstream_certs_to_client_chain + add_upstream_certs_to_client_chain=cls.add_upstream_certs_to_client_chain, + ssl_insecure=True, ) From 83102b853f2a8dad3d4c5216db39c6e65ee9ba2b Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 27 Jul 2016 19:38:36 -0700 Subject: [PATCH 13/32] minor fixes --- mitmproxy/builtins/script.py | 4 ++-- mitmproxy/main.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mitmproxy/builtins/script.py b/mitmproxy/builtins/script.py index c960dd1c1..ae1d1b915 100644 --- a/mitmproxy/builtins/script.py +++ b/mitmproxy/builtins/script.py @@ -61,13 +61,13 @@ def scriptenv(path, args): try: yield except Exception: - _, _, tb = sys.exc_info() + etype, value, tb = sys.exc_info() scriptdir = os.path.dirname(os.path.abspath(path)) for i, s in enumerate(reversed(traceback.extract_tb(tb))): tb = tb.tb_next if not os.path.abspath(s[0]).startswith(scriptdir): break - ctx.log.error("Script error: %s" % "".join(traceback.format_tb(tb))) + ctx.log.error("Script error: %s" % "".join(traceback.format_exception(etype, value, tb))) finally: sys.argv = oldargs sys.path.pop() diff --git a/mitmproxy/main.py b/mitmproxy/main.py index 6d44108e1..464c38971 100644 --- a/mitmproxy/main.py +++ b/mitmproxy/main.py @@ -92,6 +92,7 @@ def mitmdump(args=None): # pragma: no cover if args.quiet: args.flow_detail = 0 + master = None try: dump_options = dump.Options(**cmdline.get_common_options(args)) dump_options.flow_detail = args.flow_detail @@ -110,7 +111,7 @@ def mitmdump(args=None): # pragma: no cover sys.exit(1) except (KeyboardInterrupt, _thread.error): pass - if master.has_errored: + if master is None or master.has_errored: print("mitmdump: errors occurred during run", file=sys.stderr) sys.exit(1) From 8b325fd65ab7bb5b2b5120183894315036d68a17 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 27 Jul 2016 21:01:28 -0700 Subject: [PATCH 14/32] improve invalid certificate ux --- mitmproxy/console/master.py | 6 ++-- mitmproxy/console/options.py | 1 - mitmproxy/dump.py | 2 +- mitmproxy/exceptions.py | 6 ++++ mitmproxy/models/http.py | 2 +- mitmproxy/protocol/tls.py | 21 +++--------- mitmproxy/proxy/server.py | 7 ++-- netlib/tcp.py | 63 +++++++++++++++++++++++++----------- test/netlib/test_tcp.py | 12 +++---- 9 files changed, 70 insertions(+), 50 deletions(-) diff --git a/mitmproxy/console/master.py b/mitmproxy/console/master.py index 9a9addc53..7a7ed9fd4 100644 --- a/mitmproxy/console/master.py +++ b/mitmproxy/console/master.py @@ -277,11 +277,11 @@ class ConsoleMaster(flow.FlowMaster): if self.options.verbosity < utils.log_tier(level): return - if level == "error": + if level in ("error", "warn"): signals.status_message.send( - message = "Error: %s" % str(e) + message = "{}: {}".format(level.title(), e) ) - e = urwid.Text(("error", str(e))) + e = urwid.Text((level, str(e))) else: e = urwid.Text(str(e)) self.logbuffer.append(e) diff --git a/mitmproxy/console/options.py b/mitmproxy/console/options.py index d34672d37..2205bf6f1 100644 --- a/mitmproxy/console/options.py +++ b/mitmproxy/console/options.py @@ -8,7 +8,6 @@ from mitmproxy.console import grideditor from mitmproxy.console import palettes from mitmproxy.console import select from mitmproxy.console import signals -from OpenSSL import SSL footer = [ ('heading_key', "enter/space"), ":toggle ", diff --git a/mitmproxy/dump.py b/mitmproxy/dump.py index e59fd23eb..511242249 100644 --- a/mitmproxy/dump.py +++ b/mitmproxy/dump.py @@ -104,7 +104,7 @@ class DumpMaster(flow.FlowMaster): click.secho( e, file=self.options.tfile, - fg="red" if level == "error" else None, + fg=dict(error="red", warn="yellow").get(level), dim=(level == "debug"), err=(level == "error") ) diff --git a/mitmproxy/exceptions.py b/mitmproxy/exceptions.py index 3b41fe1ca..6ca11b258 100644 --- a/mitmproxy/exceptions.py +++ b/mitmproxy/exceptions.py @@ -44,6 +44,12 @@ class ClientHandshakeException(TlsProtocolException): self.server = server +class InvalidServerCertificate(TlsProtocolException): + def __repr__(self): + # In contrast to most others, this is a user-facing error which needs to look good. + return str(self) + + class Socks5ProtocolException(ProtocolException): pass diff --git a/mitmproxy/models/http.py b/mitmproxy/models/http.py index 7781e61fe..d56eb29af 100644 --- a/mitmproxy/models/http.py +++ b/mitmproxy/models/http.py @@ -225,7 +225,7 @@ class HTTPFlow(Flow): def make_error_response(status_code, message, headers=None): - response = status_codes.RESPONSES.get(status_code, "Unknown").encode() + response = status_codes.RESPONSES.get(status_code, "Unknown") body = """ diff --git a/mitmproxy/protocol/tls.py b/mitmproxy/protocol/tls.py index 51f4d80de..d08e2e329 100644 --- a/mitmproxy/protocol/tls.py +++ b/mitmproxy/protocol/tls.py @@ -543,25 +543,12 @@ class TlsLayer(base.Layer): ) tls_cert_err = self.server_conn.ssl_verification_error if tls_cert_err is not None: - self.log( - "TLS verification failed for upstream server at depth %s with error: %s" % - (tls_cert_err['depth'], tls_cert_err['errno']), - "error") - self.log("Ignoring server verification error, continuing with connection", "error") + self.log(str(tls_cert_err), "warn") + self.log("Ignoring server verification error, continuing with connection", "warn") except netlib.exceptions.InvalidCertificateException as e: - tls_cert_err = self.server_conn.ssl_verification_error - self.log( - "TLS verification failed for upstream server at depth %s with error: %s" % - (tls_cert_err['depth'], tls_cert_err['errno']), - "error") - self.log("Aborting connection attempt", "error") six.reraise( - exceptions.TlsProtocolException, - exceptions.TlsProtocolException("Cannot establish TLS with {address} (sni: {sni}): {e}".format( - address=repr(self.server_conn.address), - sni=self.server_sni, - e=repr(e), - )), + exceptions.InvalidServerCertificate, + exceptions.InvalidServerCertificate(str(e)), sys.exc_info()[2] ) except netlib.exceptions.TlsException as e: diff --git a/mitmproxy/proxy/server.py b/mitmproxy/proxy/server.py index 26f2e2948..4fd5755af 100644 --- a/mitmproxy/proxy/server.py +++ b/mitmproxy/proxy/server.py @@ -125,11 +125,14 @@ class ConnectionHandler(object): self.log( "Client Handshake failed. " "The client may not trust the proxy's certificate for {}.".format(e.server), - "error" + "warn" ) self.log(repr(e), "debug") + elif isinstance(e, exceptions.InvalidServerCertificate): + self.log(str(e), "warn") + self.log("Invalid certificate, closing connection. Pass --insecure to disable validation.", "warn") else: - self.log(repr(e), "info") + self.log(repr(e), "warn") self.log(traceback.format_exc(), "debug") # If an error propagates to the topmost level, diff --git a/netlib/tcp.py b/netlib/tcp.py index cf099eddc..4fc6c5b9e 100644 --- a/netlib/tcp.py +++ b/netlib/tcp.py @@ -8,6 +8,10 @@ import time import traceback import binascii + +from typing import Optional # noqa + +from netlib import strutils from six.moves import range import certifi @@ -35,7 +39,7 @@ EINTR = 4 if os.environ.get("NO_ALPN"): HAS_ALPN = False else: - HAS_ALPN = OpenSSL._util.lib.Cryptography_HAS_ALPN + HAS_ALPN = SSL._lib.Cryptography_HAS_ALPN # To enable all SSL methods use: SSLv23 # then add options to disable certain methods @@ -287,16 +291,7 @@ class Reader(_FileLike): raise exceptions.TcpException(repr(e)) elif isinstance(self.o, SSL.Connection): try: - if tuple(int(x) for x in OpenSSL.__version__.split(".")[:2]) > (0, 15): - return self.o.recv(length, socket.MSG_PEEK) - else: - # TODO: remove once a new version is released - # Polyfill for pyOpenSSL <= 0.15.1 - # Taken from https://github.com/pyca/pyopenssl/commit/1d95dea7fea03c7c0df345a5ea30c12d8a0378d2 - buf = SSL._ffi.new("char[]", length) - result = SSL._lib.SSL_peek(self.o._ssl, buf, length) - self.o._raise_ssl_error(self.o._ssl, result) - return SSL._ffi.buffer(buf, result)[:] + return self.o.recv(length, socket.MSG_PEEK) except SSL.Error as e: six.reraise(exceptions.TlsException, exceptions.TlsException(str(e)), sys.exc_info()[2]) else: @@ -436,6 +431,23 @@ def close_socket(sock): sock.close() +class SSLVerificationError: + def __init__(self, errno, depth, message=None): + self.errno = errno + self.depth = depth + if message: + self.message = message + else: + self.message = strutils.native(SSL._ffi.string(SSL._lib.X509_verify_cert_error_string(errno)), "utf8") + + def __str__(self): + return "Certificate Verification Error: {} (errno: {}, depth: {})".format( + self.message, + self.errno, + self.depth + ) + + class _Connection(object): rbufsize = -1 @@ -511,6 +523,7 @@ class _Connection(object): alpn_protos=None, alpn_select=None, alpn_select_callback=None, + sni=None, ): """ Creates an SSL Context. @@ -532,8 +545,14 @@ class _Connection(object): if verify_options is not None: def verify_cert(conn, x509, errno, err_depth, is_cert_verified): if not is_cert_verified: - self.ssl_verification_error = dict(errno=errno, - depth=err_depth) + self.ssl_verification_error = exceptions.InvalidCertificateException( + "Certificate Verification Error for {}: {} (errno: {}, depth: {})".format( + sni, + strutils.native(SSL._ffi.string(SSL._lib.X509_verify_cert_error_string(errno)), "utf8"), + errno, + err_depth + ) + ) return is_cert_verified context.set_verify(verify_options, verify_cert) @@ -609,7 +628,7 @@ class TCPClient(_Connection): self.source_address = source_address self.cert = None self.server_certs = [] - self.ssl_verification_error = None + self.ssl_verification_error = None # type: Optional[SSLVerificationError] self.sni = None @property @@ -671,6 +690,7 @@ class TCPClient(_Connection): context = self.create_ssl_context( alpn_protos=alpn_protos, + sni=sni, **sslctx_kwargs ) self.connection = SSL.Connection(context, self.connection) @@ -682,14 +702,14 @@ class TCPClient(_Connection): self.connection.do_handshake() except SSL.Error as v: if self.ssl_verification_error: - raise exceptions.InvalidCertificateException("SSL handshake error: %s" % repr(v)) + raise self.ssl_verification_error else: raise exceptions.TlsException("SSL handshake error: %s" % repr(v)) else: # Fix for pre v1.0 OpenSSL, which doesn't throw an exception on # certificate validation failure - if verification_mode == SSL.VERIFY_PEER and self.ssl_verification_error is not None: - raise exceptions.InvalidCertificateException("SSL handshake error: certificate verify failed") + if verification_mode == SSL.VERIFY_PEER and self.ssl_verification_error: + raise self.ssl_verification_error self.cert = certutils.SSLCert(self.connection.get_peer_certificate()) @@ -710,9 +730,14 @@ class TCPClient(_Connection): hostname = "no-hostname" ssl_match_hostname.match_hostname(crt, hostname) except (ValueError, ssl_match_hostname.CertificateError) as e: - self.ssl_verification_error = dict(depth=0, errno="Invalid Hostname") + self.ssl_verification_error = exceptions.InvalidCertificateException( + "Certificate Verification Error for {}: {}".format( + sni or repr(self.address), + str(e) + ) + ) if verification_mode == SSL.VERIFY_PEER: - raise exceptions.InvalidCertificateException("Presented certificate for {} is not valid: {}".format(sni, str(e))) + raise self.ssl_verification_error self.ssl_established = True self.rfile.set_descriptor(self.connection) diff --git a/test/netlib/test_tcp.py b/test/netlib/test_tcp.py index 273427d51..dc2f4e7e7 100644 --- a/test/netlib/test_tcp.py +++ b/test/netlib/test_tcp.py @@ -213,7 +213,7 @@ class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase): # Verification errors should be saved even if connection isn't aborted # aborted - assert c.ssl_verification_error is not None + assert c.ssl_verification_error testval = b"echo!\n" c.wfile.write(testval) @@ -226,7 +226,7 @@ class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase): c.convert_to_ssl(verify_options=SSL.VERIFY_NONE) # Verification errors should be saved even if connection isn't aborted - assert c.ssl_verification_error is not None + assert c.ssl_verification_error testval = b"echo!\n" c.wfile.write(testval) @@ -243,11 +243,11 @@ class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase): ca_pemfile=tutils.test_data.path("data/verificationcerts/trusted-root.crt") ) - assert c.ssl_verification_error is not None + assert c.ssl_verification_error # Unknown issuing certificate authority for first certificate - assert c.ssl_verification_error['errno'] == 18 - assert c.ssl_verification_error['depth'] == 0 + assert "errno: 18" in str(c.ssl_verification_error) + assert "depth: 0" in str(c.ssl_verification_error) class TestSSLUpstreamCertVerificationWBadHostname(tservers.ServerTestBase): @@ -276,7 +276,7 @@ class TestSSLUpstreamCertVerificationWBadHostname(tservers.ServerTestBase): verify_options=SSL.VERIFY_PEER, ca_pemfile=tutils.test_data.path("data/verificationcerts/trusted-root.crt") ) - assert c.ssl_verification_error is not None + assert c.ssl_verification_error class TestSSLUpstreamCertVerificationWValidCertChain(tservers.ServerTestBase): From f54b302a591ea887392a841e8e68a402f773b324 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 27 Jul 2016 22:44:06 -0700 Subject: [PATCH 15/32] fix cert verification tests, remove leftovers --- netlib/tcp.py | 19 +--- test/mitmproxy/data/servercert/9da13359.0 | 21 +++++ .../mitmproxy/data/servercert/self-signed.pem | 46 ++++++++++ .../data/servercert/trusted-leaf.pem | 45 +++++++++ .../data/servercert/trusted-root.pem | 48 ++++++++++ test/mitmproxy/data/trusted-cadir/8117bdb9.0 | 14 --- test/mitmproxy/data/trusted-cadir/9d45e6a9.0 | 14 --- .../data/trusted-cadir/trusted-ca.pem | 14 --- test/mitmproxy/data/trusted-server.crt | 33 ------- test/mitmproxy/data/untrusted-server.crt | 32 ------- test/mitmproxy/test_server.py | 92 ++++++++++--------- 11 files changed, 211 insertions(+), 167 deletions(-) create mode 100644 test/mitmproxy/data/servercert/9da13359.0 create mode 100644 test/mitmproxy/data/servercert/self-signed.pem create mode 100644 test/mitmproxy/data/servercert/trusted-leaf.pem create mode 100644 test/mitmproxy/data/servercert/trusted-root.pem delete mode 100644 test/mitmproxy/data/trusted-cadir/8117bdb9.0 delete mode 100644 test/mitmproxy/data/trusted-cadir/9d45e6a9.0 delete mode 100644 test/mitmproxy/data/trusted-cadir/trusted-ca.pem delete mode 100644 test/mitmproxy/data/trusted-server.crt delete mode 100644 test/mitmproxy/data/untrusted-server.crt diff --git a/netlib/tcp.py b/netlib/tcp.py index 4fc6c5b9e..e5c841655 100644 --- a/netlib/tcp.py +++ b/netlib/tcp.py @@ -431,23 +431,6 @@ def close_socket(sock): sock.close() -class SSLVerificationError: - def __init__(self, errno, depth, message=None): - self.errno = errno - self.depth = depth - if message: - self.message = message - else: - self.message = strutils.native(SSL._ffi.string(SSL._lib.X509_verify_cert_error_string(errno)), "utf8") - - def __str__(self): - return "Certificate Verification Error: {} (errno: {}, depth: {})".format( - self.message, - self.errno, - self.depth - ) - - class _Connection(object): rbufsize = -1 @@ -628,7 +611,7 @@ class TCPClient(_Connection): self.source_address = source_address self.cert = None self.server_certs = [] - self.ssl_verification_error = None # type: Optional[SSLVerificationError] + self.ssl_verification_error = None # type: Optional[exceptions.InvalidCertificateException] self.sni = None @property diff --git a/test/mitmproxy/data/servercert/9da13359.0 b/test/mitmproxy/data/servercert/9da13359.0 new file mode 100644 index 000000000..b22e4d20d --- /dev/null +++ b/test/mitmproxy/data/servercert/9da13359.0 @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAPAfPQGCV/Z4MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTAxMTY0ODAxWhcNMTgwODIxMTY0ODAxWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEArp8LD34JhKCwcQbwIYQMg4+eCgLVN8fwB7+/qOfJbArPs0djFBN+F7c6 +HGvMr24BKUk5u8pn4dPtNurm/vPC8ovNGmcXz62BQJpcMX2veVdRsF7yNwhNacNJ +Arq+70zNMwYBznx0XUxMF6j6nVFf3AW6SU04ylT4Mp3SY/BUUDAdfl1eRo0mPLNS +8rpsN+8YBw1Q7SCuBRVqpOgVIsL88svgQUSOlzvMZPBpG/cmB3BNKNrltwb5iFEI +1jAV7uSj5IcIuNO/246kfsDVPTFMJIzav/CUoidd5UNw+SoFDlzh8sA7L1Bm7D1/ +3KHYSKswGsSR3kynAl10w/SJKDtn8wIDAQABo1AwTjAdBgNVHQ4EFgQUgOcrtxBX +LxbpnOT65d+vpfyWUkgwHwYDVR0jBBgwFoAUgOcrtxBXLxbpnOT65d+vpfyWUkgw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEE9bFmUCA+6cvESKPoi2 +TGSpV652d0xd2U66LpEXeiWRJFLz8YGgoJCx3QFGBscJDXxrLxrBBBV/tCpEqypo +pYIqsawH7M66jpOr83Us3M8JC2eFBZJocMpXxdytWqHik5VKZNx6VQFT8bS7+yVC +VoUKePhlgcg+pmo41qjqieBNKRMh/1tXS77DI1lgO5wZLVrLXcdqWuDpmaQOKJeq +G/nxytCW/YJA7bFn/8Gjy8DYypJSeeaKu7o3P3+ONJHdIMHb+MdcheDBS9AOFSeo +xI0D5EbO9F873O77l7nbD7B0X34HFN0nGczC4poexIpbDFG3hAPekwZ5KC6VwJLc +1Q== +-----END CERTIFICATE----- diff --git a/test/mitmproxy/data/servercert/self-signed.pem b/test/mitmproxy/data/servercert/self-signed.pem new file mode 100644 index 000000000..cd066a243 --- /dev/null +++ b/test/mitmproxy/data/servercert/self-signed.pem @@ -0,0 +1,46 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIJAJ945xt1FRsfMA0GCSqGSIb3DQEBCwUAMCAxHjAcBgNV +BAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzAeFw0xNTExMDExNjQ4MDJaFw0xODA4 +MjExNjQ4MDJaMCAxHjAcBgNVBAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFxyzPfjgIghOMMnJlW80yB84xC +nJtko3tuyOdozgTCyha2W+NdIKPNZJtWrzN4P0B5PlozCDwfcSYffLs0WZs8LRWv +BfZX8+oX+14qQjKFsiqgO65cTLP3qlPySYPJQQ37vOP1Y5Yf8nQq2mwQdC18hLtT +QOANG6OFoSplpBLsYF+QeoMgqCTa6hrl/5GLmQoDRTjXkv3Sj379AUDMybuBqccm +q5EIqCrE4+xJ8JywJclAVn2YP14baiFrrYCsYYg4sS1Od6xFj+xtpLe7My3AYjB9 +/aeHd8vDiob0cqOW1TFwhqgJKuErfFyg8lZ2hJmStJKyfofWuY/gl/vnvX0CAwEA +AaNQME4wHQYDVR0OBBYEFB8d32zK8eqZIoKw4jXzYzhw4amPMB8GA1UdIwQYMBaA +FB8d32zK8eqZIoKw4jXzYzhw4amPMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAJmo2oKv1OEjZ0Q4yELO6BAnHAkmBKpW+zmLyQa8idxtLVkI9uXk3iqY +GWugkmcUZCTVFRWv/QXQQSex+00IY3x2rdHbtuZwcyKiz2u8WEmfW1rOIwBaFJ1i +v7+SA2aZs6vepN2sE56X54c/YbwQooaKZtOb+djWXYMJrc/Ezj0J7oQIJTptYV8v +/3216yCHRp/KCL7yTLtiw25xKuXNu/gkcd8wZOY9rS2qMUD897MJF0MvgJoauRBd +d4XEYCNKkrIRmfqrkiRQfAZpvpoutH6NCk7KuQYcI0BlOHlsnHHcs/w72EEqHwFq +x6476tW/t8GJDZVD74+pNBcLifXxArE= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsXHLM9+OAiCE4wycmVbzTIHzjEKcm2Sje27I52jOBMLKFrZb +410go81km1avM3g/QHk+WjMIPB9xJh98uzRZmzwtFa8F9lfz6hf7XipCMoWyKqA7 +rlxMs/eqU/JJg8lBDfu84/Vjlh/ydCrabBB0LXyEu1NA4A0bo4WhKmWkEuxgX5B6 +gyCoJNrqGuX/kYuZCgNFONeS/dKPfv0BQMzJu4GpxyarkQioKsTj7EnwnLAlyUBW +fZg/XhtqIWutgKxhiDixLU53rEWP7G2kt7szLcBiMH39p4d3y8OKhvRyo5bVMXCG +qAkq4St8XKDyVnaEmZK0krJ+h9a5j+CX++e9fQIDAQABAoIBAQCT+FvGbych2PJX +0D2KlXqgE0IAdc/YuYymstSwPLKIP9N8KyfnKtK8Jdw+uYOyfRTp8/EuEJ5OXL3j +V6CRD++lRwIlseVb7y5EySjh9oVrUhgn+aSrGucPsHkGNeZeEmbAfWugARLBrvRl +MRMhyHrJL6wT9jIEZInmy9mA3G99IuFW3rS8UR1Yu7zyvhtjvop1xg/wfEUu24Ty +PvMfnwaDcZHCz2tmu2KJvaxSBAG3FKmAqeMvk1Gt5m2keKgw03M+EX0LrM8ybWqn +VwB8tnSyMBLVFLIXMpIiSfpji10+p9fdKFMRF++D6qVwyoxPiIq+yEJapxXiqLea +mkhtJW91AoGBAOvIb7bZvH4wYvi6txs2pygF3ZMjqg/fycnplrmYMrjeeDeeN4v1 +h/5tkN9TeTkHRaN3L7v49NEUDhDyuopLTNfWpYdv63U/BVzvgMm/guacTYkx9whB +OvQ2YekR/WKg7kuyrTZidTDz+mjU+1b8JaWGjiDc6vFwxZA7uWicaGGHAoGBAMCo +y/2AwFGwCR+5bET1nTTyxok6iKo4k6R/7DJe4Bq8VLifoyX3zDlGG/33KN3xVqBU +xnT9gkii1lfX2U+4iM+GOSPl0nG0hOEqEH+vFHszpHybDeNez3FEyIbgOzg6u7sV +NOy+P94L5EMQVEmWp5g6Vm3k9kr92Bd9UacKQPnbAoGAMN8KyMu41i8RVJze9zUM +0K7mjmkGBuRL3x4br7xsRwVVxbF1sfzig0oSjTewGLH5LTi3HC8uD2gowjqNj7yr +4NEM3lXEaDj305uRBkA70bD0IUvJ+FwM7DGZecXQz3Cr8+TFIlCmGc94R+Jddlot +M3IAY69mw0SsroiylYxV1mECgYAcSGtx8rXJCDO+sYTgdsI2ZLGasbogax/ZlWIC +XwU9R4qUc/MKft8/RTiUxvT76BMUhH2B7Tl0GlunF6vyVR/Yf1biGzoSsTKUr40u +gXBbSdCK7mRSjbecZEGf80keTxkCNPHJE4DiwxImej41c2V1JpNLnMI/bhaMFDyp +bgrt4wKBgHFzZgAgM1v07F038tAkIBGrYLukY1ZFBaZoGZ9xHfy/EmLJM3HCHLO5 +8wszMGhMTe2+39EeChwgj0kFaq1YnDiucU74BC57KR1tD59y7l6UnsQXTm4/32j8 +Or6i8GekBibCb97DzzOU0ZK//fNhHTXpDDXsYt5lJUWSmgW+S9Qp +-----END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/data/servercert/trusted-leaf.pem b/test/mitmproxy/data/servercert/trusted-leaf.pem new file mode 100644 index 000000000..71700f2ac --- /dev/null +++ b/test/mitmproxy/data/servercert/trusted-leaf.pem @@ -0,0 +1,45 @@ +-----BEGIN CERTIFICATE----- +MIIC4TCCAckCCQCj6D9oVylb8jANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMB4XDTE1MTEwMTE2NDgwMloXDTE4MDgyMTE2NDgwMlowIDEeMBwG +A1UEAwwVZXhhbXBsZS5taXRtcHJveHkub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAy/L5JYHS7QFhSIsjmd6bJTgs2rdqEn6tsmPBVZKZ7SqCAVjW +hPpEu7Q23akmU6Zm9Fp/vENc3jzxQLlEKhrv7eWmFYSOrCYtbJOz3RQorlwjjfdY +LlNQh1wYUXQX3PN3r3dyYtt5vTtXKc8+aP4M4vX7qlbW+4j4LrQfmPjS0XOdYpu3 +wh+i1ZMIhZye3hpCjwnpjTf7/ff45ZFxtkoi1uzEC/+swr1RSvamY8Foe12Re17Z +5ij8ZB0NIdoSk1tDkY3sJ8iNi35+qartl0UYeG9IUXRwDRrPsEKpF4RxY1+X2bdZ +r6PKb/E4CA5JlMvS5SVmrvxjCVqTQBmTjXfxqwIDAQABMA0GCSqGSIb3DQEBCwUA +A4IBAQBmpSZJrTDvzSlo6P7P7x1LoETzHyVjwgPeqGYw6ndGXeJMN9rhhsFvRsiB +I/aHh58MIlSjti7paikDAoFHB3dBvFHR+JUa/ailWEbcZReWRSE3lV6wFiN3G3lU +OyofR7MKnPW7bv8hSqOLqP1mbupXuQFB5M6vPLRwg5VgiCHI/XBiTvzMamzvNAR3 +UHHZtsJkRqzogYm6K9YJaga7jteSx2nNo+ujLwrxeXsLChTyFMJGnVkp5IyKeNfc +qwlzNncb3y+4KnUdNkPEtuydgAxAfuyXufiFBYRcUWbQ5/9ycgF7131ySaj9f/Y2 +kMsv2jg+soKvwwVYCABsk1KSHtfz +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAy/L5JYHS7QFhSIsjmd6bJTgs2rdqEn6tsmPBVZKZ7SqCAVjW +hPpEu7Q23akmU6Zm9Fp/vENc3jzxQLlEKhrv7eWmFYSOrCYtbJOz3RQorlwjjfdY +LlNQh1wYUXQX3PN3r3dyYtt5vTtXKc8+aP4M4vX7qlbW+4j4LrQfmPjS0XOdYpu3 +wh+i1ZMIhZye3hpCjwnpjTf7/ff45ZFxtkoi1uzEC/+swr1RSvamY8Foe12Re17Z +5ij8ZB0NIdoSk1tDkY3sJ8iNi35+qartl0UYeG9IUXRwDRrPsEKpF4RxY1+X2bdZ +r6PKb/E4CA5JlMvS5SVmrvxjCVqTQBmTjXfxqwIDAQABAoIBAQC956DWq+wbhA1x +3x1nSUBth8E8Z0z9q7dRRFHhvIBXth0X5ADcEa2umj/8ZmSpv2heX2ZRhugSh+yc +t+YgzrRacFwV7ThsU6A4WdBBK2Q19tWke4xAlpOFdtut/Mu7kXkAidiY9ISHD5o5 +9B/I48ZcD3AnTHUiAogV9OL3LbogDD4HasLt4mWkbq8U2thdjxMIvxdg36olJEuo +iAZrAUCPZEXuU89BtvPLUYioe9n90nzkyneGNS0SHxotlEc9ZYK9VTsivtXJb4wB +ptDMCp+TH3tjo8BTGnbnoZEybgyyOEd0UTzxK4DlxnvRVWexFY6NXwPFhIxKlB0Y +Bg8NkAkBAoGBAOiRnmbC5QkqrKrTkLx3fghIHPqgEXPPYgHLSuY3UjTlMb3APXpq +vzQnlCn3QuSse/1fWnQj+9vLVbx1XNgKjzk7dQhn5IUY+mGN4lLmoSnTebxvSQ43 +VAgTYjST9JFmJ3wK4KkWDsEsVao8LAx0h5JEQXUTT5xZpFA2MLztYbgfAoGBAOB/ +MvhLMAwlx8+m/zXMEPLk/KOd2dVZ4q5se8bAT/GiGsi8JUcPnCk140ZZabJqryAp +JFzUHIjfVsS9ejAfocDk1JeIm7Uus4um6fQEKIPMBxI/M/UAwYCXAG9ULXqilbO3 +pTdeeuraVKrTu1Z4ea6x4du1JWKcyDfYfsHepcT1AoGBAM2fskV5G7e3G2MOG3IG +1E/OMpEE5WlXenfLnjVdxDkwS4JRbgnGR7d9JurTyzkTp6ylmfwFtLDoXq15ttTs +wSUBBMCh2tIy+201XV2eu++XIpMQca84C/v352RFTH8hqtdpZqkY74KsCDGzcd6x +SQxxfM5efIzoVPb2crEX0MZRAoGAQ2EqFSfL9flo7UQ8GRN0itJ7mUgJV2WxCZT5 +2X9i/y0eSN1feuKOhjfsTPMNLEWk5kwy48GuBs6xpj8Qa10zGUgVHp4bzdeEgAfK +9DhDSLt1694YZBKkAUpRERj8xXAC6nvWFLZAwjhhbRw7gAqMywgMt/q4i85usYRD +F0ESE/kCgYBbc083PcLmlHbkn/d1i4IcLI6wFk+tZYIEVYDid7xDOgZOBcOTTyYB +BrDzNqbKNexKRt7QHVlwR+VOGMdN5P0hf7oH3SMW23OxBKoQe8pUSGF9a4DjCS1v +vCXMekifb9kIhhUWaG71L8+MaOzNBVAmk1+3NzPZgV/YxHjAWWhGHQ== +-----END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/data/servercert/trusted-root.pem b/test/mitmproxy/data/servercert/trusted-root.pem new file mode 100644 index 000000000..2c75b88eb --- /dev/null +++ b/test/mitmproxy/data/servercert/trusted-root.pem @@ -0,0 +1,48 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAPAfPQGCV/Z4MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTAxMTY0ODAxWhcNMTgwODIxMTY0ODAxWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEArp8LD34JhKCwcQbwIYQMg4+eCgLVN8fwB7+/qOfJbArPs0djFBN+F7c6 +HGvMr24BKUk5u8pn4dPtNurm/vPC8ovNGmcXz62BQJpcMX2veVdRsF7yNwhNacNJ +Arq+70zNMwYBznx0XUxMF6j6nVFf3AW6SU04ylT4Mp3SY/BUUDAdfl1eRo0mPLNS +8rpsN+8YBw1Q7SCuBRVqpOgVIsL88svgQUSOlzvMZPBpG/cmB3BNKNrltwb5iFEI +1jAV7uSj5IcIuNO/246kfsDVPTFMJIzav/CUoidd5UNw+SoFDlzh8sA7L1Bm7D1/ +3KHYSKswGsSR3kynAl10w/SJKDtn8wIDAQABo1AwTjAdBgNVHQ4EFgQUgOcrtxBX +LxbpnOT65d+vpfyWUkgwHwYDVR0jBBgwFoAUgOcrtxBXLxbpnOT65d+vpfyWUkgw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEE9bFmUCA+6cvESKPoi2 +TGSpV652d0xd2U66LpEXeiWRJFLz8YGgoJCx3QFGBscJDXxrLxrBBBV/tCpEqypo +pYIqsawH7M66jpOr83Us3M8JC2eFBZJocMpXxdytWqHik5VKZNx6VQFT8bS7+yVC +VoUKePhlgcg+pmo41qjqieBNKRMh/1tXS77DI1lgO5wZLVrLXcdqWuDpmaQOKJeq +G/nxytCW/YJA7bFn/8Gjy8DYypJSeeaKu7o3P3+ONJHdIMHb+MdcheDBS9AOFSeo +xI0D5EbO9F873O77l7nbD7B0X34HFN0nGczC4poexIpbDFG3hAPekwZ5KC6VwJLc +1Q== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEArp8LD34JhKCwcQbwIYQMg4+eCgLVN8fwB7+/qOfJbArPs0dj +FBN+F7c6HGvMr24BKUk5u8pn4dPtNurm/vPC8ovNGmcXz62BQJpcMX2veVdRsF7y +NwhNacNJArq+70zNMwYBznx0XUxMF6j6nVFf3AW6SU04ylT4Mp3SY/BUUDAdfl1e +Ro0mPLNS8rpsN+8YBw1Q7SCuBRVqpOgVIsL88svgQUSOlzvMZPBpG/cmB3BNKNrl +twb5iFEI1jAV7uSj5IcIuNO/246kfsDVPTFMJIzav/CUoidd5UNw+SoFDlzh8sA7 +L1Bm7D1/3KHYSKswGsSR3kynAl10w/SJKDtn8wIDAQABAoIBAFgMzjDzpqz/sbhs +fS0JPp4gDtqRbx3/bSMbJvNuXPxjvzNxLZ5z7cLbmyu1l7Jlz6QXzkrI1vTiPdzR +OcUY+RYANF252iHYJTKEIzS5YX/X7dL3LT9eqlpIJEqCC8Dygw3VW5fY3Xwl+sB7 +blNhMuro4HQRwi8UBUrQlcPa7Ui5BBi323Q6en+VjYctkqpJHzNKPSqPTbsdLaK+ +B0XuXxFatM09rmeRKZCL71Lk1T8N/l0hqEzej7zxgVD7vG/x1kMFN4T3yCmXCbPa +izGHYr1EBHglm4qMNWveXCZiVJ+wmwCjdjqvggyHiZFXE2N0OCrWPhxQPdqFf5y7 +bUO9U2ECgYEA6GM1UzRnbVpjb20ezFy7dU7rlWM0nHBfG27M3bcXh4HnPpnvKp0/ +8a1WFi4kkRywrNXx8hFEd43vTbdObLpVXScXRKiY3MHmFk4k4hbWuTpmumCubQZO +AWlX6TE0HRKn1wQahgpQcxcWaDN2xJJmRQ1zVmlnNkT48/4kFgRxyykCgYEAwF08 +ngrF35oYoU/x+KKq2NXGeNUzoZMj568dE1oWW0ZFpqCi+DGT+hAbG3yUOBSaPqy9 +zn1obGo0YRlrayvtebz118kG7a/rzY02VcAPlT/GpEhvkZlXTwEK17zRJc1nJrfP +39QAZWZsaOru9NRIg/8HcdG3JPR2MhRD/De9GbsCgYAaiZnBUq6s8jGAu/lUZRKT +JtwIRzfu1XZG77Q9bXcmZlM99t41A5gVxTGbftF2MMyMMDJc7lPfQzocqd4u1GiD +Jr+le4tZSls4GNxlZS5IIL8ycW/5y0qFJr5/RrsoxsSb7UAKJothWTWZ2Karc/xx +zkNpjsfWjrHPSypbyU4lYQKBgFh1R5/BgnatjO/5LGNSok/uFkOQfxqo6BTtYOh6 +P9efO/5A1lBdtBeE+oIsSphzWO7DTtE6uB9Kw2V3Y/83hw+5RjABoG8Cu+OdMURD +eqb+WeFH8g45Pn31E8Bbcq34g5u5YR0jhz8Z13ZzuojZabNRPmIntxmGVSf4S78a +/plrAoGBANMHNng2lyr03nqnHrOM6NXD+60af0YR/YJ+2d/H40RnXxGJ4DXn7F00 +a4vJFPa97uq+xpd0HE+TE+NIrOdVDXPePD2qzBzMTsctGtj30vLzojMOT+Yf/nvO +WxTL5Q8GruJz2Dn0awSZO2z/3A8S1rmpuVZ/jT5NtRrvOSY6hmxF +-----END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/data/trusted-cadir/8117bdb9.0 b/test/mitmproxy/data/trusted-cadir/8117bdb9.0 deleted file mode 100644 index ae78b5465..000000000 --- a/test/mitmproxy/data/trusted-cadir/8117bdb9.0 +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICJzCCAZACCQCo1BdopddN/TANBgkqhkiG9w0BAQUFADBXMQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRAwDgYDVQQDEwdUUlVTVEVEMCAXDTE1MDYxOTE4MDEzMVoYDzIx -MTUwNTI2MTgwMTMxWjBXMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0 -ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQDEwdU -UlVTVEVEMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC00Jf3KrBAmLQWl+Dz -8Qrig8ActB94kv0/Lu03P/2DwOR8kH2h3w4OC3b3CFKX31h7hm/H1PPHq7cIX6IR -fwrYCtBE77UbxklSlrwn06j6YSotz0/dwLEQEFDXWITJq7AyntaiafDHazbbXESN -m/+I/YEl2wKemEHE//qWbeM9kwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAF0NREP3 -X+fTebzJGttzrFkDhGVFKRNyLXblXRVanlGOYF+q8grgZY2ufC/55gqf+ub6FRT5 -gKPhL4V2rqL8UAvCE7jq8ujpVfTB8kRAKC675W2DBZk2EJX9mjlr89t7qXGsI5nF -onpfJ1UtiJshNoV7h/NFHeoag91kx628807n ------END CERTIFICATE----- diff --git a/test/mitmproxy/data/trusted-cadir/9d45e6a9.0 b/test/mitmproxy/data/trusted-cadir/9d45e6a9.0 deleted file mode 100644 index ae78b5465..000000000 --- a/test/mitmproxy/data/trusted-cadir/9d45e6a9.0 +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICJzCCAZACCQCo1BdopddN/TANBgkqhkiG9w0BAQUFADBXMQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRAwDgYDVQQDEwdUUlVTVEVEMCAXDTE1MDYxOTE4MDEzMVoYDzIx -MTUwNTI2MTgwMTMxWjBXMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0 -ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQDEwdU -UlVTVEVEMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC00Jf3KrBAmLQWl+Dz -8Qrig8ActB94kv0/Lu03P/2DwOR8kH2h3w4OC3b3CFKX31h7hm/H1PPHq7cIX6IR -fwrYCtBE77UbxklSlrwn06j6YSotz0/dwLEQEFDXWITJq7AyntaiafDHazbbXESN -m/+I/YEl2wKemEHE//qWbeM9kwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAF0NREP3 -X+fTebzJGttzrFkDhGVFKRNyLXblXRVanlGOYF+q8grgZY2ufC/55gqf+ub6FRT5 -gKPhL4V2rqL8UAvCE7jq8ujpVfTB8kRAKC675W2DBZk2EJX9mjlr89t7qXGsI5nF -onpfJ1UtiJshNoV7h/NFHeoag91kx628807n ------END CERTIFICATE----- diff --git a/test/mitmproxy/data/trusted-cadir/trusted-ca.pem b/test/mitmproxy/data/trusted-cadir/trusted-ca.pem deleted file mode 100644 index ae78b5465..000000000 --- a/test/mitmproxy/data/trusted-cadir/trusted-ca.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICJzCCAZACCQCo1BdopddN/TANBgkqhkiG9w0BAQUFADBXMQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRAwDgYDVQQDEwdUUlVTVEVEMCAXDTE1MDYxOTE4MDEzMVoYDzIx -MTUwNTI2MTgwMTMxWjBXMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0 -ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQDEwdU -UlVTVEVEMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC00Jf3KrBAmLQWl+Dz -8Qrig8ActB94kv0/Lu03P/2DwOR8kH2h3w4OC3b3CFKX31h7hm/H1PPHq7cIX6IR -fwrYCtBE77UbxklSlrwn06j6YSotz0/dwLEQEFDXWITJq7AyntaiafDHazbbXESN -m/+I/YEl2wKemEHE//qWbeM9kwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAF0NREP3 -X+fTebzJGttzrFkDhGVFKRNyLXblXRVanlGOYF+q8grgZY2ufC/55gqf+ub6FRT5 -gKPhL4V2rqL8UAvCE7jq8ujpVfTB8kRAKC675W2DBZk2EJX9mjlr89t7qXGsI5nF -onpfJ1UtiJshNoV7h/NFHeoag91kx628807n ------END CERTIFICATE----- diff --git a/test/mitmproxy/data/trusted-server.crt b/test/mitmproxy/data/trusted-server.crt deleted file mode 100644 index 76f8559a3..000000000 --- a/test/mitmproxy/data/trusted-server.crt +++ /dev/null @@ -1,33 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC8jCCAlugAwIBAgICEAcwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQVUx -EzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMg -UHR5IEx0ZDEQMA4GA1UEAxMHVFJVU1RFRDAgFw0xNTA2MjAwMTE4MjdaGA8yMTE1 -MDUyNzAxMTgyN1owfjELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx -ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UECxMLSU5U -RVJNIFVOSVQxITAfBgNVBAMTGE9SRyBXSVRIIElOVEVSTUVESUFURSBDQTCBnzAN -BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAtRPNKgh4WdYGmU2Ae6Tf2Mbd3oaRI/uY -Qm6aKeYk1i7g41C0vVowNcD/qdNpGUNnai/Kak9anHOYyppNo7zHgf3EO8zQ4NTQ -pkDKsdCqbUQcjGfhjWXKnOw+I5er4Rj+MwM1f5cbwb8bYHiSPmXaxzdL0/SNXGAA -ys/UswgwkU8CAwEAAaOBozCBoDAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBTPkPQW -DAPOIy8mipuEsZcP1694EDBxBgNVHSMEajBooVukWTBXMQswCQYDVQQGEwJBVTET -MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ -dHkgTHRkMRAwDgYDVQQDEwdUUlVTVEVEggkAqNQXaKXXTf0wDQYJKoZIhvcNAQEF -BQADgYEApaPbwonY8l+zSxlY2Fw4WNKfl5nwcTW4fuv/0tZLzvsS6P4hTXxbYJNa -k3hQ1qlrr8DiWJewF85hYvEI2F/7eqS5dhhPTEUFPpsjhbgiqnASvW+WKQIgoY2r -aHgOXi7RNFtTcCgk0UZISWOY7ORLy8Xu6vKrLRjDhyfIbGlqnAs= ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC1E80qCHhZ1gaZTYB7pN/Yxt3ehpEj+5hCbpop5iTWLuDjULS9 -WjA1wP+p02kZQ2dqL8pqT1qcc5jKmk2jvMeB/cQ7zNDg1NCmQMqx0KptRByMZ+GN -Zcqc7D4jl6vhGP4zAzV/lxvBvxtgeJI+ZdrHN0vT9I1cYADKz9SzCDCRTwIDAQAB -AoGAfKHocKnrzEmXuSSy7meI+vfF9kfA1ndxUSg3S+dwK0uQ1mTSQhI1ZIo2bnlo -uU6/e0Lxm0KLJ2wZGjoifjSNTC8pcxIfAQY4kM9fqoUcXVSBVSS2kByTunhNSVZQ -yQyc+UTq9g1zBnJsZAltn7/PaihU4heWgP/++lposuShqmECQQDaG+7l0qul1xak -9kuZgc88BSTfn9iMK2zIQRcVKuidK4dT3QEp0wmWR5Ue8jq8lvTmVTGNGZbHcheh -KhoZfLgLAkEA1IjwAw/8z02yV3lbc2QUjIl9m9lvjHBoE2sGuSfq/cZskLKrGat+ -CVj3spqVAg22tpQwVBuHiipBziWVnEtiTQJAB9FKfchQSLBt6lm9mfHyKJeSm8VR -8Kw5yO+0URjpn4CI6DOasBIVXOKR8LsD6fCLNJpHHWSWZ+2p9SfaKaGzwwJBAM31 -Scld89qca4fzNZkT0goCrvOZeUy6HVE79Q72zPVSFSD/02kT1BaQ3bB5to5/5aD2 -6AKJjwZoPs7bgykrsD0CQBzU8U/8x2dNQnG0QeqaKQu5kKhZSZ9bsawvrCkxSl6b -WAjl/Jehi5bbQ07zQo3cge6qeR38FCWVCHQ/5wNbc54= ------END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/data/untrusted-server.crt b/test/mitmproxy/data/untrusted-server.crt deleted file mode 100644 index 62e586018..000000000 --- a/test/mitmproxy/data/untrusted-server.crt +++ /dev/null @@ -1,32 +0,0 @@ -# untrusted-interm.crt, self-signed ------BEGIN CERTIFICATE----- -MIICdTCCAd4CCQDRSKOnIMbTgDANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJB -VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRQwEgYDVQQLEwtJTlRFUk0gVU5JVDEhMB8GA1UEAxMYT1JHIFdJ -VEggSU5URVJNRURJQVRFIENBMCAXDTE1MDYyMDAxMzY0M1oYDzIxMTUwNTI3MDEz -NjQzWjB+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UE -ChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQLEwtJTlRFUk0gVU5J -VDEhMB8GA1UEAxMYT1JHIFdJVEggSU5URVJNRURJQVRFIENBMIGfMA0GCSqGSIb3 -DQEBAQUAA4GNADCBiQKBgQC1E80qCHhZ1gaZTYB7pN/Yxt3ehpEj+5hCbpop5iTW -LuDjULS9WjA1wP+p02kZQ2dqL8pqT1qcc5jKmk2jvMeB/cQ7zNDg1NCmQMqx0Kpt -RByMZ+GNZcqc7D4jl6vhGP4zAzV/lxvBvxtgeJI+ZdrHN0vT9I1cYADKz9SzCDCR -TwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAGbObAMEajCz4kj7OP2/DB5SRy2+H/G3 -8Qvc43xlMMNQyYxsDuLOFL0UMRzoKgntrrm2nni8jND+tuMt+hv3ZlBcJlYJ6ynR -sC1ITTC/1SwwwO0AFIyduUEIJYr/B3sgcVYPLcEfeDZgmEQc9Tnc01aEu3lx2+l9 -0JTSPL2L9LdA ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC1E80qCHhZ1gaZTYB7pN/Yxt3ehpEj+5hCbpop5iTWLuDjULS9 -WjA1wP+p02kZQ2dqL8pqT1qcc5jKmk2jvMeB/cQ7zNDg1NCmQMqx0KptRByMZ+GN -Zcqc7D4jl6vhGP4zAzV/lxvBvxtgeJI+ZdrHN0vT9I1cYADKz9SzCDCRTwIDAQAB -AoGAfKHocKnrzEmXuSSy7meI+vfF9kfA1ndxUSg3S+dwK0uQ1mTSQhI1ZIo2bnlo -uU6/e0Lxm0KLJ2wZGjoifjSNTC8pcxIfAQY4kM9fqoUcXVSBVSS2kByTunhNSVZQ -yQyc+UTq9g1zBnJsZAltn7/PaihU4heWgP/++lposuShqmECQQDaG+7l0qul1xak -9kuZgc88BSTfn9iMK2zIQRcVKuidK4dT3QEp0wmWR5Ue8jq8lvTmVTGNGZbHcheh -KhoZfLgLAkEA1IjwAw/8z02yV3lbc2QUjIl9m9lvjHBoE2sGuSfq/cZskLKrGat+ -CVj3spqVAg22tpQwVBuHiipBziWVnEtiTQJAB9FKfchQSLBt6lm9mfHyKJeSm8VR -8Kw5yO+0URjpn4CI6DOasBIVXOKR8LsD6fCLNJpHHWSWZ+2p9SfaKaGzwwJBAM31 -Scld89qca4fzNZkT0goCrvOZeUy6HVE79Q72zPVSFSD/02kT1BaQ3bB5to5/5aD2 -6AKJjwZoPs7bgykrsD0CQBzU8U/8x2dNQnG0QeqaKQu5kKhZSZ9bsawvrCkxSl6b -WAjl/Jehi5bbQ07zQo3cge6qeR38FCWVCHQ/5wNbc54= ------END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/test_server.py b/test/mitmproxy/test_server.py index a6dffb695..78e9b5c7b 100644 --- a/test/mitmproxy/test_server.py +++ b/test/mitmproxy/test_server.py @@ -2,21 +2,20 @@ import os import socket import time import types -from netlib.exceptions import HttpReadDisconnect, HttpException -from netlib.tcp import Address import netlib.tutils -from netlib import tcp, http, socks -from netlib.certutils import SSLCert -from netlib.http import authentication, http1 -from netlib.tutils import raises -from pathod import pathoc, pathod - -from mitmproxy.builtins import script from mitmproxy import controller from mitmproxy import options -from mitmproxy.proxy.config import HostMatcher, parse_server_spec +from mitmproxy.builtins import script from mitmproxy.models import Error, HTTPResponse, HTTPFlow +from mitmproxy.proxy.config import HostMatcher, parse_server_spec +from netlib import tcp, http, socks +from netlib.certutils import SSLCert +from netlib.exceptions import HttpReadDisconnect, HttpException +from netlib.http import authentication, http1 +from netlib.tcp import Address +from netlib.tutils import raises +from pathod import pathoc, pathod from . import tutils, tservers @@ -366,29 +365,35 @@ class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxyTest): """ ssl = True ssloptions = pathod.SSLOptions( - cn=b"trusted-cert", + cn=b"example.mitmproxy.org", certs=[ - ("trusted-cert", tutils.test_data.path("data/trusted-server.crt")) + ("example.mitmproxy.org", tutils.test_data.path("data/servercert/trusted-leaf.pem")) ] ) + def _request(self): + p = self.pathoc(sni="example.mitmproxy.org") + return p.request("get:/p/242") + def test_verification_w_cadir(self): self.config.options.update( - ssl_insecure = False, - ssl_verify_upstream_trusted_cadir = tutils.test_data.path( - "data/trusted-cadir/" - ) + ssl_insecure=False, + ssl_verify_upstream_trusted_cadir=tutils.test_data.path( + "data/servercert/" + ), + ssl_verify_upstream_trusted_ca=None, ) - self.pathoc() + assert self._request().status_code == 242 def test_verification_w_pemfile(self): self.config.options.update( - ssl_insecure = False, - ssl_verify_upstream_trusted_ca = tutils.test_data.path( - "data/trusted-cadir/trusted-ca.pem" + ssl_insecure=False, + ssl_verify_upstream_trusted_cadir=None, + ssl_verify_upstream_trusted_ca=tutils.test_data.path( + "data/servercert/trusted-root.pem" ), ) - self.pathoc() + assert self._request().status_code == 242 class TestHTTPSUpstreamServerVerificationWBadCert(tservers.HTTPProxyTest): @@ -398,33 +403,36 @@ class TestHTTPSUpstreamServerVerificationWBadCert(tservers.HTTPProxyTest): """ ssl = True ssloptions = pathod.SSLOptions( - cn=b"untrusted-cert", + cn=b"example.mitmproxy.org", certs=[ - ("untrusted-cert", tutils.test_data.path("data/untrusted-server.crt")) + ("example.mitmproxy.org", tutils.test_data.path("data/servercert/self-signed.pem")) ]) def _request(self): - p = self.pathoc() - # We need to make an actual request because the upstream connection is lazy-loaded. + p = self.pathoc(sni="example.mitmproxy.org") return p.request("get:/p/242") - def test_no_verification_w_bad_cert(self): - self.config.options.update( - ssl_insecure = True, - ssl_verify_upstream_trusted_ca = tutils.test_data.path( - "data/trusted-cadir/trusted-ca.pem" - ) + @classmethod + def get_options(cls): + opts = super(tservers.HTTPProxyTest, cls).get_options() + opts.ssl_verify_upstream_trusted_ca = tutils.test_data.path( + "data/servercert/trusted-root.pem" ) - assert self._request().status_code == 242 + return opts + + def test_no_verification_w_bad_cert(self): + self.config.options.ssl_insecure = True + r = self._request() + assert r.status_code == 242 def test_verification_w_bad_cert(self): - self.config.options.update( - ssl_insecure = False, - ssl_verify_upstream_trusted_ca = tutils.test_data.path( - "data/trusted-cadir/trusted-ca.pem" - ) - ) - assert self._request().status_code == 502 + # We only test for a single invalid cert here. + # Actual testing of different root-causes (invalid hostname, expired, ...) + # is done in netlib. + self.config.options.ssl_insecure = False + r = self._request() + assert r.status_code == 502 + assert b"Certificate Verification Error" in r.raw_content class TestHTTPSNoCommonName(tservers.HTTPProxyTest): @@ -1024,11 +1032,11 @@ class TestProxyChainingSSLReconnect(tservers.HTTPUpstreamProxyTest): class AddUpstreamCertsToClientChainMixin: ssl = True - servercert = tutils.test_data.path("data/trusted-server.crt") + servercert = tutils.test_data.path("data/servercert/trusted-root.pem") ssloptions = pathod.SSLOptions( - cn=b"trusted-cert", + cn=b"example.mitmproxy.org", certs=[ - (b"trusted-cert", servercert) + (b"example.mitmproxy.org", servercert) ] ) From 56d04b57401cbdaea771f0b77bbbc5ffc111d5c7 Mon Sep 17 00:00:00 2001 From: dufferzafar Date: Thu, 28 Jul 2016 07:41:54 -0700 Subject: [PATCH 16/32] Set upper bound on jsbeautifier package to 1.7 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2dca1a275..a1580d8b6 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ setup( "h2>=2.4.0, <3", "html2text>=2016.1.8, <=2016.5.29", "hyperframe>=4.0.1, <5", - "jsbeautifier>=1.6.3" + "jsbeautifier>=1.6.3, <1.7", "lxml>=3.5.0, <=3.6.0", # no wheels for 3.6.1 yet. "Pillow>=3.2, <3.4", "passlib>=1.6.5, <1.7", From 1cffa5f46b84928f1dd211806122d01700345b40 Mon Sep 17 00:00:00 2001 From: dufferzafar Date: Thu, 28 Jul 2016 07:48:10 -0700 Subject: [PATCH 17/32] Use replace while decoding --- mitmproxy/contentviews.py | 5 +++-- test/mitmproxy/test_contentview.py | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mitmproxy/contentviews.py b/mitmproxy/contentviews.py index 304b52411..dacef36db 100644 --- a/mitmproxy/contentviews.py +++ b/mitmproxy/contentviews.py @@ -262,7 +262,7 @@ class ViewHTMLOutline(View): content_types = ["text/html"] def __call__(self, data, **metadata): - data = data.decode("utf-8") + data = data.decode("utf-8", "replace") h = html2text.HTML2Text(baseurl="") h.ignore_images = True h.body_width = 0 @@ -389,7 +389,8 @@ class ViewJavaScript(View): def __call__(self, data, **metadata): opts = jsbeautifier.default_options() opts.indent_size = 2 - res = jsbeautifier.beautify(strutils.native(data), opts) + data = data.decode("utf-8", "replace") + res = jsbeautifier.beautify(data, opts) return "JavaScript", format_text(res) diff --git a/test/mitmproxy/test_contentview.py b/test/mitmproxy/test_contentview.py index aad53b372..66cad47bd 100644 --- a/test/mitmproxy/test_contentview.py +++ b/test/mitmproxy/test_contentview.py @@ -78,6 +78,7 @@ class TestContentView: v = cv.ViewHTMLOutline() s = b"


one

" assert v(s) + assert v(b'\xfe') def test_view_json(self): cv.VIEW_CUTOFF = 100 @@ -106,9 +107,10 @@ class TestContentView: def test_view_javascript(self): v = cv.ViewJavaScript() - assert v("[1, 2, 3]") - assert v("[1, 2, 3") - assert v("function(a){[1, 2, 3]}") + assert v(b"[1, 2, 3]") + assert v(b"[1, 2, 3") + assert v(b"function(a){[1, 2, 3]}") + assert v(b"\xfe") # invalid utf-8 def test_view_css(self): v = cv.ViewCSS() From 3b71c19af34689b6c908956b45817864c14d451c Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 29 Jul 2016 18:37:37 -0700 Subject: [PATCH 18/32] clean up release tool, build linux binaries --- .appveyor.yml | 6 +- .travis.yml | 10 +- CONTRIBUTORS | 52 +++++++-- release/rtool.py | 282 +++++++++++++++++++---------------------------- 4 files changed, 165 insertions(+), 185 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index dae129782..7d5d7b1b0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -31,12 +31,12 @@ deploy_script: ps: | if( ($Env:TOXENV -match "py35") -and - (($Env:APPVEYOR_REPO_BRANCH -match "master") -or ($Env:APPVEYOR_REPO_TAG -match "true")) + (($Env:APPVEYOR_REPO_BRANCH -match "builds") -or ($Env:APPVEYOR_REPO_TAG -match "true")) ) { pip install -U virtualenv .\dev.ps1 - cmd /c "python .\release\rtool.py bdist 2>&1" - python .\release\rtool.py upload-snapshot --bdist + cmd /c "python -u .\release\rtool.py bdist 2>&1" + python -u .\release\rtool.py upload-snapshot --bdist --wheel } cache: diff --git a/.travis.yml b/.travis.yml index e9566ebe6..09474c05b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,9 +23,9 @@ matrix: - os: osx osx_image: xcode7.3 language: generic - env: TOXENV=py35 + env: TOXENV=py35 BDIST=1 - python: 3.5 - env: TOXENV=py35 + env: TOXENV=py35 BDIST=1 - python: 3.5 env: TOXENV=py35 NO_ALPN=1 - python: 2.7 @@ -57,14 +57,14 @@ script: set -o pipefail; python -m tox -- --cov netlib --cov mitmproxy --cov pat after_success: - | - if [[ $TRAVIS_OS_NAME == "osx" && $TRAVIS_PULL_REQUEST == "false" && ($TRAVIS_BRANCH == "master" || -n $TRAVIS_TAG) ]] + if [[ $BDIST == "1" && $TRAVIS_PULL_REQUEST == "false" && ($TRAVIS_BRANCH == "builds" || -n $TRAVIS_TAG) ]] then git fetch --unshallow ./dev.sh 3.5 source venv3.5/bin/activate pip install -e ./release - python ./release/rtool.py bdist - python ./release/rtool.py upload-snapshot --bdist --wheel + python -u ./release/rtool.py bdist + python -u ./release/rtool.py upload-snapshot --bdist fi notifications: diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9609a4217..a08e2eab9 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,10 +1,13 @@ - 1813 Aldo Cortesi - 1228 Maximilian Hils - 282 Thomas Kriechbaumer + 2118 Aldo Cortesi + 1666 Maximilian Hils + 450 Thomas Kriechbaumer + 210 Shadab Zafar + 94 Jason 83 Marcelo Glezer + 36 Clemens 28 Jim Shaver 18 Henrik Nordstrom - 17 Shadab Zafar + 16 Matthew Shao 14 David Weinstein 14 Pedro Worcel 13 Thomas Roth @@ -14,39 +17,47 @@ 10 András Veres-Szentkirályi 10 Chris Czub 10 Sandor Nemes + 10 Zohar Lorberbaum 9 Kyle Morton 9 Legend Tang - 9 Matthew Shao 9 Rouli + 9 ikoz 8 Chandler Abraham 8 Jason A. Novak 7 Alexis Hildebrandt 7 Brad Peabody 7 Matthias Urlichs + 7 dufferzafar + 6 Felix Yan 5 Choongwoo Han 5 Sam Cleveland 5 Tomaz Muraus + 5 Will Coster 5 elitest 5 iroiro123 4 Bryan Bishop + 4 Clemens Brunner 4 Marc Liyanage 4 Michael J. Bazzinotti 4 Valtteri Virtanen 4 Wade 524 4 Youhei Sakurai 4 root + 4 yonder 3 Benjamin Lee 3 Chris Neasbitt 3 Eli Shvartsman - 3 Felix Yan 3 Guillem Anguera 3 Kyle Manna 3 MatthewShao 3 Ryan Welton 3 Zack B + 3 redfast00 + 3 requires.io 2 Anant 2 Bennett Blodinger 2 Colin Bendell + 2 Cory Benfield 2 Heikki Hannikainen 2 Israel Nir 2 Jaime Soriano Pastor @@ -59,34 +70,50 @@ 2 Paul 2 Rob Wills 2 Sean Coates + 2 Steven Van Acker 2 Terry Long 2 Wade Catron 2 alts 2 isra17 2 israel - 2 requires.io + 2 jpkrause + 2 lilydjwg + 2 strohu + 2 依云 + 1 Aditya 1 Andrey Plotnikov 1 Andy Smith + 1 Anthony Zhang + 1 BSalita 1 Ben Lerner 1 Bradley Baetz + 1 Brett Randall 1 Chris Hamant + 1 Christian Frichot 1 Dan Wilbraham 1 David Dworken 1 David Shaw + 1 Doug Freed 1 Doug Lethin + 1 Drake Caraker 1 Eric Entzel 1 Felix Wolfsteller 1 FreeArtMan 1 Gabriel Kirkpatrick 1 Henrik Nordström + 1 Israel Blancas 1 Ivaylo Popov 1 JC 1 Jakub Nawalaniec 1 Jakub Wilk 1 James Billingham + 1 Jason Pepas 1 Jean Regisser + 1 Jonathan Jones 1 Jorge Villacorta 1 Kit Randel + 1 Kostya Esmukov + 1 Linmiao Xu 1 Lucas Cimon 1 M. Utku Altinkaya 1 Mathieu Mitchell @@ -98,27 +125,33 @@ 1 Nick Raptis 1 Nicolas Esteves 1 Oleksandr Sheremet + 1 Parth Ganatra 1 Pritam Baral 1 Rich Somerfield 1 Rory McCann 1 Rune Halvorsen 1 Ryo Onodera + 1 Sachin Kelkar 1 Sahn Lam 1 Seppo Yli-Olli 1 Sergey Chipiga 1 Stefan Wärting 1 Steve Phillips - 1 Steven Van Acker + 1 Steven Noble 1 Suyash + 1 Tai Dickerson 1 Tarashish Mishra 1 TearsDontFalls + 1 Thiago Arrais 1 Tim Becker 1 Timothy Elliott 1 Ulrich Petri 1 Vyacheslav Bakhmutov - 1 Will Coster + 1 Wes Turner + 1 Yoginski 1 Yuangxuan Wang 1 capt8bit + 1 cle1000 1 davidpshaw 1 deployable 1 gecko655 @@ -133,4 +166,3 @@ 1 sethp-jive 1 starenka 1 vzvu3k6k - 1 依云 diff --git a/release/rtool.py b/release/rtool.py index 4e43eaefd..051a51889 100755 --- a/release/rtool.py +++ b/release/rtool.py @@ -1,29 +1,30 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from __future__ import absolute_import, print_function, division -from os.path import join + import contextlib +import fnmatch import os +import platform +import runpy +import shlex import shutil import subprocess -import re -import shlex -import runpy -import zipfile +import sys import tarfile -import platform +import zipfile +from os.path import join, normpath, dirname, exists, basename + import click import pysftp -import fnmatch # https://virtualenv.pypa.io/en/latest/userguide.html#windows-notes # scripts and executables on Windows go in ENV\Scripts\ instead of ENV/bin/ -import sys - if platform.system() == "Windows": VENV_BIN = "Scripts" else: VENV_BIN = "bin" +# ZipFile and tarfile have slightly different APIs. Fix that. if platform.system() == "Windows": def Archive(name): a = zipfile.ZipFile(name, "w") @@ -33,13 +34,13 @@ else: def Archive(name): return tarfile.open(name, "w:gz") -RELEASE_DIR = join(os.path.dirname(os.path.realpath(__file__))) -DIST_DIR = join(RELEASE_DIR, "dist") -ROOT_DIR = os.path.normpath(join(RELEASE_DIR, "..")) -RELEASE_SPEC_DIR = join(RELEASE_DIR, "specs") -VERSION_FILE = join(ROOT_DIR, "netlib/version.py") +ROOT_DIR = normpath(join(dirname(__file__), "..")) +RELEASE_DIR = join(ROOT_DIR, "release") BUILD_DIR = join(RELEASE_DIR, "build") +DIST_DIR = join(RELEASE_DIR, "dist") + +PYINSTALLER_SPEC = join(RELEASE_DIR, "specs") PYINSTALLER_TEMP = join(BUILD_DIR, "pyinstaller") PYINSTALLER_DIST = join(BUILD_DIR, "binaries") @@ -47,27 +48,35 @@ VENV_DIR = join(BUILD_DIR, "venv") VENV_PIP = join(VENV_DIR, VENV_BIN, "pip") VENV_PYINSTALLER = join(VENV_DIR, VENV_BIN, "pyinstaller") -project = { - "name": "mitmproxy", - "tools": ["pathod", "pathoc", "mitmproxy", "mitmdump", "mitmweb"], - "bdists": { - "mitmproxy": ["mitmproxy", "mitmdump", "mitmweb"], - "pathod": ["pathoc", "pathod"] - }, - "dir": ROOT_DIR, - "python_version": "py2.py3", +# Project Configuration +VERSION_FILE = join(ROOT_DIR, "netlib", "version.py") +PROJECT_NAME = "mitmproxy" +PYTHON_VERSION = "py2.py3" +BDISTS = { + "mitmproxy": ["mitmproxy", "mitmdump", "mitmweb"], + "pathod": ["pathoc", "pathod"] } if platform.system() == "Windows": - project["tools"].remove("mitmproxy") - project["bdists"]["mitmproxy"].remove("mitmproxy") + BDISTS["mitmproxy"].remove("mitmproxy") + +TOOLS = [ + tool + for tools in BDISTS.values() + for tool in tools +] -def get_version(): +def get_version() -> str: return runpy.run_path(VERSION_FILE)["VERSION"] -def get_snapshot_version(): - last_tag, tag_dist, commit = git("describe --tags --long").strip().rsplit(b"-", 2) +def git(args: str) -> str: + with chdir(ROOT_DIR): + return subprocess.check_output(["git"] + shlex.split(args)).decode() + + +def get_snapshot_version() -> str: + last_tag, tag_dist, commit = git("describe --tags --long").strip().rsplit("-", 2) tag_dist = int(tag_dist) if tag_dist == 0: return get_version() @@ -76,11 +85,11 @@ def get_snapshot_version(): return "{version}dev{tag_dist:04}-0x{commit}".format( version=get_version(), # this should already be the next version tag_dist=tag_dist, - commit=commit.decode() + commit=commit ) -def archive_name(project): +def archive_name(bdist: str) -> str: platform_tag = { "Darwin": "osx", "Windows": "win32", @@ -91,18 +100,18 @@ def archive_name(project): else: ext = "tar.gz" return "{project}-{version}-{platform}.{ext}".format( - project=project, + project=bdist, version=get_version(), platform=platform_tag, ext=ext ) -def wheel_name(): +def wheel_name() -> str: return "{project}-{version}-{py_version}-none-any.whl".format( - project=project["name"], + project=PROJECT_NAME, version=get_version(), - py_version=project["python_version"] + py_version=PYTHON_VERSION ) @@ -119,18 +128,13 @@ def empty_pythonpath(): @contextlib.contextmanager -def chdir(path): +def chdir(path: str): old_dir = os.getcwd() os.chdir(path) yield os.chdir(old_dir) -def git(args): - with chdir(ROOT_DIR): - return subprocess.check_output(["git"] + shlex.split(args)) - - @click.group(chain=True) def cli(): """ @@ -147,95 +151,79 @@ def contributors(): with chdir(ROOT_DIR): print("Updating CONTRIBUTORS...") contributors_data = git("shortlog -n -s") - with open("CONTRIBUTORS", "w") as f: - f.write(contributors_data) + with open("CONTRIBUTORS", "wb") as f: + f.write(contributors_data.encode()) -@cli.command("set-version") -@click.argument('version') -def set_version(version): +@cli.command("wheel") +def make_wheel(): """ - Update version information - """ - print("Update versions...") - version = ", ".join(version.split(".")) - print("Update %s..." % VERSION_FILE) - with open(VERSION_FILE, "rb") as f: - content = f.read() - new_content = re.sub( - r"IVERSION\s*=\s*\([\d,\s]+\)", "IVERSION = (%s)" % version, - content - ) - with open(VERSION_FILE, "wb") as f: - f.write(new_content) - - -@cli.command("wheels") -def wheels(): - """ - Build wheels + Build wheel """ with empty_pythonpath(): - print("Building release...") - if os.path.exists(DIST_DIR): + if exists(DIST_DIR): shutil.rmtree(DIST_DIR) - print("Creating wheel for %s ..." % project["name"]) + print("Creating wheel...") subprocess.check_call( [ "python", "./setup.py", "-q", "bdist_wheel", "--dist-dir", DIST_DIR, "--universal" ], - cwd=project["dir"] + cwd=ROOT_DIR ) print("Creating virtualenv for test install...") - if os.path.exists(VENV_DIR): + if exists(VENV_DIR): shutil.rmtree(VENV_DIR) subprocess.check_call(["virtualenv", "-q", VENV_DIR]) with chdir(DIST_DIR): - print("Installing %s..." % project["name"]) + print("Install wheel into virtualenv...") # lxml... if platform.system() == "Windows" and sys.version_info[0] == 3: - subprocess.check_call([VENV_PIP, "install", "-q", "https://snapshots.mitmproxy.org/misc/lxml-3.6.0-cp35-cp35m-win32.whl"]) + subprocess.check_call( + [VENV_PIP, "install", "-q", "https://snapshots.mitmproxy.org/misc/lxml-3.6.0-cp35-cp35m-win32.whl"] + ) subprocess.check_call([VENV_PIP, "install", "-q", wheel_name()]) - print("Running binaries...") - for tool in project["tools"]: + print("Running tools...") + for tool in TOOLS: tool = join(VENV_DIR, VENV_BIN, tool) print("> %s --version" % tool) - print(subprocess.check_output([tool, "--version"])) + print(subprocess.check_output([tool, "--version"]).decode()) print("Virtualenv available for further testing:") - print("source %s" % os.path.normpath(join(VENV_DIR, VENV_BIN, "activate"))) + print("source %s" % normpath(join(VENV_DIR, VENV_BIN, "activate"))) @cli.command("bdist") -@click.option("--use-existing-wheels/--no-use-existing-wheels", default=False) +@click.option("--use-existing-wheel/--no-use-existing-wheel", default=False) @click.argument("pyinstaller_version", envvar="PYINSTALLER_VERSION", default="PyInstaller~=3.1.1") +@click.argument("setuptools_version", envvar="SETUPTOOLS_VERSION", default="setuptools>=25.1.0,!=25.1.1") @click.pass_context -def bdist(ctx, use_existing_wheels, pyinstaller_version): +def make_bdist(ctx, use_existing_wheel, pyinstaller_version, setuptools_version): """ Build a binary distribution """ - if os.path.exists(PYINSTALLER_TEMP): + if exists(PYINSTALLER_TEMP): shutil.rmtree(PYINSTALLER_TEMP) - if os.path.exists(PYINSTALLER_DIST): + if exists(PYINSTALLER_DIST): shutil.rmtree(PYINSTALLER_DIST) - if not use_existing_wheels: - ctx.invoke(wheels) + if not use_existing_wheel: + ctx.invoke(make_wheel) - print("Installing PyInstaller...") - subprocess.check_call([VENV_PIP, "install", "-q", pyinstaller_version]) + print("Installing PyInstaller and setuptools...") + subprocess.check_call([VENV_PIP, "install", "-q", pyinstaller_version, setuptools_version]) + print(subprocess.check_output([VENV_PIP, "freeze"]).decode()) - for bdist_project, tools in project["bdists"].items(): - with Archive(join(DIST_DIR, archive_name(bdist_project))) as archive: + for bdist, tools in BDISTS.items(): + with Archive(join(DIST_DIR, archive_name(bdist))) as archive: for tool in tools: # This is PyInstaller, so it messes up paths. # We need to make sure that we are in the spec folder. - with chdir(RELEASE_SPEC_DIR): + with chdir(PYINSTALLER_SPEC): print("Building %s binary..." % tool) subprocess.check_call( [ @@ -255,10 +243,10 @@ def bdist(ctx, use_existing_wheels, pyinstaller_version): if platform.system() == "Windows": executable += ".exe" print("> %s --version" % executable) - subprocess.check_call([executable, "--version"]) + print(subprocess.check_output([executable, "--version"]).decode()) - archive.add(executable, os.path.basename(executable)) - print("Packed {}.".format(archive_name(bdist_project))) + archive.add(executable, basename(executable)) + print("Packed {}.".format(archive_name(bdist))) @cli.command("upload-release") @@ -298,90 +286,50 @@ def upload_snapshot(host, port, user, private_key, private_key_password, wheel, username=user, private_key=private_key, private_key_pass=private_key_password) as sftp: - - dir_name = "snapshots/v{}".format(get_version()) - sftp.makedirs(dir_name) - with sftp.cd(dir_name): - files = [] - if wheel: - files.append(wheel_name()) - for bdist in project["bdists"].keys(): + dir_name = "snapshots/v{}".format(get_version()) + sftp.makedirs(dir_name) + with sftp.cd(dir_name): + files = [] + if wheel: + files.append(wheel_name()) + if bdist: + for bdist in BDISTS.keys(): files.append(archive_name(bdist)) - for f in files: - local_path = join(DIST_DIR, f) - remote_filename = f.replace(get_version(), get_snapshot_version()) - symlink_path = "../{}".format(f.replace(get_version(), "latest")) + for f in files: + local_path = join(DIST_DIR, f) + remote_filename = f.replace(get_version(), get_snapshot_version()) + symlink_path = "../{}".format(f.replace(get_version(), "latest")) - # Delete old versions - old_version = f.replace(get_version(), "*") - for f_old in sftp.listdir(): - if fnmatch.fnmatch(f_old, old_version): - print("Removing {}...".format(f_old)) - sftp.remove(f_old) + # Upload new version + print("Uploading {} as {}...".format(f, remote_filename)) + with click.progressbar(length=os.stat(local_path).st_size) as bar: + # We hide the file during upload + sftp.put( + local_path, + "." + remote_filename, + callback=lambda done, total: bar.update(done - bar.pos) + ) - # Upload new version - print("Uploading {} as {}...".format(f, remote_filename)) - with click.progressbar(length=os.stat(local_path).st_size) as bar: - sftp.put( - local_path, - "." + remote_filename, - callback=lambda done, total: bar.update(done - bar.pos) - ) - # We hide the file during upload. - sftp.rename("." + remote_filename, remote_filename) + # Delete old versions + old_version = f.replace(get_version(), "*") + for f_old in sftp.listdir(): + if fnmatch.fnmatch(f_old, old_version): + print("Removing {}...".format(f_old)) + sftp.remove(f_old) - # update symlink for the latest release - if sftp.lexists(symlink_path): - print("Removing {}...".format(symlink_path)) - sftp.remove(symlink_path) + # Show new version + sftp.rename("." + remote_filename, remote_filename) + + # update symlink for the latest release + if sftp.lexists(symlink_path): + print("Removing {}...".format(symlink_path)) + sftp.remove(symlink_path) + if f != wheel_name(): + # "latest" isn't a proper wheel version, so this could not be installed. + # https://github.com/mitmproxy/mitmproxy/issues/1065 sftp.symlink("v{}/{}".format(get_version(), remote_filename), symlink_path) -@cli.command("wizard") -@click.option('--next-version', prompt=True) -@click.option('--username', prompt="PyPI Username") -@click.password_option(confirmation_prompt=False, prompt="PyPI Password") -@click.option('--repository', default="pypi") -@click.pass_context -def wizard(ctx, next_version, username, password, repository): - """ - Interactive Release Wizard - """ - is_dirty = git("status --porcelain") - if is_dirty: - raise RuntimeError("Repository is not clean.") - - # update contributors file - ctx.invoke(contributors) - - # Build test release - ctx.invoke(bdist) - - try: - click.confirm("Please test the release now. Is it ok?", abort=True) - except click.Abort: - # undo changes - git("checkout CONTRIBUTORS") - raise - - # Everything ok - let's ship it! - git("tag v{}".format(get_version())) - git("push --tags") - ctx.invoke( - upload_release, - username=username, password=password, repository=repository - ) - - click.confirm("Now please wait until CI has built binaries. Finished?") - - # version bump commit - ctx.invoke(set_version, version=next_version) - git("commit -a -m \"bump version\"") - git("push") - - click.echo("All done!") - - if __name__ == "__main__": cli() From 63f64cd66086f302f53456f29f60b1e28c8ee178 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 29 Jul 2016 19:07:48 -0700 Subject: [PATCH 19/32] minor fixes --- .appveyor.yml | 6 +++--- .travis.yml | 2 +- release/rtool.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 7d5d7b1b0..13782ee83 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -5,10 +5,10 @@ environment: CI_DEPS: codecov>=2.0.5 CI_COMMANDS: codecov matrix: - - PYTHON: "C:\\Python27" - TOXENV: "py27" - PYTHON: "C:\\Python35" TOXENV: "py35" + - PYTHON: "C:\\Python27" + TOXENV: "py27" SNAPSHOT_HOST: secure: NeTo57s2rJhCd/mjKHetXVxCFd3uhr8txnjnAXD1tUI= @@ -31,7 +31,7 @@ deploy_script: ps: | if( ($Env:TOXENV -match "py35") -and - (($Env:APPVEYOR_REPO_BRANCH -match "builds") -or ($Env:APPVEYOR_REPO_TAG -match "true")) + (($Env:APPVEYOR_REPO_BRANCH -match "master") -or ($Env:APPVEYOR_REPO_TAG -match "true")) ) { pip install -U virtualenv .\dev.ps1 diff --git a/.travis.yml b/.travis.yml index 09474c05b..a8301ec82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,7 +57,7 @@ script: set -o pipefail; python -m tox -- --cov netlib --cov mitmproxy --cov pat after_success: - | - if [[ $BDIST == "1" && $TRAVIS_PULL_REQUEST == "false" && ($TRAVIS_BRANCH == "builds" || -n $TRAVIS_TAG) ]] + if [[ $BDIST == "1" && $TRAVIS_PULL_REQUEST == "false" && ($TRAVIS_BRANCH == "master" || -n $TRAVIS_TAG) ]] then git fetch --unshallow ./dev.sh 3.5 diff --git a/release/rtool.py b/release/rtool.py index 051a51889..45e1416e6 100755 --- a/release/rtool.py +++ b/release/rtool.py @@ -12,7 +12,7 @@ import subprocess import sys import tarfile import zipfile -from os.path import join, normpath, dirname, exists, basename +from os.path import join, abspath, normpath, dirname, exists, basename import click import pysftp @@ -34,7 +34,7 @@ else: def Archive(name): return tarfile.open(name, "w:gz") -ROOT_DIR = normpath(join(dirname(__file__), "..")) +ROOT_DIR = abspath(join(dirname(__file__), "..")) RELEASE_DIR = join(ROOT_DIR, "release") BUILD_DIR = join(RELEASE_DIR, "build") From 453436367103dd9deb693da5b5ebb18c2c2c86ce Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 29 Jul 2016 19:54:44 -0700 Subject: [PATCH 20/32] add escape_single_quotes=False arg to bytes_to_escaped_str --- netlib/strutils.py | 4 +++- pathod/language/base.py | 8 ++++---- pathod/pathoc.py | 2 +- test/netlib/test_strutils.py | 5 ++++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/netlib/strutils.py b/netlib/strutils.py index 8f27ebb74..4a46b6b1f 100644 --- a/netlib/strutils.py +++ b/netlib/strutils.py @@ -69,7 +69,7 @@ def escape_control_characters(text, keep_spacing=True): return text.translate(trans) -def bytes_to_escaped_str(data, keep_spacing=False): +def bytes_to_escaped_str(data, keep_spacing=False, escape_single_quotes=False): """ Take bytes and return a safe string that can be displayed to the user. @@ -86,6 +86,8 @@ def bytes_to_escaped_str(data, keep_spacing=False): # We always insert a double-quote here so that we get a single-quoted string back # https://stackoverflow.com/questions/29019340/why-does-python-use-different-quotes-for-representing-strings-depending-on-their ret = repr(b'"' + data).lstrip("b")[2:-1] + if not escape_single_quotes: + ret = re.sub(r"(? Date: Sat, 30 Jul 2016 12:42:33 +0200 Subject: [PATCH 21/32] Fix platform import on Linux using python3 Using python3, sys.platform returns "linux" instead of "linux2" using python2. This patch accepts "linux" as well as "linux2". --- mitmproxy/platform/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mitmproxy/platform/__init__.py b/mitmproxy/platform/__init__.py index e1ff7c474..2e3501315 100644 --- a/mitmproxy/platform/__init__.py +++ b/mitmproxy/platform/__init__.py @@ -1,8 +1,9 @@ import sys +import re resolver = None -if sys.platform == "linux2": +if re.match(r"linux(?:2)?", sys.platform): from . import linux resolver = linux.Resolver elif sys.platform == "darwin": From 07f77f086673a065d2f0f34a85b8e9ab6b06b06a Mon Sep 17 00:00:00 2001 From: Vincent Haupert Date: Sat, 30 Jul 2016 12:49:00 +0200 Subject: [PATCH 22/32] Substitute tilde with user's home. When downloding the mitmproxy certificate using mitm.it, '~' currently is not expanded causing a FileNotFoundException. This patch uses expanduser() to replace the initial tilde with the user's home. --- mitmproxy/onboarding/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mitmproxy/onboarding/app.py b/mitmproxy/onboarding/app.py index e26efae8a..491ddbfe9 100644 --- a/mitmproxy/onboarding/app.py +++ b/mitmproxy/onboarding/app.py @@ -48,6 +48,7 @@ class PEM(tornado.web.RequestHandler): def get(self): p = os.path.join(self.request.master.options.cadir, self.filename) + p = os.path.expanduser(p) self.set_header("Content-Type", "application/x-x509-ca-cert") self.set_header( "Content-Disposition", From 6792ec40587bde8dbd7fac67c35038afc126e80b Mon Sep 17 00:00:00 2001 From: Angelo Agatino Nicolosi Date: Sat, 30 Jul 2016 14:43:53 +0200 Subject: [PATCH 23/32] Integrated encode/decoder for brotli --- mitmproxy/console/flowview.py | 2 ++ netlib/encoding.py | 15 +++++++++++++-- netlib/http/message.py | 2 +- netlib/http/request.py | 2 +- setup.py | 1 + test/netlib/test_encoding.py | 12 ++++++++++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/mitmproxy/console/flowview.py b/mitmproxy/console/flowview.py index c354563f0..4cde955a9 100644 --- a/mitmproxy/console/flowview.py +++ b/mitmproxy/console/flowview.py @@ -713,6 +713,7 @@ class FlowView(tabs.Tabs): keys = ( ("gzip", "z"), ("deflate", "d"), + ("brotli", "b"), ), callback = self.encode_callback, args = (conn,) @@ -726,6 +727,7 @@ class FlowView(tabs.Tabs): encoding_map = { "z": "gzip", "d": "deflate", + "b": "brotli", } conn.encode(encoding_map[key]) signals.flow_change.send(self, flow = self.flow) diff --git a/netlib/encoding.py b/netlib/encoding.py index da282194b..9c8acff75 100644 --- a/netlib/encoding.py +++ b/netlib/encoding.py @@ -8,6 +8,7 @@ import collections from io import BytesIO import gzip import zlib +import brotli from typing import Union # noqa @@ -45,7 +46,7 @@ def decode(encoded, encoding, errors='strict'): decoded = custom_decode[encoding](encoded) except KeyError: decoded = codecs.decode(encoded, encoding, errors) - if encoding in ("gzip", "deflate"): + if encoding in ("gzip", "deflate", "br"): _cache = CachedDecode(encoded, encoding, errors, decoded) return decoded except Exception as e: @@ -81,7 +82,7 @@ def encode(decoded, encoding, errors='strict'): encoded = custom_encode[encoding](decoded) except KeyError: encoded = codecs.encode(decoded, encoding, errors) - if encoding in ("gzip", "deflate"): + if encoding in ("gzip", "deflate", "br"): _cache = CachedDecode(encoded, encoding, errors, decoded) return encoded except Exception as e: @@ -113,6 +114,14 @@ def encode_gzip(content): return s.getvalue() +def decode_brotli(content): + return brotli.decompress(content) + + +def encode_brotli(content): + return brotli.compress(content) + + def decode_deflate(content): """ Returns decompressed data for DEFLATE. Some servers may respond with @@ -139,11 +148,13 @@ custom_decode = { "identity": identity, "gzip": decode_gzip, "deflate": decode_deflate, + "br": decode_brotli, } custom_encode = { "identity": identity, "gzip": encode_gzip, "deflate": encode_deflate, + "br": encode_brotli, } __all__ = ["encode", "decode"] diff --git a/netlib/http/message.py b/netlib/http/message.py index be35b8d17..ce92bab15 100644 --- a/netlib/http/message.py +++ b/netlib/http/message.py @@ -248,7 +248,7 @@ class Message(basetypes.Serializable): def encode(self, e): """ - Encodes body with the encoding e, where e is "gzip", "deflate" or "identity". + Encodes body with the encoding e, where e is "gzip", "deflate", "identity", or "br". Any existing content-encodings are overwritten, the content is not decoded beforehand. diff --git a/netlib/http/request.py b/netlib/http/request.py index 061217a34..d59fead45 100644 --- a/netlib/http/request.py +++ b/netlib/http/request.py @@ -337,7 +337,7 @@ class Request(message.Message): self.headers["accept-encoding"] = ( ', '.join( e - for e in {"gzip", "identity", "deflate"} + for e in {"gzip", "identity", "deflate", "br"} if e in accept_encoding ) ) diff --git a/setup.py b/setup.py index a1580d8b6..3915e4218 100644 --- a/setup.py +++ b/setup.py @@ -85,6 +85,7 @@ setup( "tornado>=4.3, <4.5", "urwid>=1.3.1, <1.4", "watchdog>=0.8.3, <0.9", + "brotlipy>=0.3.0, <0.4", ], extras_require={ ':sys_platform == "win32"': [ diff --git a/test/netlib/test_encoding.py b/test/netlib/test_encoding.py index a5e81379d..08e69ec52 100644 --- a/test/netlib/test_encoding.py +++ b/test/netlib/test_encoding.py @@ -21,6 +21,18 @@ def test_gzip(): encoding.decode(b"bogus", "gzip") +def test_brotli(): + assert b"string" == encoding.decode( + encoding.encode( + b"string", + "br" + ), + "br" + ) + with tutils.raises(ValueError): + encoding.decode(b"bogus", "br") + + def test_deflate(): assert b"string" == encoding.decode( encoding.encode( From 10ad56c8533d0c56378bc0832985c5a41caa8935 Mon Sep 17 00:00:00 2001 From: Shadab Zafar Date: Sat, 30 Jul 2016 21:48:54 +0530 Subject: [PATCH 24/32] Rename "Limit" feature to "Filter View" --- mitmproxy/cmdline.py | 8 ++++---- mitmproxy/console/flowlist.py | 10 +++++----- mitmproxy/console/flowview.py | 2 +- mitmproxy/console/master.py | 22 +++++++++++----------- mitmproxy/console/statusbar.py | 6 +++--- mitmproxy/flow/state.py | 6 +++--- mitmproxy/main.py | 2 +- test/mitmproxy/console/test_master.py | 2 +- test/mitmproxy/test_flow.py | 16 ++++++++-------- 9 files changed, 37 insertions(+), 37 deletions(-) diff --git a/mitmproxy/cmdline.py b/mitmproxy/cmdline.py index a6844241c..7a42fc707 100644 --- a/mitmproxy/cmdline.py +++ b/mitmproxy/cmdline.py @@ -773,7 +773,7 @@ def mitmproxy(): help="Show event log." ) parser.add_argument( - "-f", "--follow", + "--follow", action="store_true", dest="follow", help="Follow flow list." ) @@ -792,9 +792,9 @@ def mitmproxy(): help="Intercept filter expression." ) group.add_argument( - "-l", "--limit", action="store", - type=str, dest="limit", default=None, - help="Limit filter expression." + "-f", "--filter", action="store", + type=str, dest="filter", default=None, + help="Filter view expression." ) return parser diff --git a/mitmproxy/console/flowlist.py b/mitmproxy/console/flowlist.py index 43742083d..12caf3157 100644 --- a/mitmproxy/console/flowlist.py +++ b/mitmproxy/console/flowlist.py @@ -18,8 +18,8 @@ def _mkhelp(): ("d", "delete flow"), ("D", "duplicate flow"), ("e", "toggle eventlog"), + ("f", "filter view"), ("F", "toggle follow flow list"), - ("l", "set limit filter pattern"), ("L", "load saved flows"), ("m", "toggle flow mark"), ("M", "toggle marked flow view"), @@ -367,11 +367,11 @@ class FlowListBox(urwid.ListBox): elif key == "G": self.master.state.set_focus(self.master.state.flow_count()) signals.flowlist_change.send(self) - elif key == "l": + elif key == "f": signals.status_prompt.send( - prompt = "Limit", - text = self.master.state.limit_txt, - callback = self.master.set_limit + prompt = "Filter View", + text = self.master.state.filter_txt, + callback = self.master.set_view_filter ) elif key == "L": signals.status_prompt_path.send( diff --git a/mitmproxy/console/flowview.py b/mitmproxy/console/flowview.py index c354563f0..456b77818 100644 --- a/mitmproxy/console/flowview.py +++ b/mitmproxy/console/flowview.py @@ -80,7 +80,7 @@ def _mkhelp(): ("r", "replay request"), ("V", "revert changes to request"), ("v", "view body in external viewer"), - ("w", "save all flows matching current limit"), + ("w", "save all flows matching current view filter"), ("W", "save this flow"), ("x", "delete body"), ("z", "encode/decode a request/response"), diff --git a/mitmproxy/console/master.py b/mitmproxy/console/master.py index 9a9addc53..83a6900c5 100644 --- a/mitmproxy/console/master.py +++ b/mitmproxy/console/master.py @@ -75,8 +75,8 @@ class ConsoleState(flow.State): self.update_focus() return f - def set_limit(self, limit): - ret = super(ConsoleState, self).set_limit(limit) + def set_view_filter(self, txt): + ret = super(ConsoleState, self).set_view_filter(txt) self.set_focus(self.focus) return ret @@ -153,8 +153,8 @@ class ConsoleState(flow.State): last_focus, _ = self.get_focus() nearest_marked = self.get_nearest_matching_flow(last_focus, marked_filter) - self.last_filter = self.limit_txt - self.set_limit(marked_filter) + self.last_filter = self.filter_txt + self.set_view_filter(marked_filter) # Restore Focus if last_focus.marked: @@ -171,7 +171,7 @@ class ConsoleState(flow.State): last_focus, _ = self.get_focus() nearest_marked = self.get_nearest_matching_flow(last_focus, marked_filter) - self.set_limit(self.last_filter) + self.set_view_filter(self.last_filter) self.last_filter = "" # Restore Focus @@ -203,7 +203,7 @@ class Options(mitmproxy.options.Options): eventlog=False, # type: bool follow=False, # type: bool intercept=False, # type: bool - limit=None, # type: Optional[str] + filter=None, # type: Optional[str] palette=None, # type: Optional[str] palette_transparent=False, # type: bool no_mouse=False, # type: bool @@ -212,7 +212,7 @@ class Options(mitmproxy.options.Options): self.eventlog = eventlog self.follow = follow self.intercept = intercept - self.limit = limit + self.filter = filter self.palette = palette self.palette_transparent = palette_transparent self.no_mouse = no_mouse @@ -234,8 +234,8 @@ class ConsoleMaster(flow.FlowMaster): print("Intercept error: {}".format(r), file=sys.stderr) sys.exit(1) - if options.limit: - self.set_limit(options.limit) + if options.filter: + self.set_view_filter(options.filter) self.set_stream_large_bodies(options.stream_large_bodies) @@ -672,8 +672,8 @@ class ConsoleMaster(flow.FlowMaster): def accept_all(self): self.state.accept_all(self) - def set_limit(self, txt): - v = self.state.set_limit(txt) + def set_view_filter(self, txt): + v = self.state.set_view_filter(txt) signals.flowlist_change.send(self) return v diff --git a/mitmproxy/console/statusbar.py b/mitmproxy/console/statusbar.py index 156d1176f..9f0dda43a 100644 --- a/mitmproxy/console/statusbar.py +++ b/mitmproxy/console/statusbar.py @@ -167,10 +167,10 @@ class StatusBar(urwid.WidgetWrap): r.append("[") r.append(("heading_key", "i")) r.append(":%s]" % self.master.state.intercept_txt) - if self.master.state.limit_txt: + if self.master.state.filter_txt: r.append("[") - r.append(("heading_key", "l")) - r.append(":%s]" % self.master.state.limit_txt) + r.append(("heading_key", "f")) + r.append(":%s]" % self.master.state.filter_txt) if self.master.options.stickycookie: r.append("[") r.append(("heading_key", "t")) diff --git a/mitmproxy/flow/state.py b/mitmproxy/flow/state.py index 4287d77bf..efcb2d892 100644 --- a/mitmproxy/flow/state.py +++ b/mitmproxy/flow/state.py @@ -191,7 +191,7 @@ class State(object): self.intercept = None @property - def limit_txt(self): + def filter_txt(self): return getattr(self.view.filt, "pattern", None) def flow_count(self): @@ -225,8 +225,8 @@ class State(object): def load_flows(self, flows): self.flows._extend(flows) - def set_limit(self, txt): - if txt == self.limit_txt: + def set_view_filter(self, txt): + if txt == self.filter_txt: return if txt: f = filt.parse(txt) diff --git a/mitmproxy/main.py b/mitmproxy/main.py index 464c38971..6ae99bddd 100644 --- a/mitmproxy/main.py +++ b/mitmproxy/main.py @@ -68,7 +68,7 @@ def mitmproxy(args=None): # pragma: no cover console_options.eventlog = args.eventlog console_options.follow = args.follow console_options.intercept = args.intercept - console_options.limit = args.limit + console_options.filter = args.filter console_options.no_mouse = args.no_mouse server = process_options(parser, console_options, args) diff --git a/test/mitmproxy/console/test_master.py b/test/mitmproxy/console/test_master.py index b84e4c1c1..fcb87e1b4 100644 --- a/test/mitmproxy/console/test_master.py +++ b/test/mitmproxy/console/test_master.py @@ -76,7 +76,7 @@ class TestConsoleState: self._add_response(c) self._add_request(c) self._add_response(c) - assert not c.set_limit("~s") + assert not c.set_view_filter("~s") assert len(c.view) == 3 assert c.focus == 0 diff --git a/test/mitmproxy/test_flow.py b/test/mitmproxy/test_flow.py index 74992130e..d4bf764c6 100644 --- a/test/mitmproxy/test_flow.py +++ b/test/mitmproxy/test_flow.py @@ -504,13 +504,13 @@ class TestState: c = flow.State() f = tutils.tflow() c.add_flow(f) - c.set_limit("~e") + c.set_view_filter("~e") assert not c.view f.error = tutils.terr() assert c.update_flow(f) assert c.view - def test_set_limit(self): + def test_set_view_filter(self): c = flow.State() f = tutils.tflow() @@ -519,24 +519,24 @@ class TestState: c.add_flow(f) assert len(c.view) == 1 - c.set_limit("~s") - assert c.limit_txt == "~s" + c.set_view_filter("~s") + assert c.filter_txt == "~s" assert len(c.view) == 0 f.response = HTTPResponse.wrap(netlib.tutils.tresp()) c.update_flow(f) assert len(c.view) == 1 - c.set_limit(None) + c.set_view_filter(None) assert len(c.view) == 1 f = tutils.tflow() c.add_flow(f) assert len(c.view) == 2 - c.set_limit("~q") + c.set_view_filter("~q") assert len(c.view) == 1 - c.set_limit("~s") + c.set_view_filter("~s") assert len(c.view) == 1 - assert "Invalid" in c.set_limit("~") + assert "Invalid" in c.set_view_filter("~") def test_set_intercept(self): c = flow.State() From 87017055041a7b96688e69b2acaf5dcb8fb3ab64 Mon Sep 17 00:00:00 2001 From: Manish Kumar Date: Mon, 1 Aug 2016 15:37:26 +0530 Subject: [PATCH 25/32] integer division for python 3 compatibility --- mitmproxy/console/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitmproxy/console/common.py b/mitmproxy/console/common.py index 9fb8b5c92..2eb6a7d93 100644 --- a/mitmproxy/console/common.py +++ b/mitmproxy/console/common.py @@ -379,7 +379,7 @@ def raw_format_flow(f, focus, extended): 4: "code_400", 5: "code_500", } - ccol = codes.get(f["resp_code"] / 100, "code_other") + ccol = codes.get(f["resp_code"] // 100, "code_other") resp.append(fcol(SYMBOL_RETURN, ccol)) if f["resp_is_replay"]: resp.append(fcol(SYMBOL_REPLAY, "replay")) From 1d33d76bfd489e51c63f1e5075c83c2be81fd56a Mon Sep 17 00:00:00 2001 From: Shadab Zafar Date: Tue, 2 Aug 2016 15:58:40 +0530 Subject: [PATCH 26/32] Add missing ssl_insecure option --- mitmproxy/proxy/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index cf75830a5..604045a4f 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -78,6 +78,7 @@ class ProxyConfig: self.check_tcp = None self.certstore = None self.clientcerts = None + self.ssl_insecure = False self.openssl_verification_mode_server = None self.configure(options, set(options.keys())) options.changed.connect(self.configure) From 49ce50d5d043a1d85e6fd34b9305cdb36c1154d4 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 2 Aug 2016 19:08:20 -0700 Subject: [PATCH 27/32] Revert "Add missing ssl_insecure option" This reverts commit 1d33d76bfd489e51c63f1e5075c83c2be81fd56a. --- mitmproxy/proxy/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index 604045a4f..cf75830a5 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -78,7 +78,6 @@ class ProxyConfig: self.check_tcp = None self.certstore = None self.clientcerts = None - self.ssl_insecure = False self.openssl_verification_mode_server = None self.configure(options, set(options.keys())) options.changed.connect(self.configure) From 4ff9dba7d249c7e42ac0623f49c8047ec9b59d01 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 2 Aug 2016 20:08:04 -0700 Subject: [PATCH 28/32] fix #1465, fix ssl_insecure --- mitmproxy/console/options.py | 10 ++++++---- mitmproxy/console/statusbar.py | 4 ++-- mitmproxy/optmanager.py | 10 ++++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/mitmproxy/console/options.py b/mitmproxy/console/options.py index 2205bf6f1..f7fb2f904 100644 --- a/mitmproxy/console/options.py +++ b/mitmproxy/console/options.py @@ -94,7 +94,7 @@ class Options(urwid.WidgetWrap): select.Option( "Don't Verify SSL/TLS Certificates", "V", - lambda: master.server.config.ssl_insecure, + lambda: master.options.ssl_insecure, master.options.toggler("ssl_insecure") ), @@ -140,15 +140,17 @@ class Options(urwid.WidgetWrap): title = urwid.Text("Options") title = urwid.Padding(title, align="left", width=("relative", 100)) title = urwid.AttrWrap(title, "heading") - self._w = urwid.Frame( + w = urwid.Frame( self.lb, header = title ) + super(Options, self).__init__(w) + self.master.loop.widget.footer.update("") signals.update_settings.connect(self.sig_update_settings) - master.options.changed.connect(lambda sender, updated: self.sig_update_settings(sender)) + master.options.changed.connect(self.sig_update_settings) - def sig_update_settings(self, sender): + def sig_update_settings(self, sender, updated=None): self.lb.walker._modified() def keypress(self, size, key): diff --git a/mitmproxy/console/statusbar.py b/mitmproxy/console/statusbar.py index 9f0dda43a..43d68d51a 100644 --- a/mitmproxy/console/statusbar.py +++ b/mitmproxy/console/statusbar.py @@ -124,10 +124,10 @@ class StatusBar(urwid.WidgetWrap): super(StatusBar, self).__init__(urwid.Pile([self.ib, self.master.ab])) signals.update_settings.connect(self.sig_update_settings) signals.flowlist_change.connect(self.sig_update_settings) - master.options.changed.connect(lambda sender, updated: self.sig_update_settings(sender)) + master.options.changed.connect(self.sig_update_settings) self.redraw() - def sig_update_settings(self, sender): + def sig_update_settings(self, sender, updated=None): self.redraw() def keypress(self, *args, **kwargs): diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index 140c7ca8d..92d32b2d4 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -86,7 +86,10 @@ class OptManager(object): """ if attr not in self._opts: raise KeyError("No such option: %s" % attr) - return lambda x: self.__setattr__(attr, x) + + def setter(x): + setattr(self, attr, x) + return setter def toggler(self, attr): """ @@ -95,7 +98,10 @@ class OptManager(object): """ if attr not in self._opts: raise KeyError("No such option: %s" % attr) - return lambda: self.__setattr__(attr, not getattr(self, attr)) + + def toggle(): + setattr(self, attr, not getattr(self, attr)) + return toggle def __repr__(self): options = pprint.pformat(self._opts, indent=4).strip(" {}") From 3aa2d59f627e0fc95167fb76ffbe84330e3a5cc5 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 2 Aug 2016 23:20:58 -0700 Subject: [PATCH 29/32] Update install.rst --- docs/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index 6d82f81f3..6077c3fe1 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -13,7 +13,7 @@ This was tested on a fully patched installation of Ubuntu 14.04. .. code:: bash - sudo apt-get install python-pip python-dev libffi-dev libssl-dev libxml2-dev libxslt1-dev libjpeg8-dev zlib1g-dev + sudo apt-get install python-pip python-dev libffi-dev libssl-dev libxml2-dev libxslt1-dev libjpeg8-dev zlib1g-dev g++ sudo pip install mitmproxy # or pip install --user mitmproxy Once installation is complete you can run :ref:`mitmproxy` or :ref:`mitmdump` from a terminal. From 951885a5dd2f1dd72a67390caa1a07f10f24c8c2 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Tue, 2 Aug 2016 20:36:19 -0700 Subject: [PATCH 30/32] simplify contentview logic --- mitmproxy/builtins/dumper.py | 36 +++-------- mitmproxy/console/flowview.py | 35 ++-------- mitmproxy/contentviews.py | 81 +++++++++++++++-------- netlib/http/__init__.py | 1 + test/mitmproxy/builtins/test_dumper.py | 9 ++- test/mitmproxy/test_contentview.py | 90 +++++++++++++++----------- 6 files changed, 127 insertions(+), 125 deletions(-) diff --git a/mitmproxy/builtins/dumper.py b/mitmproxy/builtins/dumper.py index 59f9349d7..699d4678d 100644 --- a/mitmproxy/builtins/dumper.py +++ b/mitmproxy/builtins/dumper.py @@ -63,30 +63,12 @@ class Dumper(object): ) self.echo(headers, ident=4) if self.flow_detail >= 3: - try: - content = message.content - except ValueError: - content = message.get_content(strict=False) - - if content is None: - self.echo("(content missing)", ident=4) - elif content: - self.echo("") - - try: - _, lines = contentviews.get_content_view( - contentviews.get("Auto"), - content, - headers=getattr(message, "headers", None) - ) - except exceptions.ContentViewException: - s = "Content viewer failed: \n" + traceback.format_exc() - ctx.log.debug(s) - _, lines = contentviews.get_content_view( - contentviews.get("Raw"), - content, - headers=getattr(message, "headers", None) - ) + _, lines, error = contentviews.get_message_content_view( + contentviews.get("Auto"), + message + ) + if error: + ctx.log.debug(error) styles = dict( highlight=dict(bold=True), @@ -105,13 +87,13 @@ class Dumper(object): else: lines_to_echo = lines - lines_to_echo = list(lines_to_echo) - content = u"\r\n".join( u"".join(colorful(line)) for line in lines_to_echo ) + if content: + self.echo("") + self.echo(content) - self.echo(content) if next(lines, None): self.echo("(cut off)", ident=4, dim=True) diff --git a/mitmproxy/console/flowview.py b/mitmproxy/console/flowview.py index d0e6bb114..5c72be091 100644 --- a/mitmproxy/console/flowview.py +++ b/mitmproxy/console/flowview.py @@ -206,36 +206,11 @@ class FlowView(tabs.Tabs): ) def _get_content_view(self, message, viewmode, max_lines, _): - - try: - content = message.content - if content != message.raw_content: - enc = "[decoded {}]".format( - message.headers.get("content-encoding") - ) - else: - enc = None - except ValueError: - content = message.raw_content - enc = "[cannot decode]" - try: - query = None - if isinstance(message, models.HTTPRequest): - query = message.query - description, lines = contentviews.get_content_view( - viewmode, content, headers=message.headers, query=query - ) - except exceptions.ContentViewException: - s = "Content viewer failed: \n" + traceback.format_exc() - signals.add_log(s, "error") - description, lines = contentviews.get_content_view( - contentviews.get("Raw"), content, headers=message.headers - ) - description = description.replace("Raw", "Couldn't parse: falling back to Raw") - - if enc: - description = " ".join([enc, description]) - + description, lines, error = contentviews.get_message_content_view( + viewmode, message + ) + if error: + signals.add_log(error, "error") # Give hint that you have to tab for the response. if description == "No content" and isinstance(message, models.HTTPRequest): description = "No request content (press tab to view response)" diff --git a/mitmproxy/contentviews.py b/mitmproxy/contentviews.py index dacef36db..f95efb99d 100644 --- a/mitmproxy/contentviews.py +++ b/mitmproxy/contentviews.py @@ -14,31 +14,27 @@ requests, the query parameters are passed as the ``query`` keyword argument. """ from __future__ import absolute_import, print_function, division +import cssutils import datetime +import html2text +import jsbeautifier import json import logging -import subprocess -import sys - -from typing import Mapping # noqa - -import html2text import lxml.etree import lxml.html import six +import subprocess +import traceback from PIL import ExifTags from PIL import Image -from six import BytesIO - -import cssutils -import jsbeautifier - from mitmproxy import exceptions from mitmproxy.contrib.wbxml import ASCommandResponse from netlib import http from netlib import multidict -from netlib.http import url from netlib import strutils +from netlib.http import url +from six import BytesIO +from typing import Mapping # noqa try: import pyamf @@ -612,6 +608,39 @@ def safe_to_print(lines, encoding="utf8"): yield clean_line +def get_message_content_view(viewmode, message): + """ + Like get_content_view, but also handles message encoding. + """ + try: + content = message.content + except ValueError: + content = message.raw_content + enc = "[cannot decode]" + else: + if isinstance(message, http.Message) and content != message.raw_content: + enc = "[decoded {}]".format( + message.headers.get("content-encoding") + ) + else: + enc = None + + if content is None: + return "", iter([[("error", "content missing")]]), None + + query = message.query if isinstance(message, http.Request) else None + headers = message.headers if isinstance(message, http.Message) else None + + description, lines, error = get_content_view( + viewmode, content, headers=headers, query=query + ) + + if enc: + description = "{} {}".format(enc, description) + + return description, lines, error + + def get_content_view(viewmode, data, **metadata): """ Args: @@ -619,24 +648,24 @@ def get_content_view(viewmode, data, **metadata): data, **metadata: arguments passed to View instance. Returns: - A (description, content generator) tuple. + A (description, content generator, error) tuple. + If the content view raised an exception generating the view, + the exception is returned in error and the flow is formatted in raw mode. In contrast to calling the views directly, text is always safe-to-print unicode. - - Raises: - ContentViewException, if the content view threw an error. """ try: ret = viewmode(data, **metadata) + if ret is None: + ret = "Couldn't parse: falling back to Raw", get("Raw")(data, **metadata)[1] + desc, content = ret + error = None # Third-party viewers can fail in unexpected ways... - except Exception as e: - six.reraise( - exceptions.ContentViewException, - exceptions.ContentViewException(str(e)), - sys.exc_info()[2] - ) - if not ret: + except Exception: desc = "Couldn't parse: falling back to Raw" _, content = get("Raw")(data, **metadata) - else: - desc, content = ret - return desc, safe_to_print(content) + error = "{} Content viewer failed: \n{}".format( + getattr(viewmode, "name"), + traceback.format_exc() + ) + + return desc, safe_to_print(content), error diff --git a/netlib/http/__init__.py b/netlib/http/__init__.py index af95f4d09..02a37dd37 100644 --- a/netlib/http/__init__.py +++ b/netlib/http/__init__.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, print_function, division from netlib.http.request import Request from netlib.http.response import Response +from netlib.http.message import Message from netlib.http.headers import Headers, parse_content_type from netlib.http.message import decoded from netlib.http import http1, http2, status_codes, multipart diff --git a/test/mitmproxy/builtins/test_dumper.py b/test/mitmproxy/builtins/test_dumper.py index 6287fe861..1c7173e03 100644 --- a/test/mitmproxy/builtins/test_dumper.py +++ b/test/mitmproxy/builtins/test_dumper.py @@ -15,7 +15,7 @@ class TestDumper(mastertest.MasterTest): d = dumper.Dumper() sio = StringIO() - updated = set(["tfile", "flow_detail"]) + updated = {"tfile", "flow_detail"} d.configure(dump.Options(tfile = sio, flow_detail = 0), updated) d.response(tutils.tflow()) assert not sio.getvalue() @@ -66,10 +66,9 @@ class TestDumper(mastertest.MasterTest): class TestContentView(mastertest.MasterTest): - @mock.patch("mitmproxy.contentviews.get_content_view") - def test_contentview(self, get_content_view): - se = exceptions.ContentViewException(""), ("x", iter([])) - get_content_view.side_effect = se + @mock.patch("mitmproxy.contentviews.ViewAuto.__call__") + def test_contentview(self, view_auto): + view_auto.side_effect = exceptions.ContentViewException("") s = state.State() sio = StringIO() diff --git a/test/mitmproxy/test_contentview.py b/test/mitmproxy/test_contentview.py index 66cad47bd..f0afdc0bc 100644 --- a/test/mitmproxy/test_contentview.py +++ b/test/mitmproxy/test_contentview.py @@ -1,3 +1,4 @@ +import mock from mitmproxy.exceptions import ContentViewException from netlib.http import Headers from netlib.http import url @@ -5,6 +6,7 @@ from netlib import multidict import mitmproxy.contentviews as cv from . import tutils +import netlib.tutils try: import pyamf @@ -180,43 +182,6 @@ Larry assert f[0] == "Query" assert [x for x in f[1]] == [[("header", "foo: "), ("text", "bar")]] - def test_get_content_view(self): - r = cv.get_content_view( - cv.get("Raw"), - b"[1, 2, 3]", - headers=Headers(content_type="application/json") - ) - assert "Raw" in r[0] - - r = cv.get_content_view( - cv.get("Auto"), - b"[1, 2, 3]", - headers=Headers(content_type="application/json") - ) - assert r[0] == "JSON" - - r = cv.get_content_view( - cv.get("Auto"), - b"[1, 2", - headers=Headers(content_type="application/json") - ) - assert "Raw" in r[0] - - r = cv.get_content_view( - cv.get("Auto"), - b"[1, 2, 3]", - headers=Headers(content_type="application/vnd.api+json") - ) - assert r[0] == "JSON" - - tutils.raises( - ContentViewException, - cv.get_content_view, - cv.get("AMF"), - b"[1, 2", - headers=Headers() - ) - def test_add_cv(self): class TestContentView(cv.View): name = "test" @@ -233,6 +198,57 @@ Larry ) +def test_get_content_view(): + desc, lines, err = cv.get_content_view( + cv.get("Raw"), + b"[1, 2, 3]", + ) + assert "Raw" in desc + assert list(lines) + assert not err + + desc, lines, err = cv.get_content_view( + cv.get("Auto"), + b"[1, 2, 3]", + headers=Headers(content_type="application/json") + ) + assert desc == "JSON" + + desc, lines, err = cv.get_content_view( + cv.get("JSON"), + b"[1, 2", + ) + assert "Couldn't parse" in desc + + with mock.patch("mitmproxy.contentviews.ViewAuto.__call__") as view_auto: + view_auto.side_effect = ValueError + + desc, lines, err = cv.get_content_view( + cv.get("JSON"), + b"[1, 2", + ) + assert err + assert "Couldn't parse" in desc + + +def test_get_message_content_view(): + r = netlib.tutils.treq() + desc, lines, err = cv.get_message_content_view(cv.get("Raw"), r) + assert desc == "Raw" + + r.encode("gzip") + desc, lines, err = cv.get_message_content_view(cv.get("Raw"), r) + assert desc == "[decoded gzip] Raw" + + r.headers["content-encoding"] = "deflate" + desc, lines, err = cv.get_message_content_view(cv.get("Raw"), r) + assert desc == "[cannot decode] Raw" + + r.content = None + desc, lines, err = cv.get_message_content_view(cv.get("Raw"), r) + assert list(lines) == [[("error", "content missing")]] + + if pyamf: def test_view_amf_request(): v = cv.ViewAMF() From f89455fd0764816e1c480d77258059c3db4ded69 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 4 Aug 2016 15:19:33 -0700 Subject: [PATCH 31/32] minor improvements --- mitmproxy/addons.py | 2 +- mitmproxy/flow/master.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mitmproxy/addons.py b/mitmproxy/addons.py index a4bea9fa2..329d1215f 100644 --- a/mitmproxy/addons.py +++ b/mitmproxy/addons.py @@ -20,7 +20,7 @@ class Addons(object): def add(self, options, *addons): if not addons: - raise ValueError("No adons specified.") + raise ValueError("No addons specified.") self.chain.extend(addons) for i in addons: self.invoke_with_context(i, "start") diff --git a/mitmproxy/flow/master.py b/mitmproxy/flow/master.py index 088375fe6..65a95e44d 100644 --- a/mitmproxy/flow/master.py +++ b/mitmproxy/flow/master.py @@ -233,6 +233,7 @@ class FlowMaster(controller.Master): if self.server_playback: pb = self.do_server_playback(f) if not pb and self.kill_nonreplay: + self.add_log("Killed {}".format(f.request.url), "info") f.kill(self) def replay_request(self, f, block=False): From dcfa7027aed5a8d4aa80aff67fc299298659fb1b Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Thu, 4 Aug 2016 15:39:48 -0700 Subject: [PATCH 32/32] fix tests --- mitmproxy/builtins/dumper.py | 1 - mitmproxy/console/flowview.py | 2 -- netlib/http/__init__.py | 1 + test/mitmproxy/test_contentview.py | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mitmproxy/builtins/dumper.py b/mitmproxy/builtins/dumper.py index 699d4678d..743ca72eb 100644 --- a/mitmproxy/builtins/dumper.py +++ b/mitmproxy/builtins/dumper.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, print_function, division import itertools -import traceback import click diff --git a/mitmproxy/console/flowview.py b/mitmproxy/console/flowview.py index 5c72be091..1c3c4e980 100644 --- a/mitmproxy/console/flowview.py +++ b/mitmproxy/console/flowview.py @@ -3,14 +3,12 @@ from __future__ import absolute_import, print_function, division import math import os import sys -import traceback import urwid from typing import Optional, Union # noqa from mitmproxy import contentviews from mitmproxy import controller -from mitmproxy import exceptions from mitmproxy import models from mitmproxy import utils from mitmproxy.console import common diff --git a/netlib/http/__init__.py b/netlib/http/__init__.py index 02a37dd37..436b59656 100644 --- a/netlib/http/__init__.py +++ b/netlib/http/__init__.py @@ -9,6 +9,7 @@ from netlib.http import http1, http2, status_codes, multipart __all__ = [ "Request", "Response", + "Message", "Headers", "parse_content_type", "decoded", "http1", "http2", "status_codes", "multipart", diff --git a/test/mitmproxy/test_contentview.py b/test/mitmproxy/test_contentview.py index f0afdc0bc..d63ee50e0 100644 --- a/test/mitmproxy/test_contentview.py +++ b/test/mitmproxy/test_contentview.py @@ -224,7 +224,7 @@ def test_get_content_view(): view_auto.side_effect = ValueError desc, lines, err = cv.get_content_view( - cv.get("JSON"), + cv.get("Auto"), b"[1, 2", ) assert err