aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.appveyor.yml4
-rw-r--r--.gitignore2
-rw-r--r--.travis.yml18
-rw-r--r--CHANGELOG16
-rw-r--r--README.rst12
-rw-r--r--docs/certinstall.rst5
-rw-r--r--docs/howmitmproxy.rst8
-rw-r--r--docs/install.rst6
-rw-r--r--docs/scripting/overview.rst24
-rw-r--r--examples/complex/README.md2
-rw-r--r--examples/complex/dns_spoofing.py3
-rw-r--r--examples/complex/har_dump.py43
-rw-r--r--examples/complex/remote_debug.py2
-rw-r--r--examples/complex/tls_passthrough.py14
-rw-r--r--examples/simple/add_header.py5
-rw-r--r--examples/simple/add_header_class.py8
-rw-r--r--examples/simple/custom_contentview.py8
-rw-r--r--examples/simple/custom_option.py8
-rw-r--r--examples/simple/filter_flows.py23
-rw-r--r--examples/simple/io_read_dumpfile.py4
-rw-r--r--examples/simple/io_write_dumpfile.py14
-rw-r--r--examples/simple/log_events.py2
-rw-r--r--examples/simple/modify_body_inject_iframe.py39
-rw-r--r--examples/simple/modify_form.py5
-rw-r--r--examples/simple/modify_querystring.py5
-rw-r--r--examples/simple/redirect_requests.py3
-rw-r--r--examples/simple/script_arguments.py17
-rw-r--r--examples/simple/send_reply_from_proxy.py2
-rw-r--r--examples/simple/upsidedownternet.py4
-rw-r--r--examples/simple/wsgi_flask_app.py8
-rw-r--r--mitmproxy/addonmanager.py192
-rw-r--r--mitmproxy/addons/__init__.py12
-rw-r--r--mitmproxy/addons/anticache.py9
-rw-r--r--mitmproxy/addons/anticomp.py9
-rw-r--r--mitmproxy/addons/check_alpn.py2
-rw-r--r--mitmproxy/addons/check_ca.py2
-rw-r--r--mitmproxy/addons/clientplayback.py49
-rw-r--r--mitmproxy/addons/core.py259
-rw-r--r--mitmproxy/addons/core_option_validation.py4
-rw-r--r--mitmproxy/addons/cut.py151
-rw-r--r--mitmproxy/addons/disable_h2c.py3
-rw-r--r--mitmproxy/addons/dumper.py44
-rw-r--r--mitmproxy/addons/export.py75
-rw-r--r--mitmproxy/addons/intercept.py9
-rw-r--r--mitmproxy/addons/onboarding.py13
-rw-r--r--mitmproxy/addons/onboardingapp/app.py25
-rw-r--r--mitmproxy/addons/onboardingapp/templates/index.html77
-rw-r--r--mitmproxy/addons/proxyauth.py66
-rw-r--r--mitmproxy/addons/readfile.py58
-rw-r--r--mitmproxy/addons/readstdin.py26
-rw-r--r--mitmproxy/addons/replace.py4
-rw-r--r--mitmproxy/addons/save.py93
-rw-r--r--mitmproxy/addons/script.py308
-rw-r--r--mitmproxy/addons/serverplayback.py75
-rw-r--r--mitmproxy/addons/setheaders.py12
-rw-r--r--mitmproxy/addons/stickyauth.py9
-rw-r--r--mitmproxy/addons/stickycookie.py46
-rw-r--r--mitmproxy/addons/streambodies.py6
-rw-r--r--mitmproxy/addons/streamfile.py70
-rw-r--r--mitmproxy/addons/termlog.py16
-rw-r--r--mitmproxy/addons/termstatus.py14
-rw-r--r--mitmproxy/addons/upstream_auth.py12
-rw-r--r--mitmproxy/addons/view.py304
-rw-r--r--mitmproxy/addons/wsgiapp.py4
-rw-r--r--mitmproxy/command.py195
-rw-r--r--mitmproxy/contentviews/image/image_parser.py2
-rw-r--r--mitmproxy/contrib/kaitaistruct/exif.py8
-rw-r--r--mitmproxy/contrib/kaitaistruct/exif_be.py9
-rw-r--r--mitmproxy/contrib/kaitaistruct/exif_le.py9
-rw-r--r--mitmproxy/contrib/kaitaistruct/gif.py18
-rw-r--r--mitmproxy/contrib/kaitaistruct/jpeg.py12
-rwxr-xr-xmitmproxy/contrib/kaitaistruct/make.sh10
-rw-r--r--mitmproxy/contrib/kaitaistruct/png.py26
-rw-r--r--mitmproxy/ctx.py3
-rw-r--r--mitmproxy/eventsequence.py3
-rw-r--r--mitmproxy/exceptions.py6
-rw-r--r--mitmproxy/export.py188
-rw-r--r--mitmproxy/flowfilter.py25
-rw-r--r--mitmproxy/io/__init__.py7
-rw-r--r--mitmproxy/io/compat.py (renamed from mitmproxy/io_compat.py)27
-rw-r--r--mitmproxy/io/io.py (renamed from mitmproxy/io.py)22
-rw-r--r--mitmproxy/io/tnetstring.py (renamed from mitmproxy/contrib/tnetstring.py)20
-rw-r--r--mitmproxy/log.py11
-rw-r--r--mitmproxy/master.py26
-rw-r--r--mitmproxy/net/http/cookies.py96
-rw-r--r--mitmproxy/net/http/http1/read.py4
-rw-r--r--mitmproxy/net/http/message.py3
-rw-r--r--mitmproxy/net/http/response.py6
-rw-r--r--mitmproxy/net/http/url.py24
-rw-r--r--mitmproxy/net/tcp.py28
-rw-r--r--mitmproxy/options.py95
-rw-r--r--mitmproxy/optmanager.py128
-rw-r--r--mitmproxy/proxy/protocol/http.py15
-rw-r--r--mitmproxy/proxy/protocol/http2.py14
-rw-r--r--mitmproxy/proxy/protocol/tls.py17
-rw-r--r--mitmproxy/proxy/server.py3
-rw-r--r--mitmproxy/script/concurrent.py2
-rw-r--r--mitmproxy/test/taddons.py60
-rw-r--r--mitmproxy/test/tflow.py4
-rw-r--r--mitmproxy/tools/cmdline.py17
-rw-r--r--mitmproxy/tools/console/commandeditor.py38
-rw-r--r--mitmproxy/tools/console/commands.py182
-rw-r--r--mitmproxy/tools/console/common.py229
-rw-r--r--mitmproxy/tools/console/eventlog.py47
-rw-r--r--mitmproxy/tools/console/flowdetailview.py9
-rw-r--r--mitmproxy/tools/console/flowlist.py313
-rw-r--r--mitmproxy/tools/console/flowview.py492
-rw-r--r--mitmproxy/tools/console/grideditor/base.py136
-rw-r--r--mitmproxy/tools/console/grideditor/col_bytes.py7
-rw-r--r--mitmproxy/tools/console/grideditor/col_text.py7
-rw-r--r--mitmproxy/tools/console/grideditor/editors.py104
-rw-r--r--mitmproxy/tools/console/help.py12
-rw-r--r--mitmproxy/tools/console/keymap.py61
-rw-r--r--mitmproxy/tools/console/master.py641
-rw-r--r--mitmproxy/tools/console/options.py480
-rw-r--r--mitmproxy/tools/console/overlay.py144
-rw-r--r--mitmproxy/tools/console/palettepicker.py78
-rw-r--r--mitmproxy/tools/console/palettes.py3
-rw-r--r--mitmproxy/tools/console/searchable.py7
-rw-r--r--mitmproxy/tools/console/select.py1
-rw-r--r--mitmproxy/tools/console/signals.py7
-rw-r--r--mitmproxy/tools/console/statusbar.py48
-rw-r--r--mitmproxy/tools/console/tabs.py8
-rw-r--r--mitmproxy/tools/console/window.py277
-rw-r--r--mitmproxy/tools/dump.py14
-rw-r--r--mitmproxy/tools/main.py30
-rw-r--r--mitmproxy/tools/web/app.py24
-rw-r--r--mitmproxy/tools/web/master.py10
-rw-r--r--mitmproxy/tools/web/static/app.css64
-rw-r--r--mitmproxy/tools/web/static/app.js982
-rw-r--r--mitmproxy/tools/web/static/vendor.js1716
-rw-r--r--mitmproxy/types/multidict.py16
-rw-r--r--mitmproxy/utils/human.py19
-rw-r--r--mitmproxy/utils/sliding_window.py4
-rw-r--r--mitmproxy/utils/strutils.py7
-rw-r--r--mitmproxy/utils/typecheck.py56
-rw-r--r--mitmproxy/utils/version_check.py14
-rw-r--r--mitmproxy/version.py2
-rw-r--r--pathod/language/actions.py6
-rw-r--r--pathod/language/base.py15
-rw-r--r--pathod/language/http.py4
-rw-r--r--pathod/language/http2.py6
-rw-r--r--pathod/language/message.py3
-rw-r--r--pathod/language/websockets.py14
-rw-r--r--pathod/pathod.py6
-rw-r--r--pathod/protocols/http2.py8
-rw-r--r--pathod/test.py14
-rw-r--r--pathod/utils.py7
-rw-r--r--release/README.md8
-rw-r--r--setup.cfg18
-rw-r--r--setup.py22
-rw-r--r--test/examples/__init__.py0
-rw-r--r--test/examples/test_examples.py101
-rw-r--r--test/examples/test_har_dump.py86
-rw-r--r--[-rwxr-xr-x]test/examples/test_xss_scanner.py (renamed from test/mitmproxy/examples/test_xss_scanner.py)0
-rw-r--r--test/full_coverage_plugin.py7
-rw-r--r--test/helper_tools/ab.exebin82944 -> 0 bytes
-rw-r--r--test/individual_coverage.py10
-rw-r--r--test/mitmproxy/addons/onboardingapp/__init__.py0
-rw-r--r--test/mitmproxy/addons/test_clientplayback.py19
-rw-r--r--test/mitmproxy/addons/test_core.py165
-rw-r--r--test/mitmproxy/addons/test_core_option_validation.py2
-rw-r--r--test/mitmproxy/addons/test_cut.py178
-rw-r--r--test/mitmproxy/addons/test_defaults.py5
-rw-r--r--test/mitmproxy/addons/test_dumper.py7
-rw-r--r--test/mitmproxy/addons/test_export.py109
-rw-r--r--test/mitmproxy/addons/test_onboarding.py25
-rw-r--r--test/mitmproxy/addons/test_proxyauth.py38
-rw-r--r--test/mitmproxy/addons/test_readfile.py142
-rw-r--r--test/mitmproxy/addons/test_readstdin.py53
-rw-r--r--test/mitmproxy/addons/test_save.py83
-rw-r--r--test/mitmproxy/addons/test_script.py309
-rw-r--r--test/mitmproxy/addons/test_serverplayback.py310
-rw-r--r--test/mitmproxy/addons/test_stickycookie.py4
-rw-r--r--test/mitmproxy/addons/test_streamfile.py60
-rw-r--r--test/mitmproxy/addons/test_termstatus.py1
-rw-r--r--test/mitmproxy/addons/test_view.py223
-rw-r--r--test/mitmproxy/console/test_flowlist.py21
-rw-r--r--test/mitmproxy/contentviews/test_api.py15
-rw-r--r--test/mitmproxy/contentviews/test_xml_html.py8
-rw-r--r--test/mitmproxy/contentviews/test_xml_html_data/test-formatted.html44
-rw-r--r--test/mitmproxy/contentviews/test_xml_html_data/test.html14
-rw-r--r--test/mitmproxy/data/addonscripts/addon.py18
-rw-r--r--test/mitmproxy/data/addonscripts/concurrent_decorator.py1
-rw-r--r--test/mitmproxy/data/addonscripts/concurrent_decorator_class.py3
-rw-r--r--test/mitmproxy/data/addonscripts/concurrent_decorator_err.py2
-rw-r--r--test/mitmproxy/data/addonscripts/print.py2
-rw-r--r--test/mitmproxy/data/addonscripts/recorder/a.py3
-rw-r--r--test/mitmproxy/data/addonscripts/recorder/b.py3
-rw-r--r--test/mitmproxy/data/addonscripts/recorder/c.py3
-rw-r--r--test/mitmproxy/data/addonscripts/recorder/e.py3
-rw-r--r--test/mitmproxy/data/addonscripts/recorder/recorder.py (renamed from test/mitmproxy/data/addonscripts/recorder.py)8
-rw-r--r--test/mitmproxy/data/dumpfile-01840
-rw-r--r--test/mitmproxy/data/test_flow_export/locust_get.py35
-rw-r--r--test/mitmproxy/data/test_flow_export/locust_patch.py37
-rw-r--r--test/mitmproxy/data/test_flow_export/locust_post.py26
-rw-r--r--test/mitmproxy/data/test_flow_export/locust_task_get.py20
-rw-r--r--test/mitmproxy/data/test_flow_export/locust_task_patch.py22
-rw-r--r--test/mitmproxy/data/test_flow_export/locust_task_post.py11
-rw-r--r--test/mitmproxy/data/test_flow_export/python_get.py9
-rw-r--r--test/mitmproxy/data/test_flow_export/python_patch.py10
-rw-r--r--test/mitmproxy/data/test_flow_export/python_post.py8
-rw-r--r--test/mitmproxy/data/test_flow_export/python_post_json.py9
-rw-r--r--test/mitmproxy/io/test_compat.py (renamed from test/mitmproxy/test_io_compat.py)0
-rw-r--r--test/mitmproxy/io/test_io.py (renamed from test/mitmproxy/test_io.py)0
-rw-r--r--test/mitmproxy/io/test_tnetstring.py (renamed from test/mitmproxy/contrib/test_tnetstring.py)2
-rw-r--r--test/mitmproxy/net/http/http1/test_read.py1
-rw-r--r--test/mitmproxy/net/http/test_cookies.py7
-rw-r--r--test/mitmproxy/net/http/test_message.py14
-rw-r--r--test/mitmproxy/net/test_imports.py1
-rw-r--r--test/mitmproxy/net/test_tcp.py43
-rw-r--r--test/mitmproxy/platform/__init__.py0
-rw-r--r--test/mitmproxy/proxy/protocol/test_http2.py16
-rw-r--r--test/mitmproxy/proxy/protocol/test_websocket.py2
-rw-r--r--test/mitmproxy/proxy/test_server.py4
-rw-r--r--test/mitmproxy/script/test_concurrent.py13
-rw-r--r--test/mitmproxy/test_addonmanager.py161
-rw-r--r--test/mitmproxy/test_command.py165
-rw-r--r--test/mitmproxy/test_connections.py2
-rw-r--r--test/mitmproxy/test_controller.py2
-rw-r--r--test/mitmproxy/test_examples.py204
-rw-r--r--test/mitmproxy/test_export.py106
-rw-r--r--test/mitmproxy/test_flow.py6
-rw-r--r--test/mitmproxy/test_optmanager.py75
-rw-r--r--test/mitmproxy/test_proxy.py1
-rw-r--r--test/mitmproxy/test_taddons.py20
-rw-r--r--test/mitmproxy/test_websocket.py2
-rw-r--r--test/mitmproxy/tools/console/test_help.py6
-rw-r--r--test/mitmproxy/tools/console/test_keymap.py29
-rw-r--r--test/mitmproxy/tools/console/test_master.py8
-rw-r--r--test/mitmproxy/tools/console/test_pathedit.py8
-rw-r--r--test/mitmproxy/tools/test_dump.py2
-rw-r--r--test/mitmproxy/tools/web/test_app.py28
-rw-r--r--test/mitmproxy/tservers.py2
-rw-r--r--test/mitmproxy/utils/test_human.py7
-rw-r--r--test/mitmproxy/utils/test_typecheck.py78
-rw-r--r--tox.ini16
-rw-r--r--web/package.json14
-rw-r--r--web/src/css/header.less30
-rw-r--r--web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js108
-rw-r--r--web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap160
-rw-r--r--web/src/js/__tests__/components/ValueEditor/ValidateEditorSpec.js47
-rw-r--r--web/src/js/__tests__/components/ValueEditor/ValueEditorSpec.js155
-rw-r--r--web/src/js/__tests__/components/ValueEditor/__snapshots__/ValidateEditorSpec.js.snap21
-rw-r--r--web/src/js/__tests__/components/ValueEditor/__snapshots__/ValueEditorSpec.js.snap21
-rw-r--r--web/src/js/__tests__/components/common/ButtonSpec.js26
-rw-r--r--web/src/js/__tests__/components/common/DocsLinkSpec.js17
-rw-r--r--web/src/js/__tests__/components/common/DropdownSpec.js38
-rw-r--r--web/src/js/__tests__/components/common/FileChooserSpec.js38
-rw-r--r--web/src/js/__tests__/components/common/SplitterSpec.js84
-rw-r--r--web/src/js/__tests__/components/common/ToggleButtonSpec.js26
-rw-r--r--web/src/js/__tests__/components/common/ToggleInputButtonSpec.js43
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap30
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.js.snap21
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/DropdownSpec.js.snap162
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/FileChooserSpec.js.snap19
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/SplitterSpec.js.snap12
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/ToggleButtonSpec.js.snap14
-rw-r--r--web/src/js/__tests__/components/common/__snapshots__/ToggleInputButtonSpec.js.snap31
-rw-r--r--web/src/js/__tests__/components/helpers/AutoScrollSpec.js41
-rw-r--r--web/src/js/__tests__/components/helpers/VirtualScrollSpec.js21
-rw-r--r--web/src/js/__tests__/ducks/_tflow.js97
-rw-r--r--web/src/js/__tests__/ducks/connectionSpec.js41
-rw-r--r--web/src/js/__tests__/ducks/eventLogSpec.js38
-rw-r--r--web/src/js/__tests__/ducks/flowsSpec.js241
-rw-r--r--web/src/js/__tests__/ducks/indexSpec.js12
-rw-r--r--web/src/js/__tests__/ducks/settingsSpec.js25
-rw-r--r--web/src/js/__tests__/ducks/tutils.js5
-rw-r--r--web/src/js/__tests__/ducks/ui/flowSpec.js4
-rw-r--r--web/src/js/__tests__/ducks/ui/headerSpec.js3
-rw-r--r--web/src/js/__tests__/ducks/ui/indexSpec.js9
-rw-r--r--web/src/js/__tests__/ducks/ui/keyboardSpec.js157
-rw-r--r--web/src/js/__tests__/ducks/utils/storeSpec.js2
-rw-r--r--web/src/js/__tests__/flow/utilsSpec.js69
-rw-r--r--web/src/js/__tests__/urlStateSpec.js100
-rw-r--r--web/src/js/__tests__/utilsSpec.js95
-rw-r--r--web/src/js/backends/websocket.js19
-rw-r--r--web/src/js/components/ContentView.jsx7
-rw-r--r--web/src/js/components/ContentView/CodeEditor.jsx3
-rw-r--r--web/src/js/components/ContentView/ContentLoader.jsx3
-rw-r--r--web/src/js/components/ContentView/ContentViewOptions.jsx7
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx5
-rw-r--r--web/src/js/components/ContentView/DownloadContentButton.jsx2
-rw-r--r--web/src/js/components/ContentView/ShowFullContentButton.jsx3
-rw-r--r--web/src/js/components/ContentView/UploadContentButton.jsx4
-rw-r--r--web/src/js/components/ContentView/ViewSelector.jsx3
-rw-r--r--web/src/js/components/EventLog.jsx5
-rw-r--r--web/src/js/components/EventLog/EventList.jsx10
-rw-r--r--web/src/js/components/FlowTable.jsx3
-rw-r--r--web/src/js/components/FlowTable/FlowRow.jsx3
-rw-r--r--web/src/js/components/FlowTable/FlowTableHead.jsx7
-rw-r--r--web/src/js/components/FlowView/Headers.jsx3
-rw-r--r--web/src/js/components/FlowView/Messages.jsx3
-rw-r--r--web/src/js/components/FlowView/Nav.jsx3
-rw-r--r--web/src/js/components/FlowView/ToggleEdit.jsx3
-rw-r--r--web/src/js/components/Footer.jsx13
-rw-r--r--web/src/js/components/Header.jsx9
-rw-r--r--web/src/js/components/Header/ConnectionIndicator.jsx30
-rw-r--r--web/src/js/components/Header/FileMenu.jsx3
-rw-r--r--web/src/js/components/Header/FilterInput.jsx3
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx3
-rw-r--r--web/src/js/components/Header/MainMenu.jsx3
-rw-r--r--web/src/js/components/Header/MenuToggle.jsx2
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx3
-rw-r--r--web/src/js/components/MainView.jsx3
-rwxr-xr-xweb/src/js/components/Prompt.jsx3
-rw-r--r--web/src/js/components/ProxyApp.jsx3
-rwxr-xr-xweb/src/js/components/ValueEditor/ValidateEditor.jsx3
-rw-r--r--web/src/js/components/ValueEditor/ValueEditor.jsx3
-rw-r--r--web/src/js/components/common/Button.jsx3
-rw-r--r--web/src/js/components/common/DocsLink.jsx3
-rw-r--r--web/src/js/components/common/Dropdown.jsx3
-rw-r--r--web/src/js/components/common/FileChooser.jsx5
-rw-r--r--web/src/js/components/common/ToggleButton.jsx3
-rw-r--r--web/src/js/components/common/ToggleInputButton.jsx3
-rw-r--r--web/src/js/ducks/connection.js44
-rw-r--r--web/src/js/ducks/eventLog.js2
-rw-r--r--web/src/js/ducks/flows.js37
-rw-r--r--web/src/js/ducks/index.js12
-rw-r--r--web/src/js/ducks/ui/flow.js6
-rw-r--r--web/src/js/ducks/ui/header.js2
-rw-r--r--web/src/js/ducks/ui/keyboard.js19
-rw-r--r--web/src/js/filt/filt.js2
-rw-r--r--web/src/js/urlState.js4
-rw-r--r--web/yarn.lock42
325 files changed, 10410 insertions, 5315 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
index 53e33614..549a0607 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,6 +1,10 @@
version: '{build}'
build: off # Not a C# project
+branches:
+ except:
+ - requires-io-master
+
environment:
CI_DEPS: codecov>=2.0.5
CI_COMMANDS: codecov
diff --git a/.gitignore b/.gitignore
index c289ed50..a37a1f31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ MANIFEST
.cache/
.tox*/
build/
+mitmproxy/contrib/kaitaistruct/*.ksy
# UI
@@ -21,3 +22,4 @@ sslkeylogfile.log
.tox/
.python-version
coverage.xml
+web/coverage/
diff --git a/.travis.yml b/.travis.yml
index e64bf6d4..0ff0fb59 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,15 @@
sudo: false
language: python
+branches:
+ except:
+ - requires-io-master
+
env:
global:
- CI_DEPS=codecov>=2.0.5
- CI_COMMANDS=codecov
+
git:
depth: 10000
@@ -18,13 +23,13 @@ matrix:
language: generic
env: TOXENV=py35 BDIST=1
- python: 3.5
- env: TOXENV=py35 OPENSSL_OLD
+ env: TOXENV=py35 OPENSSL=old
addons:
apt:
packages:
- libssl-dev
- python: 3.5
- env: TOXENV=py35 BDIST=1 OPENSSL_ALPN
+ env: TOXENV=py35 BDIST=1 OPENSSL=with-alpn
addons:
apt:
sources:
@@ -34,7 +39,7 @@ matrix:
packages:
- libssl-dev
- python: 3.6
- env: TOXENV=py36 OPENSSL_ALPN
+ env: TOXENV=py36 OPENSSL=with-alpn
addons:
apt:
sources:
@@ -52,8 +57,10 @@ matrix:
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH=$HOME/.yarn/bin:$PATH
- install: cd web && yarn
- script: npm test
+ install:
+ - cd web && yarn
+ - yarn global add codecov
+ script: npm test && codecov
cache:
yarn: true
directories:
@@ -102,4 +109,3 @@ cache:
directories:
- $HOME/.pyenv
- $HOME/.cache/pip
- # - $HOME/build/mitmproxy/mitmproxy/.tox
diff --git a/CHANGELOG b/CHANGELOG
index f276df61..33d5e4ad 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,19 @@
+28 April 2017: mitmproxy 2.0.2
+
+ * Fix mitmweb's Content-Security-Policy to work with Chrome 58+
+
+ * HTTP/2: actually use header normalization from hyper-h2
+
+
+15 March 2017: mitmproxy 2.0.1
+
+ * bump cryptography dependency
+
+ * bump pyparsing dependency
+
+ * HTTP/2: use header normalization from hyper-h2
+
+
21 February 2017: mitmproxy 2.0
* HTTP/2 is now enabled by default.
diff --git a/README.rst b/README.rst
index 168190aa..b69cce96 100644
--- a/README.rst
+++ b/README.rst
@@ -62,7 +62,7 @@ Development Setup
To get started hacking on mitmproxy, please follow the `advanced installation`_ steps to install mitmproxy from source, but stop right before running ``pip3 install mitmproxy``. Instead, do the following:
-.. code-block:: text
+.. code-block:: bash
git clone https://github.com/mitmproxy/mitmproxy.git
cd mitmproxy
@@ -80,7 +80,7 @@ The main executables for the project - ``mitmdump``, ``mitmproxy``,
virtualenv. After activating the virtualenv, they will be on your $PATH, and
you can run them like any other command:
-.. code-block:: text
+.. code-block:: bash
. venv/bin/activate # "venv\Scripts\activate" on Windows
mitmdump --version
@@ -91,13 +91,13 @@ Testing
If you've followed the procedure above, you already have all the development
requirements installed, and you can run the full test suite (including tests for code style and documentation) with tox_:
-.. code-block:: text
+.. code-block:: bash
tox
For speedier testing, we recommend you run `pytest`_ directly on individual test files or folders:
-.. code-block:: text
+.. code-block:: bash
cd test/mitmproxy/addons
pytest --cov mitmproxy.addons.anticache --looponfail test_anticache.py
@@ -114,7 +114,7 @@ The mitmproxy documentation is build using Sphinx_, which is installed
automatically if you set up a development environment as described above. After
installation, you can render the documentation like this:
-.. code-block:: text
+.. code-block:: bash
cd docs
make clean
@@ -136,7 +136,7 @@ This is automatically enforced on every PR. If we detect a linting error, the
PR checks will fail and block merging. You can run our lint checks yourself
with the following command:
-.. code-block:: text
+.. code-block:: bash
tox -e lint
diff --git a/docs/certinstall.rst b/docs/certinstall.rst
index 1bd6df99..2594c439 100644
--- a/docs/certinstall.rst
+++ b/docs/certinstall.rst
@@ -24,6 +24,9 @@ something like this:
Click on the relevant icon, follow the setup instructions for the platform
you're on and you are good to go.
+For iOS version 10.3 or up, you need to make sure ``mitmproxy`` is enabled in
+``Certificate Trust Settings``, you can check it by going to
+``Settings > General > About > Certificate Trust Settings``.
Installing the mitmproxy CA certificate manually
------------------------------------------------
@@ -132,7 +135,7 @@ mitmproxy-ca-cert.cer Same file as .pem, but with an extension expected by some
Using a custom certificate
--------------------------
-You can use your own certificate by passing the ``--cert [domain=]path_to_certificate`` option to
+You can use your own (leaf) certificate by passing the ``--cert [domain=]path_to_certificate`` option to
mitmproxy. Mitmproxy then uses the provided certificate for interception of the
specified domain instead of generating a certificate signed by its own CA.
diff --git a/docs/howmitmproxy.rst b/docs/howmitmproxy.rst
index 133863e3..4f3c804e 100644
--- a/docs/howmitmproxy.rst
+++ b/docs/howmitmproxy.rst
@@ -43,7 +43,7 @@ client connects to the proxy and makes a request that looks like this:
CONNECT example.com:443 HTTP/1.1
-A conventional proxy can neither view nor manipulate an TLS-encrypted data
+A conventional proxy can neither view nor manipulate a TLS-encrypted data
stream, so a CONNECT request simply asks the proxy to open a pipe between the
client and server. The proxy here is just a facilitator - it blindly forwards
data in both directions without knowing anything about the contents. The
@@ -63,7 +63,7 @@ exactly this attack, by allowing a trusted third-party to cryptographically sign
a server's certificates to verify that they are legit. If this signature doesn't
match or is from a non-trusted party, a secure client will simply drop the
connection and refuse to proceed. Despite the many shortcomings of the CA system
-as it exists today, this is usually fatal to attempts to MITM an TLS connection
+as it exists today, this is usually fatal to attempts to MITM a TLS connection
for analysis. Our answer to this conundrum is to become a trusted Certificate
Authority ourselves. Mitmproxy includes a full CA implementation that generates
interception certificates on the fly. To get the client to trust these
@@ -143,7 +143,7 @@ Lets put all of this together into the complete explicitly proxied HTTPS flow.
2. Mitmproxy responds with a ``200 Connection Established``, as if it has set up the CONNECT pipe.
3. The client believes it's talking to the remote server, and initiates the TLS connection.
It uses SNI to indicate the hostname it is connecting to.
-4. Mitmproxy connects to the server, and establishes an TLS connection using the SNI hostname
+4. Mitmproxy connects to the server, and establishes a TLS connection using the SNI hostname
indicated by the client.
5. The server responds with the matching certificate, which contains the CN and SAN values
needed to generate the interception certificate.
@@ -217,7 +217,7 @@ explicit HTTPS connections to establish the CN and SANs, and cope with SNI.
destination was.
3. The client believes it's talking to the remote server, and initiates the TLS connection.
It uses SNI to indicate the hostname it is connecting to.
-4. Mitmproxy connects to the server, and establishes an TLS connection using the SNI hostname
+4. Mitmproxy connects to the server, and establishes a TLS connection using the SNI hostname
indicated by the client.
5. The server responds with the matching certificate, which contains the CN and SAN values
needed to generate the interception certificate.
diff --git a/docs/install.rst b/docs/install.rst
index b37d9c91..7753dc44 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -123,12 +123,12 @@ You can check you Python version by running ``python3 --version``.
sudo zypper install python3-pip python3-devel libffi-devel openssl-devel gcc-c++
sudo pip3 install mitmproxy
-
+
.. _install-source-windows:
-🐱💻 Installation from Source on Windows
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Installation from Source on Windows
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. note::
Mitmproxy's console interface is not supported on Windows, but you can use
diff --git a/docs/scripting/overview.rst b/docs/scripting/overview.rst
index 6ec0caaa..5ceb5da3 100644
--- a/docs/scripting/overview.rst
+++ b/docs/scripting/overview.rst
@@ -29,6 +29,12 @@ will be added to all responses passing through the proxy:
>>> mitmdump -s add_header.py
+Examples
+--------
+
+A collection of addons that demonstrate popular features can be found at :src:`examples/simple`.
+
+
Using classes
-------------
@@ -54,24 +60,8 @@ and is replaced by the class instance.
Handling arguments
------------------
-Scripts can handle their own command-line arguments, just like any other Python
-program. Let's build on the example above to do something slightly more
-sophisticated - replace one value with another in all responses. Mitmproxy's
-`HTTPRequest <api.html#mitmproxy.models.http.HTTPRequest>`_ and `HTTPResponse
-<api.html#mitmproxy.models.http.HTTPResponse>`_ objects have a handy `replace
-<api.html#mitmproxy.models.http.HTTPResponse.replace>`_ method that takes care
-of all the details for us.
-
-.. literalinclude:: ../../examples/simple/script_arguments.py
- :caption: :src:`examples/simple/script_arguments.py`
- :language: python
-
-We can now call this script on the command-line like this:
-
->>> mitmdump -dd -s "./script_arguments.py html faketml"
-Whenever a handler is called, mitpmroxy rewrites the script environment so that
-it sees its own arguments as if it was invoked from the command-line.
+FIXME
Logging and the context
diff --git a/examples/complex/README.md b/examples/complex/README.md
index 452f2395..77dbe2f5 100644
--- a/examples/complex/README.md
+++ b/examples/complex/README.md
@@ -5,7 +5,6 @@
| change_upstream_proxy.py | Dynamically change the upstream proxy. |
| dns_spoofing.py | Use mitmproxy in a DNS spoofing scenario. |
| dup_and_replay.py | Duplicates each request, changes it, and then replays the modified request. |
-| flowbasic.py | Basic use of mitmproxy's FlowMaster directly. |
| full_transparency_shim.c | Setuid wrapper that can be used to run mitmproxy in full transparency mode, as a normal user. |
| har_dump.py | Dump flows as HAR files. |
| mitmproxywrapper.py | Bracket mitmproxy run with proxy enable/disable on OS X |
@@ -16,3 +15,4 @@
| stream_modify.py | Modify a streamed response body. |
| tcp_message.py | Modify a raw TCP connection |
| tls_passthrough.py | Use conditional TLS interception based on a user-defined strategy. |
+| xss_scanner.py | Scan all visited webpages. |
diff --git a/examples/complex/dns_spoofing.py b/examples/complex/dns_spoofing.py
index ca2bcd35..632783a7 100644
--- a/examples/complex/dns_spoofing.py
+++ b/examples/complex/dns_spoofing.py
@@ -54,5 +54,4 @@ class Rerouter:
flow.request.port = port
-def start(opts):
- return Rerouter()
+addons = [Rerouter()]
diff --git a/examples/complex/har_dump.py b/examples/complex/har_dump.py
index 9a86e45e..21bcc341 100644
--- a/examples/complex/har_dump.py
+++ b/examples/complex/har_dump.py
@@ -4,7 +4,6 @@ This inline script can be used to dump flows as HAR files.
import json
-import sys
import base64
import zlib
import os
@@ -15,6 +14,7 @@ from datetime import timezone
import mitmproxy
from mitmproxy import version
+from mitmproxy import ctx
from mitmproxy.utils import strutils
from mitmproxy.net.http import cookies
@@ -25,17 +25,13 @@ HAR = {}
SERVERS_SEEN = set()
-def start(opts):
- """
- Called once on script startup before any other events.
- """
- if len(sys.argv) != 2:
- raise ValueError(
- 'Usage: -s "har_dump.py filename" '
- '(- will output to stdout, filenames ending with .zhar '
- 'will result in compressed har)'
- )
+def load(l):
+ l.add_option(
+ "hardump", str, "", "HAR dump path.",
+ )
+
+def configure(updated):
HAR.update({
"log": {
"version": "1.2",
@@ -156,21 +152,20 @@ def done():
"""
Called once on script shutdown, after any other events.
"""
- dump_file = sys.argv[1]
+ if ctx.options.hardump:
+ json_dump = json.dumps(HAR, indent=2) # type: str
- json_dump = json.dumps(HAR, indent=2) # type: str
-
- if dump_file == '-':
- mitmproxy.ctx.log(json_dump)
- else:
- raw = json_dump.encode() # type: bytes
- if dump_file.endswith('.zhar'):
- raw = zlib.compress(raw, 9)
+ if ctx.options.hardump == '-':
+ mitmproxy.ctx.log(json_dump)
+ else:
+ raw = json_dump.encode() # type: bytes
+ if ctx.options.hardump.endswith('.zhar'):
+ raw = zlib.compress(raw, 9)
- with open(os.path.expanduser(dump_file), "wb") as f:
- f.write(raw)
+ with open(os.path.expanduser(ctx.options.hardump), "wb") as f:
+ f.write(raw)
- mitmproxy.ctx.log("HAR dump finished (wrote %s bytes to file)" % len(json_dump))
+ mitmproxy.ctx.log("HAR dump finished (wrote %s bytes to file)" % len(json_dump))
def format_cookies(cookie_list):
@@ -206,7 +201,7 @@ def format_request_cookies(fields):
def format_response_cookies(fields):
- return format_cookies((c[0], c[1].value, c[1].attrs) for c in fields)
+ return format_cookies((c[0], c[1][0], c[1][1]) for c in fields)
def name_value(obj):
diff --git a/examples/complex/remote_debug.py b/examples/complex/remote_debug.py
index ae0dffc1..fa6f3d33 100644
--- a/examples/complex/remote_debug.py
+++ b/examples/complex/remote_debug.py
@@ -14,6 +14,6 @@ Usage:
"""
-def start(opts):
+def load(l):
import pydevd
pydevd.settrace("localhost", port=5678, stdoutToServer=True, stderrToServer=True)
diff --git a/examples/complex/tls_passthrough.py b/examples/complex/tls_passthrough.py
index 6dba7ca1..9bb27d25 100644
--- a/examples/complex/tls_passthrough.py
+++ b/examples/complex/tls_passthrough.py
@@ -23,10 +23,10 @@ Authors: Maximilian Hils, Matthew Tuusberg
import collections
import random
-import sys
from enum import Enum
import mitmproxy
+from mitmproxy import ctx
from mitmproxy.exceptions import TlsProtocolException
from mitmproxy.proxy.protocol import TlsLayer, RawTCPLayer
@@ -112,10 +112,16 @@ class TlsFeedback(TlsLayer):
tls_strategy = None
-def start(opts):
+def load(l):
+ l.add_option(
+ "tlsstrat", int, 0, "TLS passthrough strategy (0-100)",
+ )
+
+
+def configure(updated):
global tls_strategy
- if len(sys.argv) == 2:
- tls_strategy = ProbabilisticStrategy(float(sys.argv[1]))
+ if ctx.options.tlsstrat > 0:
+ tls_strategy = ProbabilisticStrategy(float(ctx.options.tlsstrat) / 100.0)
else:
tls_strategy = ConservativeStrategy()
diff --git a/examples/simple/add_header.py b/examples/simple/add_header.py
index 3e0b5f1e..64fc6267 100644
--- a/examples/simple/add_header.py
+++ b/examples/simple/add_header.py
@@ -1,2 +1,5 @@
-def response(flow):
+from mitmproxy import http
+
+
+def response(flow: http.HTTPFlow) -> None:
flow.response.headers["newheader"] = "foo"
diff --git a/examples/simple/add_header_class.py b/examples/simple/add_header_class.py
index 9270be09..419c99ac 100644
--- a/examples/simple/add_header_class.py
+++ b/examples/simple/add_header_class.py
@@ -1,7 +1,9 @@
+from mitmproxy import http
+
+
class AddHeader:
- def response(self, flow):
+ def response(self, flow: http.HTTPFlow) -> None:
flow.response.headers["newheader"] = "foo"
-def start(opts):
- return AddHeader()
+addons = [AddHeader()]
diff --git a/examples/simple/custom_contentview.py b/examples/simple/custom_contentview.py
index 4bc17af0..71f92575 100644
--- a/examples/simple/custom_contentview.py
+++ b/examples/simple/custom_contentview.py
@@ -3,6 +3,10 @@ This example shows how one can add a custom contentview to mitmproxy.
The content view API is explained in the mitmproxy.contentviews module.
"""
from mitmproxy import contentviews
+import typing
+
+
+CVIEWSWAPCASE = typing.Tuple[str, typing.Iterable[typing.List[typing.Tuple[str, typing.AnyStr]]]]
class ViewSwapCase(contentviews.View):
@@ -13,14 +17,14 @@ class ViewSwapCase(contentviews.View):
prompt = ("swap case text", "z")
content_types = ["text/plain"]
- def __call__(self, data: bytes, **metadata):
+ def __call__(self, data: typing.AnyStr, **metadata) -> CVIEWSWAPCASE:
return "case-swapped text", contentviews.format_text(data.swapcase())
view = ViewSwapCase()
-def start(opts):
+def load(l):
contentviews.add(view)
diff --git a/examples/simple/custom_option.py b/examples/simple/custom_option.py
index 324d27e7..5b6070dd 100644
--- a/examples/simple/custom_option.py
+++ b/examples/simple/custom_option.py
@@ -1,11 +1,11 @@
from mitmproxy import ctx
-def start(options):
+def load(l):
ctx.log.info("Registering option 'custom'")
- options.add_option("custom", bool, False, "A custom option")
+ l.add_option("custom", bool, False, "A custom option")
-def configure(options, updated):
+def configure(updated):
if "custom" in updated:
- ctx.log.info("custom option value: %s" % options.custom)
+ ctx.log.info("custom option value: %s" % ctx.options.custom)
diff --git a/examples/simple/filter_flows.py b/examples/simple/filter_flows.py
index 24e8b6c1..70979591 100644
--- a/examples/simple/filter_flows.py
+++ b/examples/simple/filter_flows.py
@@ -1,23 +1,26 @@
"""
This scripts demonstrates how to use mitmproxy's filter pattern in scripts.
-Usage:
- mitmdump -s "flowfilter.py FILTER"
"""
-import sys
from mitmproxy import flowfilter
+from mitmproxy import ctx, http
class Filter:
- def __init__(self, spec):
- self.filter = flowfilter.parse(spec)
+ def __init__(self):
+ self.filter = None # type: flowfilter.TFilter
- def response(self, flow):
+ def configure(self, updated):
+ self.filter = flowfilter.parse(ctx.options.flowfilter)
+
+ def load(self, l):
+ l.add_option(
+ "flowfilter", str, "", "Check that flow matches filter."
+ )
+
+ def response(self, flow: http.HTTPFlow) -> None:
if flowfilter.match(self.filter, flow):
print("Flow matches filter:")
print(flow)
-def start(opts):
- if len(sys.argv) != 2:
- raise ValueError("Usage: -s 'filt.py FILTER'")
- return Filter(sys.argv[1])
+addons = [Filter()]
diff --git a/examples/simple/io_read_dumpfile.py b/examples/simple/io_read_dumpfile.py
index edbbe2dd..ea544cc4 100644
--- a/examples/simple/io_read_dumpfile.py
+++ b/examples/simple/io_read_dumpfile.py
@@ -1,13 +1,15 @@
#!/usr/bin/env python
+
+# type: ignore
#
# Simple script showing how to read a mitmproxy dump file
#
-
from mitmproxy import io
from mitmproxy.exceptions import FlowReadException
import pprint
import sys
+
with open(sys.argv[1], "rb") as logfile:
freader = io.FlowReader(logfile)
pp = pprint.PrettyPrinter(indent=4)
diff --git a/examples/simple/io_write_dumpfile.py b/examples/simple/io_write_dumpfile.py
index 311950af..cf7c4f52 100644
--- a/examples/simple/io_write_dumpfile.py
+++ b/examples/simple/io_write_dumpfile.py
@@ -7,23 +7,21 @@ to multiple files in parallel.
"""
import random
import sys
-from mitmproxy import io
+from mitmproxy import io, http
+import typing # noqa
class Writer:
- def __init__(self, path):
+ def __init__(self, path: str) -> None:
if path == "-":
- f = sys.stdout
+ f = sys.stdout # type: typing.IO[typing.Any]
else:
f = open(path, "wb")
self.w = io.FlowWriter(f)
- def response(self, flow):
+ def response(self, flow: http.HTTPFlow) -> None:
if random.choice([True, False]):
self.w.add(flow)
-def start(opts):
- if len(sys.argv) != 2:
- raise ValueError('Usage: -s "flowriter.py filename"')
- return Writer(sys.argv[1])
+addons = [Writer(sys.argv[1])]
diff --git a/examples/simple/log_events.py b/examples/simple/log_events.py
index a81892aa..581b99f3 100644
--- a/examples/simple/log_events.py
+++ b/examples/simple/log_events.py
@@ -7,6 +7,6 @@ If you want to help us out: https://github.com/mitmproxy/mitmproxy/issues/1530 :
from mitmproxy import ctx
-def start(opts):
+def load(l):
ctx.log.info("This is some informative text.")
ctx.log.error("This is an error.")
diff --git a/examples/simple/modify_body_inject_iframe.py b/examples/simple/modify_body_inject_iframe.py
index ab5abf27..595bd9f2 100644
--- a/examples/simple/modify_body_inject_iframe.py
+++ b/examples/simple/modify_body_inject_iframe.py
@@ -1,29 +1,26 @@
-# Usage: mitmdump -s "iframe_injector.py url"
# (this script works best with --anticache)
-import sys
from bs4 import BeautifulSoup
+from mitmproxy import ctx, http
class Injector:
- def __init__(self, iframe_url):
- self.iframe_url = iframe_url
+ def load(self, loader):
+ loader.add_option(
+ "iframe", str, "", "IFrame to inject"
+ )
- def response(self, flow):
- if flow.request.host in self.iframe_url:
- return
- html = BeautifulSoup(flow.response.content, "html.parser")
- if html.body:
- iframe = html.new_tag(
- "iframe",
- src=self.iframe_url,
- frameborder=0,
- height=0,
- width=0)
- html.body.insert(0, iframe)
- flow.response.content = str(html).encode("utf8")
+ def response(self, flow: http.HTTPFlow) -> None:
+ if ctx.options.iframe:
+ html = BeautifulSoup(flow.response.content, "html.parser")
+ if html.body:
+ iframe = html.new_tag(
+ "iframe",
+ src=ctx.options.iframe,
+ frameborder=0,
+ height=0,
+ width=0)
+ html.body.insert(0, iframe)
+ flow.response.content = str(html).encode("utf8")
-def start(opts):
- if len(sys.argv) != 2:
- raise ValueError('Usage: -s "iframe_injector.py url"')
- return Injector(sys.argv[1])
+addons = [Injector()]
diff --git a/examples/simple/modify_form.py b/examples/simple/modify_form.py
index b425efb0..8742a976 100644
--- a/examples/simple/modify_form.py
+++ b/examples/simple/modify_form.py
@@ -1,4 +1,7 @@
-def request(flow):
+from mitmproxy import http
+
+
+def request(flow: http.HTTPFlow) -> None:
if flow.request.urlencoded_form:
# If there's already a form, one can just add items to the dict:
flow.request.urlencoded_form["mitmproxy"] = "rocks"
diff --git a/examples/simple/modify_querystring.py b/examples/simple/modify_querystring.py
index ee8a89ad..12b16fda 100644
--- a/examples/simple/modify_querystring.py
+++ b/examples/simple/modify_querystring.py
@@ -1,2 +1,5 @@
-def request(flow):
+from mitmproxy import http
+
+
+def request(flow: http.HTTPFlow) -> None:
flow.request.query["mitmproxy"] = "rocks"
diff --git a/examples/simple/redirect_requests.py b/examples/simple/redirect_requests.py
index 51876df7..ddb89961 100644
--- a/examples/simple/redirect_requests.py
+++ b/examples/simple/redirect_requests.py
@@ -1,9 +1,10 @@
"""
This example shows two ways to redirect flows to another server.
"""
+from mitmproxy import http
-def request(flow):
+def request(flow: http.HTTPFlow) -> None:
# pretty_host takes the "Host" header of the request into account,
# which is useful in transparent mode where we usually only have the IP
# otherwise.
diff --git a/examples/simple/script_arguments.py b/examples/simple/script_arguments.py
deleted file mode 100644
index b46a1960..00000000
--- a/examples/simple/script_arguments.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import argparse
-
-
-class Replacer:
- def __init__(self, src, dst):
- self.src, self.dst = src, dst
-
- def response(self, flow):
- flow.response.replace(self.src, self.dst)
-
-
-def start(opts):
- parser = argparse.ArgumentParser()
- parser.add_argument("src", type=str)
- parser.add_argument("dst", type=str)
- args = parser.parse_args()
- return Replacer(args.src, args.dst)
diff --git a/examples/simple/send_reply_from_proxy.py b/examples/simple/send_reply_from_proxy.py
index bef2e7e7..5011fd2e 100644
--- a/examples/simple/send_reply_from_proxy.py
+++ b/examples/simple/send_reply_from_proxy.py
@@ -5,7 +5,7 @@ without sending any data to the remote server.
from mitmproxy import http
-def request(flow):
+def request(flow: http.HTTPFlow) -> None:
# pretty_url takes the "Host" header of the request into account, which
# is useful in transparent mode where we usually only have the IP otherwise.
diff --git a/examples/simple/upsidedownternet.py b/examples/simple/upsidedownternet.py
index 8ba450ab..f150a5c3 100644
--- a/examples/simple/upsidedownternet.py
+++ b/examples/simple/upsidedownternet.py
@@ -2,11 +2,11 @@
This script rotates all images passing through the proxy by 180 degrees.
"""
import io
-
from PIL import Image
+from mitmproxy import http
-def response(flow):
+def response(flow: http.HTTPFlow) -> None:
if flow.response.headers.get("content-type", "").startswith("image"):
s = io.BytesIO(flow.response.content)
img = Image.open(s).rotate(180)
diff --git a/examples/simple/wsgi_flask_app.py b/examples/simple/wsgi_flask_app.py
index db3b1adf..4be38000 100644
--- a/examples/simple/wsgi_flask_app.py
+++ b/examples/simple/wsgi_flask_app.py
@@ -10,14 +10,14 @@ app = Flask("proxapp")
@app.route('/')
-def hello_world():
+def hello_world() -> str:
return 'Hello World!'
-def start(opts):
- # Host app at the magic domain "proxapp" on port 80. Requests to this
+def load(l):
+ # Host app at the magic domain "proxapp.local" on port 80. Requests to this
# domain and port combination will now be routed to the WSGI app instance.
- return wsgiapp.WSGIApp(app, "proxapp", 80)
+ return wsgiapp.WSGIApp(app, "proxapp.local", 80)
# SSL works too, but the magic domain needs to be resolvable from the mitmproxy machine due to mitmproxy's design.
# mitmproxy will connect to said domain and use serve its certificate (unless --no-upstream-cert is set)
diff --git a/mitmproxy/addonmanager.py b/mitmproxy/addonmanager.py
index 670c4f24..0bbe6287 100644
--- a/mitmproxy/addonmanager.py
+++ b/mitmproxy/addonmanager.py
@@ -1,6 +1,12 @@
+import typing
+import traceback
+import contextlib
+import sys
+
from mitmproxy import exceptions
from mitmproxy import eventsequence
from mitmproxy import controller
+from mitmproxy import flow
from . import ctx
import pprint
@@ -9,18 +15,115 @@ def _get_name(itm):
return getattr(itm, "name", itm.__class__.__name__.lower())
+def cut_traceback(tb, func_name):
+ """
+ Cut off a traceback at the function with the given name.
+ The func_name's frame is excluded.
+
+ Args:
+ tb: traceback object, as returned by sys.exc_info()[2]
+ func_name: function name
+
+ Returns:
+ Reduced traceback.
+ """
+ tb_orig = tb
+ for _, _, fname, _ in traceback.extract_tb(tb):
+ tb = tb.tb_next
+ if fname == func_name:
+ break
+ return tb or tb_orig
+
+
+class StreamLog:
+ """
+ A class for redirecting output using contextlib.
+ """
+ def __init__(self, log):
+ self.log = log
+
+ def write(self, buf):
+ if buf.strip():
+ self.log(buf)
+
+ def flush(self): # pragma: no cover
+ # Click uses flush sometimes, so we dummy it up
+ pass
+
+
+@contextlib.contextmanager
+def safecall():
+ stdout_replacement = StreamLog(ctx.log.warn)
+ try:
+ with contextlib.redirect_stdout(stdout_replacement):
+ yield
+ except (exceptions.AddonHalt, exceptions.OptionsError):
+ raise
+ except Exception as e:
+ etype, value, tb = sys.exc_info()
+ tb = cut_traceback(tb, "invoke_addon").tb_next
+ ctx.log.error(
+ "Addon error: %s" % "".join(
+ traceback.format_exception(etype, value, tb)
+ )
+ )
+
+
+class Loader:
+ """
+ A loader object is passed to the load() event when addons start up.
+ """
+ def __init__(self, master):
+ self.master = master
+
+ def add_option(
+ self,
+ name: str,
+ typespec: type,
+ default: typing.Any,
+ help: str,
+ choices: typing.Optional[typing.Sequence[str]] = None
+ ) -> None:
+ if name in self.master.options:
+ ctx.log.warn("Over-riding existing option %s" % name)
+ self.master.options.add_option(
+ name,
+ typespec,
+ default,
+ help,
+ choices
+ )
+
+ def add_command(self, path: str, func: typing.Callable) -> None:
+ self.master.commands.add(path, func)
+
+
+def traverse(chain):
+ """
+ Recursively traverse an addon chain.
+ """
+ for a in chain:
+ yield a
+ if hasattr(a, "addons"):
+ yield from traverse(a.addons)
+
+
class AddonManager:
def __init__(self, master):
+ self.lookup = {}
self.chain = []
self.master = master
- master.options.changed.connect(self.configure_all)
+ master.options.changed.connect(self._configure_all)
+
+ def _configure_all(self, options, updated):
+ self.trigger("configure", updated)
def clear(self):
"""
Remove all addons.
"""
- self.done()
- self.chain = []
+ for i in self.chain:
+ self.remove(i)
def get(self, name):
"""
@@ -28,32 +131,58 @@ class AddonManager:
attribute on the instance, or the lower case class name if that
does not exist.
"""
- for i in self.chain:
- if name == _get_name(i):
- return i
+ return self.lookup.get(name, None)
+
+ def register(self, addon):
+ """
+ Register an addon, call its load event, and then register all its
+ sub-addons. This should be used by addons that dynamically manage
+ addons.
- def configure_all(self, options, updated):
- self.trigger("configure", options, updated)
+ If the calling addon is already running, it should follow with
+ running and configure events. Must be called within a current
+ context.
+ """
+ for a in traverse([addon]):
+ name = _get_name(a)
+ if name in self.lookup:
+ raise exceptions.AddonManagerError(
+ "An addon called '%s' already exists." % name
+ )
+ l = Loader(self.master)
+ self.invoke_addon(addon, "load", l)
+ for a in traverse([addon]):
+ name = _get_name(a)
+ self.lookup[name] = a
+ for a in traverse([addon]):
+ self.master.commands.collect_commands(a)
+ return addon
def add(self, *addons):
"""
- Add addons to the end of the chain, and run their startup events.
+ Add addons to the end of the chain, and run their load event.
+ If any addon has sub-addons, they are registered.
"""
- self.chain.extend(addons)
with self.master.handlecontext():
for i in addons:
- self.invoke_addon(i, "start", self.master.options)
+ self.chain.append(self.register(i))
def remove(self, addon):
"""
- Remove an addon from the chain, and run its done events.
+ Remove an addon and all its sub-addons.
+
+ If the addon is not in the chain - that is, if it's managed by a
+ parent addon - it's the parent's responsibility to remove it from
+ its own addons attribute.
"""
- self.chain = [i for i in self.chain if i is not addon]
+ for a in traverse([addon]):
+ n = _get_name(a)
+ if n not in self.lookup:
+ raise exceptions.AddonManagerError("No such addon: %s" % n)
+ self.chain = [i for i in self.chain if i is not a]
+ del self.lookup[_get_name(a)]
with self.master.handlecontext():
- self.invoke_addon(addon, "done")
-
- def done(self):
- self.trigger("done")
+ self.invoke_addon(a, "done")
def __len__(self):
return len(self.chain)
@@ -87,24 +216,24 @@ class AddonManager:
if isinstance(message.reply, controller.DummyReply):
message.reply.mark_reset()
+ if isinstance(message, flow.Flow):
+ self.trigger("update", [message])
+
def invoke_addon(self, addon, name, *args, **kwargs):
"""
- Invoke an event on an addon. This method must run within an
- established handler context.
+ Invoke an event on an addon and all its children. This method must
+ run within an established handler context.
"""
- if not ctx.master:
- raise exceptions.AddonError(
- "invoke_addon called without a handler context."
- )
if name not in eventsequence.Events:
name = "event_" + name
- func = getattr(addon, name, None)
- if func:
- if not callable(func):
- raise exceptions.AddonError(
- "Addon handler %s not callable" % name
- )
- func(*args, **kwargs)
+ for a in traverse([addon]):
+ func = getattr(a, name, None)
+ if func:
+ if not callable(func):
+ raise exceptions.AddonManagerError(
+ "Addon handler %s not callable" % name
+ )
+ func(*args, **kwargs)
def trigger(self, name, *args, **kwargs):
"""
@@ -113,6 +242,7 @@ class AddonManager:
with self.master.handlecontext():
for i in self.chain:
try:
- self.invoke_addon(i, name, *args, **kwargs)
+ with safecall():
+ self.invoke_addon(i, name, *args, **kwargs)
except exceptions.AddonHalt:
return
diff --git a/mitmproxy/addons/__init__.py b/mitmproxy/addons/__init__.py
index b4367d78..783a2c94 100644
--- a/mitmproxy/addons/__init__.py
+++ b/mitmproxy/addons/__init__.py
@@ -4,30 +4,35 @@ from mitmproxy.addons import check_alpn
from mitmproxy.addons import check_ca
from mitmproxy.addons import clientplayback
from mitmproxy.addons import core_option_validation
+from mitmproxy.addons import core
+from mitmproxy.addons import cut
from mitmproxy.addons import disable_h2c
+from mitmproxy.addons import export
from mitmproxy.addons import onboarding
from mitmproxy.addons import proxyauth
from mitmproxy.addons import replace
-from mitmproxy.addons import readfile
from mitmproxy.addons import script
from mitmproxy.addons import serverplayback
from mitmproxy.addons import setheaders
from mitmproxy.addons import stickyauth
from mitmproxy.addons import stickycookie
from mitmproxy.addons import streambodies
-from mitmproxy.addons import streamfile
+from mitmproxy.addons import save
from mitmproxy.addons import upstream_auth
def default_addons():
return [
+ core.Core(),
core_option_validation.CoreOptionValidation(),
anticache.AntiCache(),
anticomp.AntiComp(),
check_alpn.CheckALPN(),
check_ca.CheckCA(),
clientplayback.ClientPlayback(),
+ cut.Cut(),
disable_h2c.DisableH2C(),
+ export.Export(),
onboarding.Onboarding(),
proxyauth.ProxyAuth(),
replace.Replace(),
@@ -37,7 +42,6 @@ def default_addons():
stickyauth.StickyAuth(),
stickycookie.StickyCookie(),
streambodies.StreamBodies(),
- streamfile.StreamFile(),
- readfile.ReadFile(),
+ save.Save(),
upstream_auth.UpstreamAuth(),
]
diff --git a/mitmproxy/addons/anticache.py b/mitmproxy/addons/anticache.py
index 8d748a21..5b34d5a5 100644
--- a/mitmproxy/addons/anticache.py
+++ b/mitmproxy/addons/anticache.py
@@ -1,10 +1,7 @@
-class AntiCache:
- def __init__(self):
- self.enabled = False
+from mitmproxy import ctx
- def configure(self, options, updated):
- self.enabled = options.anticache
+class AntiCache:
def request(self, flow):
- if self.enabled:
+ if ctx.options.anticache:
flow.request.anticache()
diff --git a/mitmproxy/addons/anticomp.py b/mitmproxy/addons/anticomp.py
index eaf01296..d7d1ca8d 100644
--- a/mitmproxy/addons/anticomp.py
+++ b/mitmproxy/addons/anticomp.py
@@ -1,10 +1,7 @@
-class AntiComp:
- def __init__(self):
- self.enabled = False
+from mitmproxy import ctx
- def configure(self, options, updated):
- self.enabled = options.anticomp
+class AntiComp:
def request(self, flow):
- if self.enabled:
+ if ctx.options.anticomp:
flow.request.anticomp()
diff --git a/mitmproxy/addons/check_alpn.py b/mitmproxy/addons/check_alpn.py
index cb3c87e3..193159b2 100644
--- a/mitmproxy/addons/check_alpn.py
+++ b/mitmproxy/addons/check_alpn.py
@@ -7,7 +7,7 @@ class CheckALPN:
def __init__(self):
self.failed = False
- def configure(self, options, updated):
+ def configure(self, updated):
self.failed = mitmproxy.ctx.master.options.http2 and not tcp.HAS_ALPN
if self.failed:
ctx.log.warn(
diff --git a/mitmproxy/addons/check_ca.py b/mitmproxy/addons/check_ca.py
index a83ab8e1..f786af5a 100644
--- a/mitmproxy/addons/check_ca.py
+++ b/mitmproxy/addons/check_ca.py
@@ -5,7 +5,7 @@ class CheckCA:
def __init__(self):
self.failed = False
- def configure(self, options, updated):
+ def configure(self, updated):
has_ca = (
mitmproxy.ctx.master.server and
mitmproxy.ctx.master.server.config and
diff --git a/mitmproxy/addons/clientplayback.py b/mitmproxy/addons/clientplayback.py
index 3345e65a..0db6d336 100644
--- a/mitmproxy/addons/clientplayback.py
+++ b/mitmproxy/addons/clientplayback.py
@@ -2,6 +2,7 @@ from mitmproxy import exceptions
from mitmproxy import ctx
from mitmproxy import io
from mitmproxy import flow
+from mitmproxy import command
import typing
@@ -11,32 +12,54 @@ class ClientPlayback:
self.flows = None
self.current_thread = None
self.has_replayed = False
+ self.configured = False
def count(self) -> int:
if self.flows:
return len(self.flows)
return 0
- def load(self, flows: typing.Sequence[flow.Flow]):
+ @command.command("replay.client.stop")
+ def stop_replay(self) -> None:
+ """
+ Stop client replay.
+ """
+ self.flows = []
+ ctx.master.addons.trigger("update", [])
+
+ @command.command("replay.client")
+ def start_replay(self, flows: typing.Sequence[flow.Flow]) -> None:
+ """
+ Replay requests from flows.
+ """
+ self.flows = flows
+ ctx.master.addons.trigger("update", [])
+
+ @command.command("replay.client.file")
+ def load_file(self, path: str) -> None:
+ try:
+ flows = io.read_flows_from_paths([path])
+ except exceptions.FlowReadException as e:
+ raise exceptions.CommandError(str(e))
self.flows = flows
- def configure(self, options, updated):
- if "client_replay" in updated:
- if options.client_replay:
- ctx.log.info("Client Replay: {}".format(options.client_replay))
- try:
- flows = io.read_flows_from_paths(options.client_replay)
- except exceptions.FlowReadException as e:
- raise exceptions.OptionsError(str(e))
- self.load(flows)
- else:
- self.flows = None
+ def configure(self, updated):
+ if not self.configured and ctx.options.client_replay:
+ self.configured = True
+ ctx.log.info("Client Replay: {}".format(ctx.options.client_replay))
+ try:
+ flows = io.read_flows_from_paths(ctx.options.client_replay)
+ except exceptions.FlowReadException as e:
+ raise exceptions.OptionsError(str(e))
+ self.start_replay(flows)
def tick(self):
if self.current_thread and not self.current_thread.is_alive():
self.current_thread = None
if self.flows and not self.current_thread:
- self.current_thread = ctx.master.replay_request(self.flows.pop(0))
+ f = self.flows.pop(0)
+ self.current_thread = ctx.master.replay_request(f)
+ ctx.master.addons.trigger("update", [f])
self.has_replayed = True
if self.has_replayed:
if not self.flows and not self.current_thread:
diff --git a/mitmproxy/addons/core.py b/mitmproxy/addons/core.py
new file mode 100644
index 00000000..426c47ad
--- /dev/null
+++ b/mitmproxy/addons/core.py
@@ -0,0 +1,259 @@
+import typing
+
+from mitmproxy import ctx
+from mitmproxy import exceptions
+from mitmproxy import command
+from mitmproxy import flow
+from mitmproxy import optmanager
+from mitmproxy.net.http import status_codes
+
+
+class Core:
+ @command.command("set")
+ def set(self, spec: str) -> None:
+ """
+ Set an option of the form "key[=value]". When the value is omitted,
+ booleans are set to true, strings and integers are set to None (if
+ permitted), and sequences are emptied. Boolean values can be true,
+ false or toggle.
+ """
+ try:
+ ctx.options.set(spec)
+ except exceptions.OptionsError as e:
+ raise exceptions.CommandError(e) from e
+
+ @command.command("flow.resume")
+ def resume(self, flows: typing.Sequence[flow.Flow]) -> None:
+ """
+ Resume flows if they are intercepted.
+ """
+ intercepted = [i for i in flows if i.intercepted]
+ for f in intercepted:
+ f.resume()
+ ctx.master.addons.trigger("update", intercepted)
+
+ # FIXME: this will become view.mark later
+ @command.command("flow.mark")
+ def mark(self, flows: typing.Sequence[flow.Flow], val: bool) -> None:
+ """
+ Mark flows.
+ """
+ updated = []
+ for i in flows:
+ if i.marked != val:
+ i.marked = val
+ updated.append(i)
+ ctx.master.addons.trigger("update", updated)
+
+ # FIXME: this will become view.mark.toggle later
+ @command.command("flow.mark.toggle")
+ def mark_toggle(self, flows: typing.Sequence[flow.Flow]) -> None:
+ """
+ Toggle mark for flows.
+ """
+ for i in flows:
+ i.marked = not i.marked
+ ctx.master.addons.trigger("update", flows)
+
+ @command.command("flow.kill")
+ def kill(self, flows: typing.Sequence[flow.Flow]) -> None:
+ """
+ Kill running flows.
+ """
+ updated = []
+ for f in flows:
+ if f.killable:
+ f.kill()
+ updated.append(f)
+ ctx.log.alert("Killed %s flows." % len(updated))
+ ctx.master.addons.trigger("update", updated)
+
+ # FIXME: this will become view.revert later
+ @command.command("flow.revert")
+ def revert(self, flows: typing.Sequence[flow.Flow]) -> None:
+ """
+ Revert flow changes.
+ """
+ updated = []
+ for f in flows:
+ if f.modified():
+ f.revert()
+ updated.append(f)
+ ctx.log.alert("Reverted %s flows." % len(updated))
+ ctx.master.addons.trigger("update", updated)
+
+ @command.command("flow.set.options")
+ def flow_set_options(self) -> typing.Sequence[str]:
+ return [
+ "host",
+ "status_code",
+ "method",
+ "path",
+ "url",
+ "reason",
+ ]
+
+ @command.command("flow.set")
+ def flow_set(
+ self,
+ flows: typing.Sequence[flow.Flow], spec: str, sval: str
+ ) -> None:
+ """
+ Quickly set a number of common values on flows.
+ """
+ opts = self.flow_set_options()
+ if spec not in opts:
+ raise exceptions.CommandError(
+ "Set spec must be one of: %s." % ", ".join(opts)
+ )
+
+ val = sval # type: typing.Union[int, str]
+ if spec == "status_code":
+ try:
+ val = int(val)
+ except ValueError as v:
+ raise exceptions.CommandError(
+ "Status code is not an integer: %s" % val
+ ) from v
+
+ updated = []
+ for f in flows:
+ req = getattr(f, "request", None)
+ rupdate = True
+ if req:
+ if spec == "method":
+ req.method = val
+ elif spec == "host":
+ req.host = val
+ elif spec == "path":
+ req.path = val
+ elif spec == "url":
+ try:
+ req.url = val
+ except ValueError as e:
+ raise exceptions.CommandError(
+ "URL %s is invalid: %s" % (repr(val), e)
+ ) from e
+ else:
+ self.rupdate = False
+
+ resp = getattr(f, "response", None)
+ supdate = True
+ if resp:
+ if spec == "status_code":
+ resp.status_code = val
+ if val in status_codes.RESPONSES:
+ resp.reason = status_codes.RESPONSES[int(val)]
+ elif spec == "reason":
+ resp.reason = val
+ else:
+ supdate = False
+
+ if rupdate or supdate:
+ updated.append(f)
+
+ ctx.master.addons.trigger("update", updated)
+ ctx.log.alert("Set %s on %s flows." % (spec, len(updated)))
+
+ @command.command("flow.decode")
+ def decode(self, flows: typing.Sequence[flow.Flow], part: str) -> None:
+ """
+ Decode flows.
+ """
+ updated = []
+ for f in flows:
+ p = getattr(f, part, None)
+ if p:
+ p.decode()
+ updated.append(f)
+ ctx.master.addons.trigger("update", updated)
+ ctx.log.alert("Decoded %s flows." % len(updated))
+
+ @command.command("flow.encode.toggle")
+ def encode_toggle(self, flows: typing.Sequence[flow.Flow], part: str) -> None:
+ """
+ Toggle flow encoding on and off, using deflate for encoding.
+ """
+ updated = []
+ for f in flows:
+ p = getattr(f, part, None)
+ if p:
+ current_enc = p.headers.get("content-encoding", "identity")
+ if current_enc == "identity":
+ p.encode("deflate")
+ else:
+ p.decode()
+ updated.append(f)
+ ctx.master.addons.trigger("update", updated)
+ ctx.log.alert("Toggled encoding on %s flows." % len(updated))
+
+ @command.command("flow.encode")
+ def encode(self, flows: typing.Sequence[flow.Flow], part: str, enc: str) -> None:
+ """
+ Encode flows with a specified encoding.
+ """
+ if enc not in self.encode_options():
+ raise exceptions.CommandError("Invalid encoding format: %s" % enc)
+
+ updated = []
+ for f in flows:
+ p = getattr(f, part, None)
+ if p:
+ current_enc = p.headers.get("content-encoding", "identity")
+ if current_enc == "identity":
+ p.encode(enc)
+ updated.append(f)
+ ctx.master.addons.trigger("update", updated)
+ ctx.log.alert("Encoded %s flows." % len(updated))
+
+ @command.command("flow.encode.options")
+ def encode_options(self) -> typing.Sequence[str]:
+ """
+ The possible values for an encoding specification.
+
+ """
+ return ["gzip", "deflate", "br"]
+
+ @command.command("options.load")
+ def options_load(self, path: str) -> None:
+ """
+ Load options from a file.
+ """
+ try:
+ optmanager.load_paths(ctx.options, path)
+ except (OSError, exceptions.OptionsError) as e:
+ raise exceptions.CommandError(
+ "Could not load options - %s" % e
+ ) from e
+
+ @command.command("options.save")
+ def options_save(self, path: str) -> None:
+ """
+ Save options to a file.
+ """
+ try:
+ optmanager.save(ctx.options, path)
+ except OSError as e:
+ raise exceptions.CommandError(
+ "Could not save options - %s" % e
+ ) from e
+
+ @command.command("options.reset")
+ def options_reset(self) -> None:
+ """
+ Reset all options to defaults.
+ """
+ ctx.options.reset()
+
+ @command.command("options.reset.one")
+ def options_reset_one(self, name: str) -> None:
+ """
+ Reset one option to its default value.
+ """
+ if name not in ctx.options:
+ raise exceptions.CommandError("No such option: %s" % name)
+ setattr(
+ ctx.options,
+ name,
+ ctx.options.default(name),
+ )
diff --git a/mitmproxy/addons/core_option_validation.py b/mitmproxy/addons/core_option_validation.py
index fd5f2788..baeee764 100644
--- a/mitmproxy/addons/core_option_validation.py
+++ b/mitmproxy/addons/core_option_validation.py
@@ -4,12 +4,14 @@
"""
from mitmproxy import exceptions
from mitmproxy import platform
+from mitmproxy import ctx
from mitmproxy.net import server_spec
from mitmproxy.utils import human
class CoreOptionValidation:
- def configure(self, opts, updated):
+ def configure(self, updated):
+ opts = ctx.options
if opts.add_upstream_certs_to_client_chain and not opts.upstream_cert:
raise exceptions.OptionsError(
"The no-upstream-cert and add-upstream-certs-to-client-chain "
diff --git a/mitmproxy/addons/cut.py b/mitmproxy/addons/cut.py
new file mode 100644
index 00000000..a4a2107b
--- /dev/null
+++ b/mitmproxy/addons/cut.py
@@ -0,0 +1,151 @@
+import io
+import csv
+import typing
+from mitmproxy import command
+from mitmproxy import exceptions
+from mitmproxy import flow
+from mitmproxy import ctx
+from mitmproxy import certs
+from mitmproxy.utils import strutils
+
+import pyperclip
+
+
+def headername(spec: str):
+ if not (spec.startswith("header[") and spec.endswith("]")):
+ raise exceptions.CommandError("Invalid header spec: %s" % spec)
+ return spec[len("header["):-1].strip()
+
+
+flow_shortcuts = {
+ "q": "request",
+ "s": "response",
+ "cc": "client_conn",
+ "sc": "server_conn",
+}
+
+
+def is_addr(v):
+ return isinstance(v, tuple) and len(v) > 1
+
+
+def extract(cut: str, f: flow.Flow) -> typing.Union[str, bytes]:
+ path = cut.split(".")
+ current = f # type: typing.Any
+ for i, spec in enumerate(path):
+ if spec.startswith("_"):
+ raise exceptions.CommandError("Can't access internal attribute %s" % spec)
+ if isinstance(current, flow.Flow):
+ spec = flow_shortcuts.get(spec, spec)
+
+ part = getattr(current, spec, None)
+ if i == len(path) - 1:
+ if spec == "port" and is_addr(current):
+ return str(current[1])
+ if spec == "host" and is_addr(current):
+ return str(current[0])
+ elif spec.startswith("header["):
+ return current.headers.get(headername(spec), "")
+ elif isinstance(part, bytes):
+ return part
+ elif isinstance(part, bool):
+ return "true" if part else "false"
+ elif isinstance(part, certs.SSLCert):
+ return part.to_pem().decode("ascii")
+ current = part
+ return str(current or "")
+
+
+def parse_cutspec(s: str) -> typing.Tuple[str, typing.Sequence[str]]:
+ """
+ Returns (flowspec, [cuts]).
+
+ Raises exceptions.CommandError if input is invalid.
+ """
+ parts = s.split("|", maxsplit=1)
+ flowspec = "@all"
+ if len(parts) == 2:
+ flowspec = parts[1].strip()
+ cuts = parts[0]
+ cutparts = [i.strip() for i in cuts.split(",") if i.strip()]
+ if len(cutparts) == 0:
+ raise exceptions.CommandError("Invalid cut specification.")
+ return flowspec, cutparts
+
+
+class Cut:
+ @command.command("cut")
+ def cut(self, cutspec: str) -> command.Cuts:
+ """
+ Resolve a cut specification of the form "cuts|flowspec". The cuts
+ are a comma-separated list of cut snippets. Cut snippets are
+ attribute paths from the base of the flow object, with a few
+ conveniences - "q", "s", "cc" and "sc" are shortcuts for request,
+ response, client_conn and server_conn, "port" and "host" retrieve
+ parts of an address tuple, ".header[key]" retrieves a header value.
+ Return values converted sensibly: SSL certicates are converted to PEM
+ format, bools are "true" or "false", "bytes" are preserved, and all
+ other values are converted to strings. The flowspec is optional, and
+ if it is not specified, it is assumed to be @all.
+ """
+ flowspec, cuts = parse_cutspec(cutspec)
+ flows = ctx.master.commands.call_args("view.resolve", [flowspec])
+ ret = []
+ for f in flows:
+ ret.append([extract(c, f) for c in cuts])
+ return ret
+
+ @command.command("cut.save")
+ def save(self, cuts: command.Cuts, path: str) -> None:
+ """
+ Save cuts to file. If there are multiple rows or columns, the format
+ is UTF-8 encoded CSV. If there is exactly one row and one column,
+ the data is written to file as-is, with raw bytes preserved. If the
+ path is prefixed with a "+", values are appended if there is an
+ existing file.
+ """
+ append = False
+ if path.startswith("+"):
+ append = True
+ path = path[1:]
+ if len(cuts) == 1 and len(cuts[0]) == 1:
+ with open(path, "ab" if append else "wb") as fp:
+ if fp.tell() > 0:
+ # We're appending to a file that already exists and has content
+ fp.write(b"\n")
+ v = cuts[0][0]
+ if isinstance(v, bytes):
+ fp.write(v)
+ else:
+ fp.write(v.encode("utf8"))
+ ctx.log.alert("Saved single cut.")
+ else:
+ with open(path, "a" if append else "w", newline='', encoding="utf8") as fp:
+ writer = csv.writer(fp)
+ for r in cuts:
+ writer.writerow(
+ [strutils.always_str(c) or "" for c in r] # type: ignore
+ )
+ ctx.log.alert("Saved %s cuts as CSV." % len(cuts))
+
+ @command.command("cut.clip")
+ def clip(self, cuts: command.Cuts) -> None:
+ """
+ Send cuts to the system clipboard.
+ """
+ fp = io.StringIO(newline="")
+ if len(cuts) == 1 and len(cuts[0]) == 1:
+ v = cuts[0][0]
+ if isinstance(v, bytes):
+ fp.write(strutils.always_str(v))
+ else:
+ fp.write("utf8")
+ ctx.log.alert("Clipped single cut.")
+ else:
+ writer = csv.writer(fp)
+ for r in cuts:
+ writer.writerow(
+ [strutils.always_str(c) or "" for c in r] # type: ignore
+ )
+ ctx.log.alert("Clipped %s cuts as CSV." % len(cuts))
+ pyperclip.copy(fp.getvalue())
diff --git a/mitmproxy/addons/disable_h2c.py b/mitmproxy/addons/disable_h2c.py
index b43d5207..392a29a5 100644
--- a/mitmproxy/addons/disable_h2c.py
+++ b/mitmproxy/addons/disable_h2c.py
@@ -14,9 +14,6 @@ class DisableH2C:
by sending the connection preface. We just kill those flows.
"""
- def configure(self, options, updated):
- pass
-
def process_flow(self, f):
if f.request.headers.get('upgrade', '') == 'h2c':
mitmproxy.ctx.log.warn("HTTP/2 cleartext connections (h2c upgrade requests) are currently not supported.")
diff --git a/mitmproxy/addons/dumper.py b/mitmproxy/addons/dumper.py
index 222f1167..3c3e1c65 100644
--- a/mitmproxy/addons/dumper.py
+++ b/mitmproxy/addons/dumper.py
@@ -29,24 +29,18 @@ def colorful(line, styles):
class Dumper:
def __init__(self, outfile=sys.stdout):
self.filter = None # type: flowfilter.TFilter
- self.flow_detail = None # type: int
self.outfp = outfile # type: typing.io.TextIO
- self.showhost = None # type: bool
- self.default_contentview = "auto" # type: str
- def configure(self, options, updated):
- if "filtstr" in updated:
- if options.filtstr:
- self.filter = flowfilter.parse(options.filtstr)
+ def configure(self, updated):
+ if "view_filter" in updated:
+ if ctx.options.view_filter:
+ self.filter = flowfilter.parse(ctx.options.view_filter)
if not self.filter:
raise exceptions.OptionsError(
- "Invalid filter expression: %s" % options.filtstr
+ "Invalid filter expression: %s" % ctx.options.view_filter
)
else:
self.filter = None
- self.flow_detail = options.flow_detail
- self.showhost = options.showhost
- self.default_contentview = options.default_contentview
def echo(self, text, ident=None, **style):
if ident:
@@ -67,13 +61,13 @@ class Dumper:
def _echo_message(self, message):
_, lines, error = contentviews.get_message_content_view(
- self.default_contentview,
+ ctx.options.default_contentview,
message
)
if error:
ctx.log.debug(error)
- if self.flow_detail == 3:
+ if ctx.options.flow_detail == 3:
lines_to_echo = itertools.islice(lines, 70)
else:
lines_to_echo = lines
@@ -95,14 +89,14 @@ class Dumper:
if next(lines, None):
self.echo("(cut off)", ident=4, dim=True)
- if self.flow_detail >= 2:
+ if ctx.options.flow_detail >= 2:
self.echo("")
def _echo_request_line(self, flow):
if flow.client_conn:
client = click.style(
strutils.escape_control_characters(
- repr(flow.client_conn.address)
+ human.format_address(flow.client_conn.address)
)
)
elif flow.request.is_replay:
@@ -121,12 +115,12 @@ class Dumper:
fg=method_color,
bold=True
)
- if self.showhost:
+ if ctx.options.showhost:
url = flow.request.pretty_url
else:
url = flow.request.url
terminalWidthLimit = max(shutil.get_terminal_size()[0] - 25, 50)
- if self.flow_detail < 1 and len(url) > terminalWidthLimit:
+ if ctx.options.flow_detail < 1 and len(url) > terminalWidthLimit:
url = url[:terminalWidthLimit] + "…"
url = click.style(strutils.escape_control_characters(url), bold=True)
@@ -176,7 +170,7 @@ class Dumper:
size = click.style(size, bold=True)
arrows = click.style(" <<", bold=True)
- if self.flow_detail == 1:
+ if ctx.options.flow_detail == 1:
# This aligns the HTTP response code with the HTTP request method:
# 127.0.0.1:59519: GET http://example.com/
# << 304 Not Modified 0b
@@ -194,16 +188,16 @@ class Dumper:
def echo_flow(self, f):
if f.request:
self._echo_request_line(f)
- if self.flow_detail >= 2:
+ if ctx.options.flow_detail >= 2:
self._echo_headers(f.request.headers)
- if self.flow_detail >= 3:
+ if ctx.options.flow_detail >= 3:
self._echo_message(f.request)
if f.response:
self._echo_response_line(f)
- if self.flow_detail >= 2:
+ if ctx.options.flow_detail >= 2:
self._echo_headers(f.response.headers)
- if self.flow_detail >= 3:
+ if ctx.options.flow_detail >= 3:
self._echo_message(f.response)
if f.error:
@@ -211,7 +205,7 @@ class Dumper:
self.echo(" << {}".format(msg), bold=True, fg="red")
def match(self, f):
- if self.flow_detail == 0:
+ if ctx.options.flow_detail == 0:
return False
if not self.filter:
return True
@@ -239,7 +233,7 @@ class Dumper:
if self.match(f):
message = f.messages[-1]
self.echo(f.message_info(message))
- if self.flow_detail >= 3:
+ if ctx.options.flow_detail >= 3:
self._echo_message(message)
def websocket_end(self, f):
@@ -267,5 +261,5 @@ class Dumper:
server=repr(f.server_conn.address),
direction=direction,
))
- if self.flow_detail >= 3:
+ if ctx.options.flow_detail >= 3:
self._echo_message(message)
diff --git a/mitmproxy/addons/export.py b/mitmproxy/addons/export.py
new file mode 100644
index 00000000..fd0c830e
--- /dev/null
+++ b/mitmproxy/addons/export.py
@@ -0,0 +1,75 @@
+import typing
+
+from mitmproxy import command
+from mitmproxy import flow
+from mitmproxy import exceptions
+from mitmproxy.utils import strutils
+from mitmproxy.net.http.http1 import assemble
+
+import pyperclip
+
+
+def curl_command(f: flow.Flow) -> str:
+ if not hasattr(f, "request"):
+ raise exceptions.CommandError("Can't export flow with no request.")
+ data = "curl "
+ request = f.request.copy() # type: ignore
+ request.decode(strict=False)
+ for k, v in request.headers.items(multi=True):
+ data += "-H '%s:%s' " % (k, v)
+ if request.method != "GET":
+ data += "-X %s " % request.method
+ data += "'%s'" % request.url
+ if request.content:
+ data += " --data-binary '%s'" % strutils.bytes_to_escaped_str(
+ request.content,
+ escape_single_quotes=True
+ )
+ return data
+
+
+def raw(f: flow.Flow) -> bytes:
+ if not hasattr(f, "request"):
+ raise exceptions.CommandError("Can't export flow with no request.")
+ return assemble.assemble_request(f.request) # type: ignore
+
+
+formats = dict(
+ curl = curl_command,
+ raw = raw,
+)
+
+
+class Export():
+ @command.command("export.formats")
+ def formats(self) -> typing.Sequence[str]:
+ """
+ Return a list of the supported export formats.
+ """
+ return list(sorted(formats.keys()))
+
+ @command.command("export.file")
+ def file(self, fmt: str, f: flow.Flow, path: str) -> None:
+ """
+ Export a flow to path.
+ """
+ if fmt not in formats:
+ raise exceptions.CommandError("No such export format: %s" % fmt)
+ func = formats[fmt] # type: typing.Any
+ v = func(f)
+ with open(path, "wb") as fp:
+ if isinstance(v, bytes):
+ fp.write(v)
+ else:
+ fp.write(v.encode("utf-8"))
+
+ @command.command("export.clip")
+ def clip(self, fmt: str, f: flow.Flow) -> None:
+ """
+ Export a flow to the system clipboard.
+ """
+ if fmt not in formats:
+ raise exceptions.CommandError("No such export format: %s" % fmt)
+ func = formats[fmt] # type: typing.Any
+ v = strutils.always_str(func(f))
+ pyperclip.copy(v)
diff --git a/mitmproxy/addons/intercept.py b/mitmproxy/addons/intercept.py
index 4a3fe17e..ac8c4c88 100644
--- a/mitmproxy/addons/intercept.py
+++ b/mitmproxy/addons/intercept.py
@@ -1,20 +1,21 @@
from mitmproxy import flowfilter
from mitmproxy import exceptions
+from mitmproxy import ctx
class Intercept:
def __init__(self):
self.filt = None
- def configure(self, opts, updated):
+ def configure(self, updated):
if "intercept" in updated:
- if not opts.intercept:
+ if not ctx.options.intercept:
self.filt = None
return
- self.filt = flowfilter.parse(opts.intercept)
+ self.filt = flowfilter.parse(ctx.options.intercept)
if not self.filt:
raise exceptions.OptionsError(
- "Invalid interception filter: %s" % opts.intercept
+ "Invalid interception filter: %s" % ctx.options.intercept
)
def process_flow(self, f):
diff --git a/mitmproxy/addons/onboarding.py b/mitmproxy/addons/onboarding.py
index cb57990f..07536c34 100644
--- a/mitmproxy/addons/onboarding.py
+++ b/mitmproxy/addons/onboarding.py
@@ -1,17 +1,18 @@
from mitmproxy.addons import wsgiapp
from mitmproxy.addons.onboardingapp import app
+from mitmproxy import ctx
class Onboarding(wsgiapp.WSGIApp):
+ name = "onboarding"
+
def __init__(self):
super().__init__(app.Adapter(app.application), None, None)
- self.enabled = False
- def configure(self, options, updated):
- self.host = options.onboarding_host
- self.port = options.onboarding_port
- self.enabled = options.onboarding
+ def configure(self, updated):
+ self.host = ctx.options.onboarding_host
+ self.port = ctx.options.onboarding_port
def request(self, f):
- if self.enabled:
+ if ctx.options.onboarding:
super().request(f)
diff --git a/mitmproxy/addons/onboardingapp/app.py b/mitmproxy/addons/onboardingapp/app.py
index d418952c..0f09e32c 100644
--- a/mitmproxy/addons/onboardingapp/app.py
+++ b/mitmproxy/addons/onboardingapp/app.py
@@ -44,6 +44,18 @@ class PEM(tornado.web.RequestHandler):
def filename(self):
return config.CONF_BASENAME + "-ca-cert.pem"
+ def head(self):
+ p = os.path.join(self.request.master.options.cadir, self.filename)
+ p = os.path.expanduser(p)
+ content_length = os.path.getsize(p)
+
+ self.set_header("Content-Type", "application/x-x509-ca-cert")
+ self.set_header(
+ "Content-Disposition",
+ "inline; filename={}".format(
+ self.filename))
+ self.set_header("Content-Length", content_length)
+
def get(self):
p = os.path.join(self.request.master.options.cadir, self.filename)
p = os.path.expanduser(p)
@@ -63,6 +75,19 @@ class P12(tornado.web.RequestHandler):
def filename(self):
return config.CONF_BASENAME + "-ca-cert.p12"
+ def head(self):
+ p = os.path.join(self.request.master.options.cadir, self.filename)
+ p = os.path.expanduser(p)
+ content_length = os.path.getsize(p)
+
+ self.set_header("Content-Type", "application/x-pkcs12")
+ self.set_header(
+ "Content-Disposition",
+ "inline; filename={}".format(
+ self.filename))
+
+ self.set_header("Content-Length", content_length)
+
def get(self):
p = os.path.join(self.request.master.options.cadir, self.filename)
p = os.path.expanduser(p)
diff --git a/mitmproxy/addons/onboardingapp/templates/index.html b/mitmproxy/addons/onboardingapp/templates/index.html
index f2b54b69..c8d0f07a 100644
--- a/mitmproxy/addons/onboardingapp/templates/index.html
+++ b/mitmproxy/addons/onboardingapp/templates/index.html
@@ -1,47 +1,86 @@
{% extends "frame.html" %}
{% block body %}
+<script>
+function changeTo(device) {
+ if (device == "apple") {
+ var text = `<h3>Apple: How to install on macOS / OSX</h3>
+ <ul>
+ <li>Double-click the PEM file</li>
+ <li>The "Keychain Access" applications opens</li>
+ <li>Find the new certificate "mitmproxy" in the list</li>
+ <li>Double-click the "mitmproxy" entry</li>
+ <li>A dialog window openes up</li>
+ <li>Change "Secure Socket Layer (SSL)" to "Always Trust"</li>
+ <li>Close the dialog window (and enter your password if prompted)</li>
+ <li>For iOS version 10.3 or up, you need to make sure mitmproxy is enabled in<br>
+ Certificate Trust Settings, you can check it by going to<br>
+ Settings > General > About > Certificate Trust Settings</li>
+ <li>Done!</li>
+ </ul>`;
+ }
+ else if (device == "windows") {
+ var text = `<h3>Windows: How to install on Windows</h3>
+ <ul>
+ <li>Double-click the P12 file</li>
+ <li>Select Store Location for Current User and click Next</li>
+ <li>Click Next</li>
+ <li>Leave the Password column blank and click Next</li>
+ <li>Select Place all certificates in the following store</li>
+ <li>Click Browse and select Trusted Root Certification Authorities</li>
+ <li>Click Next and then click Finish</li>
+ <li>Click Yes if prompted for confirmation</li>
+ <li>Done!</li>
+ </ul>`;
+ }
+ else if (device == "android") {
+ var text = `<h3>Android: How to install on Android</h3>
+ <ul>
+ <li>Open your device's Settings app</li>
+ <li>Under "Credential storage," tap Install from storage</li>
+ <li>Under "Open from," tap where you saved the certificate</li>
+ <li>Tap the file</li>
+ <li>If prompted, enter the key store password and tap OK</li>
+ <li>Type a name for the certificate</li>
+ <li>Pick VPN and apps</li>
+ <li>Tap OK</li>
+ <li>Done!</li>
+ </ul>`;
+ }
+ else if (device == "asterisk") {
+ var text = "";
+ }
+ document.getElementById("dynamic").innerHTML = text;
+}
+</script>
+
<center>
<h2> Click to install the mitmproxy certificate: </h2>
</center>
<div id="certbank" class="row">
<div class="col-md-3">
- <a href="/cert/pem"><i class="fa fa-apple fa-5x"></i></a>
+ <a onclick="changeTo('apple')" href="/cert/pem"><i class="fa fa-apple fa-5x"></i></a>
<p>Apple</p>
</div>
<div class="col-md-3">
- <a href="/cert/p12"><i class="fa fa-windows fa-5x"></i></a>
+ <a onclick="changeTo('windows')" href="/cert/p12"><i class="fa fa-windows fa-5x"></i></a>
<p>Windows</p>
</div>
<div class="col-md-3">
- <a href="/cert/pem"><i class="fa fa-android fa-5x"></i></a>
+ <a onclick="changeTo('android')" href="/cert/pem"><i class="fa fa-android fa-5x"></i></a>
<p>Android</p>
</div>
<div class="col-md-3">
- <a href="/cert/pem"><i class="fa fa-asterisk fa-5x"></i></a>
+ <a onclick="changeTo('asterisk')" href="/cert/pem"><i class="fa fa-asterisk fa-5x"></i></a>
<p>Other</p>
</div>
</div>
<hr/>
-<div class="text-left">
- <h3>Apple: How to install on macOS / OSX</h3>
- <ul>
- <li>Download PEM file (from above link)</li>
- <li>Double-click the PEM file</li>
- <li>The "Keychain Access" applications opens</li>
- <li>Find the new certificate "mitmproxy" in the list</li>
- <li>Double-click the "mitmproxy" entry</li>
- <li>A dialog window openes up</li>
- <li>Change "Secure Socket Layer (SSL)" to "Always Trust"</li>
- <li>Close the dialog window (and enter your password if prompted)</li>
- <li>Done!</li>
- </ul>
+<div class="text-left" id="dynamic">
</div>
-
-
<hr/>
<div class="text-center">
diff --git a/mitmproxy/addons/proxyauth.py b/mitmproxy/addons/proxyauth.py
index 43677f61..5f884b55 100644
--- a/mitmproxy/addons/proxyauth.py
+++ b/mitmproxy/addons/proxyauth.py
@@ -1,5 +1,6 @@
import binascii
import weakref
+import ldap3
from typing import Optional
from typing import MutableMapping # noqa
from typing import Tuple
@@ -10,6 +11,7 @@ import mitmproxy.net.http
from mitmproxy import connections # noqa
from mitmproxy import exceptions
from mitmproxy import http
+from mitmproxy import ctx
from mitmproxy.net.http import status_codes
REALM = "mitmproxy"
@@ -45,12 +47,13 @@ class ProxyAuth:
self.nonanonymous = False
self.htpasswd = None
self.singleuser = None
- self.mode = None
+ self.ldapconn = None
+ self.ldapserver = None
self.authenticated = weakref.WeakKeyDictionary() # type: MutableMapping[connections.ClientConnection, Tuple[str, str]]
"""Contains all connections that are permanently authenticated after an HTTP CONNECT"""
def enabled(self) -> bool:
- return any([self.nonanonymous, self.htpasswd, self.singleuser])
+ return any([self.nonanonymous, self.htpasswd, self.singleuser, self.ldapconn, self.ldapserver])
def is_proxy_auth(self) -> bool:
"""
@@ -58,7 +61,7 @@ class ProxyAuth:
- True, if authentication is done as if mitmproxy is a proxy
- False, if authentication is done as if mitmproxy is a HTTP server
"""
- return self.mode in ("regular", "upstream")
+ return ctx.options.mode in ("regular", "upstream")
def which_auth_header(self) -> str:
if self.is_proxy_auth():
@@ -99,7 +102,18 @@ class ProxyAuth:
elif self.htpasswd:
if self.htpasswd.check_password(username, password):
return username, password
-
+ elif self.ldapconn:
+ if not username or not password:
+ return None
+ self.ldapconn.search(ctx.options.proxyauth.split(':')[4], '(cn=' + username + ')')
+ if self.ldapconn.response:
+ conn = ldap3.Connection(
+ self.ldapserver,
+ self.ldapconn.response[0]['dn'],
+ password,
+ auto_bind=True)
+ if conn:
+ return username, password
return None
def authenticate(self, f: http.HTTPFlow) -> bool:
@@ -113,37 +127,61 @@ class ProxyAuth:
return False
# Handlers
- def configure(self, options, updated):
+ def configure(self, updated):
if "proxyauth" in updated:
self.nonanonymous = False
self.singleuser = None
self.htpasswd = None
- if options.proxyauth:
- if options.proxyauth == "any":
+ self.ldapserver = None
+ if ctx.options.proxyauth:
+ if ctx.options.proxyauth == "any":
self.nonanonymous = True
- elif options.proxyauth.startswith("@"):
- p = options.proxyauth[1:]
+ elif ctx.options.proxyauth.startswith("@"):
+ p = ctx.options.proxyauth[1:]
try:
self.htpasswd = passlib.apache.HtpasswdFile(p)
except (ValueError, OSError) as v:
raise exceptions.OptionsError(
"Could not open htpasswd file: %s" % p
)
+ elif ctx.options.proxyauth.startswith("ldap"):
+ parts = ctx.options.proxyauth.split(':')
+ security = parts[0]
+ ldap_server = parts[1]
+ dn_baseauth = parts[2]
+ password_baseauth = parts[3]
+ if len(parts) != 5:
+ raise exceptions.OptionsError(
+ "Invalid ldap specification"
+ )
+ if security == "ldaps":
+ server = ldap3.Server(ldap_server, use_ssl=True)
+ elif security == "ldap":
+ server = ldap3.Server(ldap_server)
+ else:
+ raise exceptions.OptionsError(
+ "Invalid ldap specfication on the first part"
+ )
+ conn = ldap3.Connection(
+ server,
+ dn_baseauth,
+ password_baseauth,
+ auto_bind=True)
+ self.ldapconn = conn
+ self.ldapserver = server
else:
- parts = options.proxyauth.split(':')
+ parts = ctx.options.proxyauth.split(':')
if len(parts) != 2:
raise exceptions.OptionsError(
"Invalid single-user auth specification."
)
self.singleuser = parts
- if "mode" in updated:
- self.mode = options.mode
if self.enabled():
- if options.mode == "transparent":
+ if ctx.options.mode == "transparent":
raise exceptions.OptionsError(
"Proxy Authentication not supported in transparent mode."
)
- if options.mode == "socks5":
+ if ctx.options.mode == "socks5":
raise exceptions.OptionsError(
"Proxy Authentication not supported in SOCKS mode. "
"https://github.com/mitmproxy/mitmproxy/issues/738"
diff --git a/mitmproxy/addons/readfile.py b/mitmproxy/addons/readfile.py
index 03dcd084..05b6c309 100644
--- a/mitmproxy/addons/readfile.py
+++ b/mitmproxy/addons/readfile.py
@@ -1,46 +1,56 @@
import os.path
+import sys
+import typing
from mitmproxy import ctx
-from mitmproxy import io
from mitmproxy import exceptions
+from mitmproxy import io
class ReadFile:
"""
An addon that handles reading from file on startup.
"""
- def __init__(self):
- self.path = None
- def load_flows_file(self, path: str) -> int:
- path = os.path.expanduser(path)
+ def load_flows(self, fo: typing.IO[bytes]) -> int:
cnt = 0
+ freader = io.FlowReader(fo)
try:
- with open(path, "rb") as f:
- freader = io.FlowReader(f)
- for i in freader.stream():
- cnt += 1
- ctx.master.load_flow(i)
- return cnt
- except (IOError, exceptions.FlowReadException) as v:
+ for flow in freader.stream():
+ ctx.master.load_flow(flow)
+ cnt += 1
+ except (IOError, exceptions.FlowReadException) as e:
if cnt:
- ctx.log.warn(
- "Flow file corrupted - loaded %i flows." % cnt,
- )
+ ctx.log.warn("Flow file corrupted - loaded %i flows." % cnt)
else:
ctx.log.error("Flow file corrupted.")
- raise exceptions.FlowReadException(v)
+ raise exceptions.FlowReadException(str(e)) from e
+ else:
+ return cnt
- def configure(self, options, updated):
- if "rfile" in updated and options.rfile:
- self.path = options.rfile
+ def load_flows_from_path(self, path: str) -> int:
+ path = os.path.expanduser(path)
+ try:
+ with open(path, "rb") as f:
+ return self.load_flows(f)
+ except IOError as e:
+ ctx.log.error("Cannot load flows: {}".format(e))
+ raise exceptions.FlowReadException(str(e)) from e
def running(self):
- if self.path:
+ if ctx.options.rfile:
try:
- self.load_flows_file(self.path)
- except exceptions.FlowReadException as v:
- raise exceptions.OptionsError(v)
+ self.load_flows_from_path(ctx.options.rfile)
+ except exceptions.FlowReadException as e:
+ raise exceptions.OptionsError(e) from e
finally:
- self.path = None
ctx.master.addons.trigger("processing_complete")
+
+
+class ReadFileStdin(ReadFile):
+ """Support the special case of "-" for reading from stdin"""
+ def load_flows_from_path(self, path: str) -> int:
+ if path == "-":
+ return self.load_flows(sys.stdin.buffer)
+ else:
+ return super().load_flows_from_path(path)
diff --git a/mitmproxy/addons/readstdin.py b/mitmproxy/addons/readstdin.py
deleted file mode 100644
index 93a99f01..00000000
--- a/mitmproxy/addons/readstdin.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from mitmproxy import ctx
-from mitmproxy import io
-from mitmproxy import exceptions
-import sys
-
-
-class ReadStdin:
- """
- An addon that reads from stdin if we're not attached to (someting like)
- a tty.
- """
- def running(self, stdin = sys.stdin):
- if not stdin.isatty():
- ctx.log.info("Reading from stdin")
- try:
- stdin.buffer.read(0)
- except Exception as e:
- ctx.log.warn("Cannot read from stdin: {}".format(e))
- return
- freader = io.FlowReader(stdin.buffer)
- try:
- for i in freader.stream():
- ctx.master.load_flow(i)
- except exceptions.FlowReadException as e:
- ctx.log.error("Error reading from stdin: %s" % e)
- ctx.master.addons.trigger("processing_complete")
diff --git a/mitmproxy/addons/replace.py b/mitmproxy/addons/replace.py
index d6c11ca4..054264fa 100644
--- a/mitmproxy/addons/replace.py
+++ b/mitmproxy/addons/replace.py
@@ -47,7 +47,7 @@ class Replace:
def __init__(self):
self.lst = []
- def configure(self, options, updated):
+ def configure(self, updated):
"""
.replacements is a list of tuples (fpat, rex, s):
@@ -57,7 +57,7 @@ class Replace:
"""
if "replacements" in updated:
lst = []
- for rep in options.replacements:
+ for rep in ctx.options.replacements:
fpatt, rex, s = parse_hook(rep)
flt = flowfilter.parse(fpatt)
diff --git a/mitmproxy/addons/save.py b/mitmproxy/addons/save.py
new file mode 100644
index 00000000..3dbef14e
--- /dev/null
+++ b/mitmproxy/addons/save.py
@@ -0,0 +1,93 @@
+import os.path
+import typing
+
+from mitmproxy import exceptions
+from mitmproxy import flowfilter
+from mitmproxy import io
+from mitmproxy import ctx
+from mitmproxy import flow
+
+
+class Save:
+ def __init__(self):
+ self.stream = None
+ self.filt = None
+ self.active_flows = set() # type: Set[flow.Flow]
+
+ def open_file(self, path):
+ if path.startswith("+"):
+ path = path[1:]
+ mode = "ab"
+ else:
+ mode = "wb"
+ path = os.path.expanduser(path)
+ return open(path, mode)
+
+ def start_stream_to_path(self, path, flt):
+ try:
+ f = self.open_file(path)
+ except IOError as v:
+ raise exceptions.OptionsError(str(v))
+ self.stream = io.FilteredFlowWriter(f, flt)
+ self.active_flows = set()
+
+ def configure(self, updated):
+ # We're already streaming - stop the previous stream and restart
+ if "save_stream_filter" in updated:
+ if ctx.options.save_stream_filter:
+ self.filt = flowfilter.parse(ctx.options.save_stream_filter)
+ if not self.filt:
+ raise exceptions.OptionsError(
+ "Invalid filter specification: %s" % ctx.options.save_stream_filter
+ )
+ else:
+ self.filt = None
+ if "save_stream_file" in updated:
+ if self.stream:
+ self.done()
+ if ctx.options.save_stream_file:
+ self.start_stream_to_path(ctx.options.save_stream_file, self.filt)
+
+ def save(self, flows: typing.Sequence[flow.Flow], path: str) -> None:
+ """
+ Save flows to a file. If the path starts with a +, flows are
+ appended to the file, otherwise it is over-written.
+ """
+ try:
+ f = self.open_file(path)
+ except IOError as v:
+ raise exceptions.CommandError(v) from v
+ stream = io.FlowWriter(f)
+ for i in flows:
+ stream.add(i)
+ f.close()
+ ctx.log.alert("Saved %s flows." % len(flows))
+
+ def load(self, l):
+ l.add_command("save.file", self.save)
+
+ def tcp_start(self, flow):
+ if self.stream:
+ self.active_flows.add(flow)
+
+ def tcp_end(self, flow):
+ if self.stream:
+ self.stream.add(flow)
+ self.active_flows.discard(flow)
+
+ def response(self, flow):
+ if self.stream:
+ self.stream.add(flow)
+ self.active_flows.discard(flow)
+
+ def request(self, flow):
+ if self.stream:
+ self.active_flows.add(flow)
+
+ def done(self):
+ if self.stream:
+ for f in self.active_flows:
+ self.stream.add(f)
+ self.active_flows = set([])
+ self.stream.fo.close()
+ self.stream = None
diff --git a/mitmproxy/addons/script.py b/mitmproxy/addons/script.py
index 4d893f1c..e90dd885 100644
--- a/mitmproxy/addons/script.py
+++ b/mitmproxy/addons/script.py
@@ -1,209 +1,75 @@
-import contextlib
import os
-import shlex
+import importlib
+import time
import sys
-import threading
-import traceback
-import types
+import typing
+from mitmproxy import addonmanager
from mitmproxy import exceptions
-from mitmproxy import ctx
+from mitmproxy import flow
+from mitmproxy import command
from mitmproxy import eventsequence
+from mitmproxy import ctx
-import watchdog.events
-from watchdog.observers import polling
-
-
-def parse_command(command):
- """
- Returns a (path, args) tuple.
- """
- if not command or not command.strip():
- raise ValueError("Empty script command.")
- # Windows: escape all backslashes in the path.
- if os.name == "nt": # pragma: no cover
- backslashes = shlex.split(command, posix=False)[0].count("\\")
- command = command.replace("\\", "\\\\", backslashes)
- args = shlex.split(command) # pragma: no cover
- args[0] = os.path.expanduser(args[0])
- if not os.path.exists(args[0]):
- raise ValueError(
- ("Script file not found: %s.\r\n"
- "If your script path contains spaces, "
- "make sure to wrap it in additional quotes, e.g. -s \"'./foo bar/baz.py' --args\".") %
- args[0])
- elif os.path.isdir(args[0]):
- raise ValueError("Not a file: %s" % args[0])
- return args[0], args[1:]
-
-
-def cut_traceback(tb, func_name):
- """
- Cut off a traceback at the function with the given name.
- The func_name's frame is excluded.
-
- Args:
- tb: traceback object, as returned by sys.exc_info()[2]
- func_name: function name
-
- Returns:
- Reduced traceback.
- """
- tb_orig = tb
-
- for _, _, fname, _ in traceback.extract_tb(tb):
- tb = tb.tb_next
- if fname == func_name:
- break
-
- if tb is None:
- # We could not find the method, take the full stack trace.
- # This may happen on some Python interpreters/flavors (e.g. PyInstaller).
- return tb_orig
- else:
- return tb
-
-
-@contextlib.contextmanager
-def scriptenv(path, args):
- oldargs = sys.argv
- sys.argv = [path] + args
- script_dir = os.path.dirname(os.path.abspath(path))
- sys.path.append(script_dir)
+def load_script(actx, path):
+ if not os.path.exists(path):
+ ctx.log.info("No such file: %s" % path)
+ return
+ loader = importlib.machinery.SourceFileLoader(os.path.basename(path), path)
try:
- yield
- except SystemExit as v:
- ctx.log.error("Script exited with code %s" % v.code)
- except Exception:
- etype, value, tb = sys.exc_info()
- tb = cut_traceback(tb, "scriptenv").tb_next
- ctx.log.error(
- "Script error: %s" % "".join(
- traceback.format_exception(etype, value, tb)
- )
- )
+ oldpath = sys.path
+ sys.path.insert(0, os.path.dirname(path))
+ with addonmanager.safecall():
+ m = loader.load_module()
+ if not getattr(m, "name", None):
+ m.name = path
+ return m
finally:
- sys.argv = oldargs
- sys.path.pop()
-
-
-def load_script(path, args):
- with open(path, "rb") as f:
- try:
- code = compile(f.read(), path, 'exec')
- except SyntaxError as e:
- ctx.log.error(
- "Script error: %s line %s: %s" % (
- e.filename, e.lineno, e.msg
- )
- )
- return
- ns = {'__file__': os.path.abspath(path)}
- with scriptenv(path, args):
- exec(code, ns)
- return types.SimpleNamespace(**ns)
-
-
-class ReloadHandler(watchdog.events.FileSystemEventHandler):
- def __init__(self, callback):
- self.callback = callback
-
- def filter(self, event):
- """
- Returns True only when .py file is changed
- """
- if event.is_directory:
- return False
- if os.path.basename(event.src_path).startswith("."):
- return False
- if event.src_path.endswith(".py"):
- return True
- return False
-
- def on_modified(self, event):
- if self.filter(event):
- self.callback()
-
- def on_created(self, event):
- if self.filter(event):
- self.callback()
+ sys.path[:] = oldpath
class Script:
"""
An addon that manages a single script.
"""
- def __init__(self, command):
- self.name = command
+ ReloadInterval = 2
- self.command = command
- self.path, self.args = parse_command(command)
+ def __init__(self, path):
+ self.name = "scriptmanager:" + path
+ self.path = path
+ self.fullpath = os.path.expanduser(path)
self.ns = None
- self.observer = None
- self.dead = False
-
- self.last_options = None
- self.should_reload = threading.Event()
-
- for i in eventsequence.Events:
- if not hasattr(self, i):
- def mkprox():
- evt = i
-
- def prox(*args, **kwargs):
- self.run(evt, *args, **kwargs)
- return prox
- setattr(self, i, mkprox())
-
- def run(self, name, *args, **kwargs):
- # It's possible for ns to be un-initialised if we failed during
- # configure
- if self.ns is not None and not self.dead:
- func = getattr(self.ns, name, None)
- if func:
- with scriptenv(self.path, self.args):
- return func(*args, **kwargs)
- def reload(self):
- self.should_reload.set()
+ self.last_load = 0
+ self.last_mtime = 0
+ if not os.path.isfile(self.fullpath):
+ raise exceptions.OptionsError("No such script: %s" % path)
- def load_script(self):
- self.ns = load_script(self.path, self.args)
- ret = self.run("start", self.last_options)
- if ret:
- self.ns = ret
- self.run("start", self.last_options)
+ @property
+ def addons(self):
+ return [self.ns] if self.ns else []
def tick(self):
- if self.should_reload.is_set():
- self.should_reload.clear()
- ctx.log.info("Reloading script: %s" % self.name)
- self.ns = load_script(self.path, self.args)
- self.start(self.last_options)
- self.configure(self.last_options, self.last_options.keys())
- else:
- self.run("tick")
-
- def start(self, opts):
- self.last_options = opts
- self.load_script()
-
- def configure(self, options, updated):
- self.last_options = options
- if not self.observer:
- self.observer = polling.PollingObserver()
- # Bind the handler to the real underlying master object
- self.observer.schedule(
- ReloadHandler(self.reload),
- os.path.dirname(self.path) or "."
- )
- self.observer.start()
- self.run("configure", options, updated)
-
- def done(self):
- self.run("done")
- self.dead = True
+ if time.time() - self.last_load > self.ReloadInterval:
+ mtime = os.stat(self.fullpath).st_mtime
+ if mtime > self.last_mtime:
+ ctx.log.info("Loading script: %s" % self.path)
+ if self.ns:
+ ctx.master.addons.remove(self.ns)
+ self.ns = load_script(ctx, self.fullpath)
+ if self.ns:
+ # We're already running, so we have to explicitly register and
+ # configure the addon
+ ctx.master.addons.register(self.ns)
+ ctx.master.addons.invoke_addon(self.ns, "running")
+ ctx.master.addons.invoke_addon(
+ self.ns,
+ "configure",
+ ctx.options.keys()
+ )
+ self.last_load = time.time()
+ self.last_mtime = mtime
class ScriptLoader:
@@ -212,72 +78,68 @@ class ScriptLoader:
"""
def __init__(self):
self.is_running = False
+ self.addons = []
def running(self):
self.is_running = True
- def run_once(self, command, flows):
+ @command.command("script.run")
+ def script_run(self, flows: typing.Sequence[flow.Flow], path: str) -> None:
+ """
+ Run a script on the specified flows. The script is loaded with
+ default options, and all lifecycle events for each flow are
+ simulated.
+ """
try:
- sc = Script(command)
- except ValueError as e:
- raise ValueError(str(e))
- sc.load_script()
- for f in flows:
- for evt, o in eventsequence.iterate(f):
- sc.run(evt, o)
- sc.done()
- return sc
-
- def configure(self, options, updated):
+ s = Script(path)
+ l = addonmanager.Loader(ctx.master)
+ ctx.master.addons.invoke_addon(s, "load", l)
+ ctx.master.addons.invoke_addon(s, "configure", ctx.options.keys())
+ # Script is loaded on the first tick
+ ctx.master.addons.invoke_addon(s, "tick")
+ for f in flows:
+ for evt, arg in eventsequence.iterate(f):
+ ctx.master.addons.invoke_addon(s, evt, arg)
+ except exceptions.OptionsError as e:
+ raise exceptions.CommandError("Error running script: %s" % e) from e
+
+ def configure(self, updated):
if "scripts" in updated:
- for s in options.scripts:
- if options.scripts.count(s) > 1:
+ for s in ctx.options.scripts:
+ if ctx.options.scripts.count(s) > 1:
raise exceptions.OptionsError("Duplicate script: %s" % s)
- for a in ctx.master.addons.chain[:]:
- if isinstance(a, Script) and a.name not in options.scripts:
+ for a in self.addons[:]:
+ if a.path not in ctx.options.scripts:
ctx.log.info("Un-loading script: %s" % a.name)
ctx.master.addons.remove(a)
+ self.addons.remove(a)
# The machinations below are to ensure that:
# - Scripts remain in the same order
- # - Scripts are listed directly after the script addon. This is
- # needed to ensure that interactions with, for instance, flow
- # serialization remains correct.
# - Scripts are not initialized un-necessarily. If only a
- # script's order in the script list has changed, it should simply
- # be moved.
+ # script's order in the script list has changed, it is just
+ # moved.
current = {}
- for a in ctx.master.addons.chain[:]:
- if isinstance(a, Script):
- current[a.name] = a
- ctx.master.addons.chain.remove(a)
+ for a in self.addons:
+ current[a.path] = a
ordered = []
newscripts = []
- for s in options.scripts:
+ for s in ctx.options.scripts:
if s in current:
ordered.append(current[s])
else:
- ctx.log.info("Loading script: %s" % s)
- try:
- sc = Script(s)
- except ValueError as e:
- raise exceptions.OptionsError(str(e))
+ sc = Script(s)
ordered.append(sc)
newscripts.append(sc)
- ochain = ctx.master.addons.chain
- pos = ochain.index(self)
- ctx.master.addons.chain = ochain[:pos + 1] + ordered + ochain[pos + 1:]
+ self.addons = ordered
for s in newscripts:
- ctx.master.addons.invoke_addon(s, "start", options)
+ ctx.master.addons.register(s)
if self.is_running:
# If we're already running, we configure and tell the addon
# we're up and running.
- ctx.master.addons.invoke_addon(
- s, "configure", options, options.keys()
- )
ctx.master.addons.invoke_addon(s, "running")
diff --git a/mitmproxy/addons/serverplayback.py b/mitmproxy/addons/serverplayback.py
index be2d6f2b..927f6e15 100644
--- a/mitmproxy/addons/serverplayback.py
+++ b/mitmproxy/addons/serverplayback.py
@@ -1,29 +1,50 @@
import hashlib
import urllib
+import typing
from typing import Any # noqa
from typing import List # noqa
from mitmproxy import ctx
+from mitmproxy import flow
from mitmproxy import exceptions
from mitmproxy import io
+from mitmproxy import command
class ServerPlayback:
def __init__(self):
- self.options = None
-
self.flowmap = {}
self.stop = False
self.final_flow = None
+ self.configured = False
- def load(self, flows):
+ @command.command("replay.server")
+ def load_flows(self, flows: typing.Sequence[flow.Flow]) -> None:
+ """
+ Replay server responses from flows.
+ """
+ self.flowmap = {}
for i in flows:
- if i.response:
+ if i.response: # type: ignore
l = self.flowmap.setdefault(self._hash(i), [])
l.append(i)
-
- def clear(self):
+ ctx.master.addons.trigger("update", [])
+
+ @command.command("replay.server.file")
+ def load_file(self, path: str) -> None:
+ try:
+ flows = io.read_flows_from_paths([path])
+ except exceptions.FlowReadException as e:
+ raise exceptions.CommandError(str(e))
+ self.load_flows(flows)
+
+ @command.command("replay.server.stop")
+ def clear(self) -> None:
+ """
+ Stop server replay.
+ """
self.flowmap = {}
+ ctx.master.addons.trigger("update", [])
def count(self):
return sum([len(i) for i in self.flowmap.values()])
@@ -38,27 +59,27 @@ class ServerPlayback:
queriesArray = urllib.parse.parse_qsl(query, keep_blank_values=True)
key = [str(r.port), str(r.scheme), str(r.method), str(path)] # type: List[Any]
- if not self.options.server_replay_ignore_content:
- if self.options.server_replay_ignore_payload_params and r.multipart_form:
+ if not ctx.options.server_replay_ignore_content:
+ if ctx.options.server_replay_ignore_payload_params and r.multipart_form:
key.extend(
(k, v)
for k, v in r.multipart_form.items(multi=True)
- if k.decode(errors="replace") not in self.options.server_replay_ignore_payload_params
+ if k.decode(errors="replace") not in ctx.options.server_replay_ignore_payload_params
)
- elif self.options.server_replay_ignore_payload_params and r.urlencoded_form:
+ elif ctx.options.server_replay_ignore_payload_params and r.urlencoded_form:
key.extend(
(k, v)
for k, v in r.urlencoded_form.items(multi=True)
- if k not in self.options.server_replay_ignore_payload_params
+ if k not in ctx.options.server_replay_ignore_payload_params
)
else:
key.append(str(r.raw_content))
- if not self.options.server_replay_ignore_host:
+ if not ctx.options.server_replay_ignore_host:
key.append(r.host)
filtered = []
- ignore_params = self.options.server_replay_ignore_params or []
+ ignore_params = ctx.options.server_replay_ignore_params or []
for p in queriesArray:
if p[0] not in ignore_params:
filtered.append(p)
@@ -66,9 +87,9 @@ class ServerPlayback:
key.append(p[0])
key.append(p[1])
- if self.options.server_replay_use_headers:
+ if ctx.options.server_replay_use_headers:
headers = []
- for i in self.options.server_replay_use_headers:
+ for i in ctx.options.server_replay_use_headers:
v = r.headers.get(i)
headers.append((i, v))
key.append(headers)
@@ -83,7 +104,7 @@ class ServerPlayback:
"""
hsh = self._hash(request)
if hsh in self.flowmap:
- if self.options.server_replay_nopop:
+ if ctx.options.server_replay_nopop:
return self.flowmap[hsh][0]
else:
ret = self.flowmap[hsh].pop(0)
@@ -91,16 +112,14 @@ class ServerPlayback:
del self.flowmap[hsh]
return ret
- def configure(self, options, updated):
- self.options = options
- if "server_replay" in updated:
- self.clear()
- if options.server_replay:
- try:
- flows = io.read_flows_from_paths(options.server_replay)
- except exceptions.FlowReadException as e:
- raise exceptions.OptionsError(str(e))
- self.load(flows)
+ def configure(self, updated):
+ if not self.configured and ctx.options.server_replay:
+ self.configured = True
+ try:
+ flows = io.read_flows_from_paths(ctx.options.server_replay)
+ except exceptions.FlowReadException as e:
+ raise exceptions.OptionsError(str(e))
+ self.load_flows(flows)
def tick(self):
if self.stop and not self.final_flow.live:
@@ -112,13 +131,13 @@ class ServerPlayback:
if rflow:
response = rflow.response.copy()
response.is_replay = True
- if self.options.refresh_server_playback:
+ if ctx.options.refresh_server_playback:
response.refresh()
f.response = response
if not self.flowmap:
self.final_flow = f
self.stop = True
- elif self.options.replay_kill_extra:
+ elif ctx.options.replay_kill_extra:
ctx.log.warn(
"server_playback: killed non-replay request {}".format(
f.request.url
diff --git a/mitmproxy/addons/setheaders.py b/mitmproxy/addons/setheaders.py
index 9e60eabd..d4d16e40 100644
--- a/mitmproxy/addons/setheaders.py
+++ b/mitmproxy/addons/setheaders.py
@@ -1,5 +1,6 @@
from mitmproxy import exceptions
from mitmproxy import flowfilter
+from mitmproxy import ctx
def parse_setheader(s):
@@ -43,17 +44,10 @@ class SetHeaders:
def __init__(self):
self.lst = []
- def configure(self, options, updated):
- """
- options.setheaders is a tuple of (fpatt, header, value)
-
- fpatt: String specifying a filter pattern.
- header: Header name.
- value: Header value string
- """
+ def configure(self, updated):
if "setheaders" in updated:
self.lst = []
- for shead in options.setheaders:
+ for shead in ctx.options.setheaders:
fpatt, header, value = parse_setheader(shead)
flt = flowfilter.parse(fpatt)
diff --git a/mitmproxy/addons/stickyauth.py b/mitmproxy/addons/stickyauth.py
index 1a1d4fc4..24831d5b 100644
--- a/mitmproxy/addons/stickyauth.py
+++ b/mitmproxy/addons/stickyauth.py
@@ -1,5 +1,6 @@
from mitmproxy import exceptions
from mitmproxy import flowfilter
+from mitmproxy import ctx
class StickyAuth:
@@ -7,13 +8,13 @@ class StickyAuth:
self.flt = None
self.hosts = {}
- def configure(self, options, updated):
+ def configure(self, updated):
if "stickyauth" in updated:
- if options.stickyauth:
- flt = flowfilter.parse(options.stickyauth)
+ if ctx.options.stickyauth:
+ flt = flowfilter.parse(ctx.options.stickyauth)
if not flt:
raise exceptions.OptionsError(
- "stickyauth: invalid filter expression: %s" % options.stickyauth
+ "stickyauth: invalid filter expression: %s" % ctx.options.stickyauth
)
self.flt = flt
else:
diff --git a/mitmproxy/addons/stickycookie.py b/mitmproxy/addons/stickycookie.py
index fb1c5072..e58e0a58 100644
--- a/mitmproxy/addons/stickycookie.py
+++ b/mitmproxy/addons/stickycookie.py
@@ -1,13 +1,14 @@
import collections
from http import cookiejar
+from typing import List, Tuple, Dict, Optional # noqa
+from mitmproxy import http, flowfilter, ctx, exceptions
from mitmproxy.net.http import cookies
-from mitmproxy import exceptions
-from mitmproxy import flowfilter
+TOrigin = Tuple[str, int, str]
-def ckey(attrs, f):
+def ckey(attrs: Dict[str, str], f: http.HTTPFlow) -> TOrigin:
"""
Returns a (domain, port, path) tuple.
"""
@@ -20,32 +21,32 @@ def ckey(attrs, f):
return (domain, f.request.port, path)
-def domain_match(a, b):
- if cookiejar.domain_match(a, b):
+def domain_match(a: str, b: str) -> bool:
+ if cookiejar.domain_match(a, b): # type: ignore
return True
- elif cookiejar.domain_match(a, b.strip(".")):
+ elif cookiejar.domain_match(a, b.strip(".")): # type: ignore
return True
return False
class StickyCookie:
def __init__(self):
- self.jar = collections.defaultdict(dict)
- self.flt = None
+ self.jar = collections.defaultdict(dict) # type: Dict[TOrigin, Dict[str, str]]
+ self.flt = None # type: Optional[flowfilter.TFilter]
- def configure(self, options, updated):
+ def configure(self, updated):
if "stickycookie" in updated:
- if options.stickycookie:
- flt = flowfilter.parse(options.stickycookie)
+ if ctx.options.stickycookie:
+ flt = flowfilter.parse(ctx.options.stickycookie)
if not flt:
raise exceptions.OptionsError(
- "stickycookie: invalid filter expression: %s" % options.stickycookie
+ "stickycookie: invalid filter expression: %s" % ctx.options.stickycookie
)
self.flt = flt
else:
self.flt = None
- def response(self, flow):
+ def response(self, flow: http.HTTPFlow):
if self.flt:
for name, (value, attrs) in flow.response.cookies.items(multi=True):
# FIXME: We now know that Cookie.py screws up some cookies with
@@ -62,24 +63,21 @@ class StickyCookie:
if not self.jar[dom_port_path]:
self.jar.pop(dom_port_path, None)
else:
- b = attrs.copy()
- b.insert(0, name, value)
- self.jar[dom_port_path][name] = b
+ self.jar[dom_port_path][name] = value
- def request(self, flow):
+ def request(self, flow: http.HTTPFlow):
if self.flt:
- l = []
+ cookie_list = [] # type: List[Tuple[str,str]]
if flowfilter.match(self.flt, flow):
- for domain, port, path in self.jar.keys():
+ for (domain, port, path), c in self.jar.items():
match = [
domain_match(flow.request.host, domain),
flow.request.port == port,
flow.request.path.startswith(path)
]
if all(match):
- c = self.jar[(domain, port, path)]
- l.extend([cookies.format_cookie_header(c[name].items(multi=True)) for name in c.keys()])
- if l:
+ cookie_list.extend(c.items())
+ if cookie_list:
# FIXME: we need to formalise this...
- flow.request.stickycookie = True
- flow.request.headers["cookie"] = "; ".join(l)
+ flow.metadata["stickycookie"] = True
+ flow.request.headers["cookie"] = cookies.format_cookie_header(cookie_list)
diff --git a/mitmproxy/addons/streambodies.py b/mitmproxy/addons/streambodies.py
index a10bdb93..181f0337 100644
--- a/mitmproxy/addons/streambodies.py
+++ b/mitmproxy/addons/streambodies.py
@@ -8,10 +8,10 @@ class StreamBodies:
def __init__(self):
self.max_size = None
- def configure(self, options, updated):
- if "stream_large_bodies" in updated and options.stream_large_bodies:
+ def configure(self, updated):
+ if "stream_large_bodies" in updated and ctx.options.stream_large_bodies:
try:
- self.max_size = human.parse_size(options.stream_large_bodies)
+ self.max_size = human.parse_size(ctx.options.stream_large_bodies)
except ValueError as e:
raise exceptions.OptionsError(e)
diff --git a/mitmproxy/addons/streamfile.py b/mitmproxy/addons/streamfile.py
deleted file mode 100644
index 624297f2..00000000
--- a/mitmproxy/addons/streamfile.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import os.path
-
-from mitmproxy import exceptions
-from mitmproxy import flowfilter
-from mitmproxy import io
-
-
-class StreamFile:
- def __init__(self):
- self.stream = None
- self.filt = None
- self.active_flows = set() # type: Set[flow.Flow]
-
- def start_stream_to_path(self, path, mode, flt):
- path = os.path.expanduser(path)
- try:
- f = open(path, mode)
- except IOError as v:
- raise exceptions.OptionsError(str(v))
- self.stream = io.FilteredFlowWriter(f, flt)
- self.active_flows = set()
-
- def configure(self, options, updated):
- # We're already streaming - stop the previous stream and restart
- if "filtstr" in updated:
- if options.filtstr:
- self.filt = flowfilter.parse(options.filtstr)
- if not self.filt:
- raise exceptions.OptionsError(
- "Invalid filter specification: %s" % options.filtstr
- )
- else:
- self.filt = None
- if "streamfile" in updated:
- if self.stream:
- self.done()
- if options.streamfile:
- if options.streamfile.startswith("+"):
- path = options.streamfile[1:]
- mode = "ab"
- else:
- path = options.streamfile
- mode = "wb"
- self.start_stream_to_path(path, mode, self.filt)
-
- def tcp_start(self, flow):
- if self.stream:
- self.active_flows.add(flow)
-
- def tcp_end(self, flow):
- if self.stream:
- self.stream.add(flow)
- self.active_flows.discard(flow)
-
- def response(self, flow):
- if self.stream:
- self.stream.add(flow)
- self.active_flows.discard(flow)
-
- def request(self, flow):
- if self.stream:
- self.active_flows.add(flow)
-
- def done(self):
- if self.stream:
- for flow in self.active_flows:
- self.stream.add(flow)
- self.active_flows = set([])
- self.stream.fo.close()
- self.stream = None
diff --git a/mitmproxy/addons/termlog.py b/mitmproxy/addons/termlog.py
index 5fdb6245..4c37b005 100644
--- a/mitmproxy/addons/termlog.py
+++ b/mitmproxy/addons/termlog.py
@@ -2,23 +2,25 @@ import sys
import click
from mitmproxy import log
+from mitmproxy import ctx
+
+# These get over-ridden by the save execution context. Keep them around so we
+# can log directly.
+realstdout = sys.stdout
+realstderr = sys.stderr
class TermLog:
def __init__(self, outfile=None):
- self.options = None
self.outfile = outfile
- def configure(self, options, updated):
- self.options = options
-
def log(self, e):
if log.log_tier(e.level) == log.log_tier("error"):
- outfile = self.outfile or sys.stderr
+ outfile = self.outfile or realstderr
else:
- outfile = self.outfile or sys.stdout
+ outfile = self.outfile or realstdout
- if self.options.verbosity >= log.log_tier(e.level):
+ if ctx.options.verbosity >= log.log_tier(e.level):
click.secho(
e.msg,
file=outfile,
diff --git a/mitmproxy/addons/termstatus.py b/mitmproxy/addons/termstatus.py
index 7b05f409..c3c91283 100644
--- a/mitmproxy/addons/termstatus.py
+++ b/mitmproxy/addons/termstatus.py
@@ -1,4 +1,5 @@
from mitmproxy import ctx
+from mitmproxy.utils import human
"""
A tiny addon to print the proxy status to terminal. Eventually this could
@@ -7,17 +8,10 @@ from mitmproxy import ctx
class TermStatus:
- def __init__(self):
- self.server = False
-
- def configure(self, options, updated):
- if "server" in updated:
- self.server = options.server
-
def running(self):
- if self.server:
+ if ctx.options.server:
ctx.log.info(
- "Proxy server listening at http://{}:{}".format(
- *ctx.master.server.address,
+ "Proxy server listening at http://{}".format(
+ human.format_address(ctx.master.server.address)
)
)
diff --git a/mitmproxy/addons/upstream_auth.py b/mitmproxy/addons/upstream_auth.py
index 9beecfc0..685494c2 100644
--- a/mitmproxy/addons/upstream_auth.py
+++ b/mitmproxy/addons/upstream_auth.py
@@ -2,6 +2,7 @@ import re
import base64
from mitmproxy import exceptions
+from mitmproxy import ctx
from mitmproxy.utils import strutils
@@ -26,20 +27,17 @@ class UpstreamAuth():
"""
def __init__(self):
self.auth = None
- self.root_mode = None
- def configure(self, options, updated):
+ def configure(self, updated):
# FIXME: We're doing this because our proxy core is terminally confused
# at the moment. Ideally, we should be able to check if we're in
# reverse proxy mode at the HTTP layer, so that scripts can put the
# proxy in reverse proxy mode for specific reuests.
- if "mode" in updated:
- self.root_mode = options.mode
if "upstream_auth" in updated:
- if options.upstream_auth is None:
+ if ctx.options.upstream_auth is None:
self.auth = None
else:
- self.auth = parse_upstream_auth(options.upstream_auth)
+ self.auth = parse_upstream_auth(ctx.options.upstream_auth)
def http_connect(self, f):
if self.auth and f.mode == "upstream":
@@ -49,5 +47,5 @@ class UpstreamAuth():
if self.auth:
if f.mode == "upstream" and not f.server_conn.via:
f.request.headers["Proxy-Authorization"] = self.auth
- elif self.root_mode == "reverse":
+ elif ctx.options.mode == "reverse":
f.request.headers["Proxy-Authorization"] = self.auth
diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py
index 2218327c..13a17c56 100644
--- a/mitmproxy/addons/view.py
+++ b/mitmproxy/addons/view.py
@@ -10,7 +10,6 @@ The View:
"""
import collections
import typing
-import datetime
import blinker
import sortedcontainers
@@ -18,6 +17,11 @@ import sortedcontainers
import mitmproxy.flow
from mitmproxy import flowfilter
from mitmproxy import exceptions
+from mitmproxy import command
+from mitmproxy import connections
+from mitmproxy import ctx
+from mitmproxy import io
+from mitmproxy import http # noqa
# The underlying sorted list implementation expects the sort key to be stable
# for the lifetime of the object. However, if we sort by size, for instance,
@@ -34,7 +38,7 @@ class _OrderKey:
def __init__(self, view):
self.view = view
- def generate(self, f: mitmproxy.flow.Flow) -> typing.Any: # pragma: no cover
+ def generate(self, f: http.HTTPFlow) -> typing.Any: # pragma: no cover
pass
def refresh(self, f):
@@ -64,22 +68,22 @@ class _OrderKey:
class OrderRequestStart(_OrderKey):
- def generate(self, f: mitmproxy.flow.Flow) -> datetime.datetime:
+ def generate(self, f: http.HTTPFlow) -> int:
return f.request.timestamp_start or 0
class OrderRequestMethod(_OrderKey):
- def generate(self, f: mitmproxy.flow.Flow) -> str:
+ def generate(self, f: http.HTTPFlow) -> str:
return f.request.method
class OrderRequestURL(_OrderKey):
- def generate(self, f: mitmproxy.flow.Flow) -> str:
+ def generate(self, f: http.HTTPFlow) -> str:
return f.request.url
class OrderKeySize(_OrderKey):
- def generate(self, f: mitmproxy.flow.Flow) -> int:
+ def generate(self, f: http.HTTPFlow) -> int:
s = 0
if f.request.raw_content:
s += len(f.request.raw_content)
@@ -109,16 +113,16 @@ class View(collections.Sequence):
self.default_order = OrderRequestStart(self)
self.orders = dict(
- time = self.default_order,
- method = OrderRequestMethod(self),
- url = OrderRequestURL(self),
- size = OrderKeySize(self),
+ time = OrderRequestStart(self), method = OrderRequestMethod(self),
+ url = OrderRequestURL(self), size = OrderKeySize(self),
)
self.order_key = self.default_order
self.order_reversed = False
self.focus_follow = False
- self._view = sortedcontainers.SortedListWithKey(key = self.order_key)
+ self._view = sortedcontainers.SortedListWithKey(
+ key = self.order_key
+ )
# The sig_view* signals broadcast events that affect the view. That is,
# an update to a flow in the store but not in the view does not trigger
@@ -165,7 +169,7 @@ class View(collections.Sequence):
def __len__(self):
return len(self._view)
- def __getitem__(self, offset) -> mitmproxy.flow.Flow:
+ def __getitem__(self, offset) -> typing.Any:
return self._view[self._rev(offset)]
# Reflect some methods to the efficient underlying implementation
@@ -177,7 +181,7 @@ class View(collections.Sequence):
def index(self, f: mitmproxy.flow.Flow, start: int = 0, stop: typing.Optional[int] = None) -> int:
return self._rev(self._view.index(f, start, stop))
- def __contains__(self, f: mitmproxy.flow.Flow) -> bool:
+ def __contains__(self, f: typing.Any) -> bool:
return self._view.__contains__(f)
def _order_key_name(self):
@@ -197,7 +201,36 @@ class View(collections.Sequence):
self.sig_view_refresh.send(self)
# API
- def toggle_marked(self):
+ @command.command("view.focus.next")
+ def focus_next(self) -> None:
+ """
+ Set focus to the next flow.
+ """
+ idx = self.focus.index + 1
+ if self.inbounds(idx):
+ self.focus.flow = self[idx]
+
+ @command.command("view.focus.prev")
+ def focus_prev(self) -> None:
+ """
+ Set focus to the previous flow.
+ """
+ idx = self.focus.index - 1
+ if self.inbounds(idx):
+ self.focus.flow = self[idx]
+
+ @command.command("view.order.options")
+ def order_options(self) -> typing.Sequence[str]:
+ """
+ A list of all the orders we support.
+ """
+ return list(sorted(self.orders.keys()))
+
+ @command.command("view.marked.toggle")
+ def toggle_marked(self) -> None:
+ """
+ Toggle whether to show marked views only.
+ """
self.show_marked = not self.show_marked
self._refilter()
@@ -221,7 +254,7 @@ class View(collections.Sequence):
self.filter = flt or matchall
self._refilter()
- def clear(self):
+ def clear(self) -> None:
"""
Clears both the store and view.
"""
@@ -241,55 +274,19 @@ class View(collections.Sequence):
self._refilter()
self.sig_store_refresh.send(self)
- def add(self, f: mitmproxy.flow.Flow) -> None:
+ def add(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None:
"""
Adds a flow to the state. If the flow already exists, it is
ignored.
"""
- if f.id not in self._store:
- self._store[f.id] = f
- if self.filter(f):
- self._base_add(f)
- if self.focus_follow:
- self.focus.flow = f
- self.sig_view_add.send(self, flow=f)
-
- def remove(self, f: mitmproxy.flow.Flow):
- """
- Removes the flow from the underlying store and the view.
- """
- if f.id in self._store:
- if f in self._view:
- self._view.remove(f)
- self.sig_view_remove.send(self, flow=f)
- del self._store[f.id]
- self.sig_store_remove.send(self, flow=f)
-
- def update(self, f: mitmproxy.flow.Flow):
- """
- Updates a flow. If the flow is not in the state, it's ignored.
- """
- if f.id in self._store:
- if self.filter(f):
- if f not in self._view:
+ for f in flows:
+ if f.id not in self._store:
+ self._store[f.id] = f
+ if self.filter(f):
self._base_add(f)
if self.focus_follow:
self.focus.flow = f
self.sig_view_add.send(self, flow=f)
- else:
- # This is a tad complicated. The sortedcontainers
- # implementation assumes that the order key is stable. If
- # it changes mid-way Very Bad Things happen. We detect when
- # this happens, and re-fresh the item.
- self.order_key.refresh(f)
- self.sig_view_update.send(self, flow=f)
- else:
- try:
- self._view.remove(f)
- self.sig_view_remove.send(self, flow=f)
- except ValueError:
- # The value was not in the view
- pass
def get_by_id(self, flow_id: str) -> typing.Optional[mitmproxy.flow.Flow]:
"""
@@ -298,48 +295,199 @@ class View(collections.Sequence):
"""
return self._store.get(flow_id)
+ @command.command("view.getval")
+ def getvalue(self, f: mitmproxy.flow.Flow, key: str, default: str) -> str:
+ """
+ Get a value from the settings store for the specified flow.
+ """
+ return self.settings[f].get(key, default)
+
+ @command.command("view.setval.toggle")
+ def setvalue_toggle(
+ self,
+ flows: typing.Sequence[mitmproxy.flow.Flow],
+ key: str
+ ) -> None:
+ """
+ Toggle a boolean value in the settings store, seting the value to
+ the string "true" or "false".
+ """
+ updated = []
+ for f in flows:
+ current = self.settings[f].get("key", "false")
+ self.settings[f][key] = "false" if current == "true" else "true"
+ updated.append(f)
+ ctx.master.addons.trigger("update", updated)
+
+ @command.command("view.setval")
+ def setvalue(
+ self,
+ flows: typing.Sequence[mitmproxy.flow.Flow],
+ key: str, value: str
+ ) -> None:
+ """
+ Set a value in the settings store for the specified flows.
+ """
+ updated = []
+ for f in flows:
+ self.settings[f][key] = value
+ updated.append(f)
+ ctx.master.addons.trigger("update", updated)
+
+ @command.command("view.load")
+ def load_file(self, path: str) -> None:
+ """
+ Load flows into the view, without processing them with addons.
+ """
+ for i in io.FlowReader(open(path, "rb")).stream():
+ # Do this to get a new ID, so we can load the same file N times and
+ # get new flows each time. It would be more efficient to just have a
+ # .newid() method or something.
+ self.add([i.copy()])
+
+ @command.command("view.go")
+ def go(self, dst: int) -> None:
+ """
+ Go to a specified offset. Positive offests are from the beginning of
+ the view, negative from the end of the view, so that 0 is the first
+ flow, -1 is the last flow.
+ """
+ if len(self) == 0:
+ return
+ if dst < 0:
+ dst = len(self) + dst
+ if dst < 0:
+ dst = 0
+ if dst > len(self) - 1:
+ dst = len(self) - 1
+ self.focus.flow = self[dst]
+
+ @command.command("view.duplicate")
+ def duplicate(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None:
+ """
+ Duplicates the specified flows, and sets the focus to the first
+ duplicate.
+ """
+ dups = [f.copy() for f in flows]
+ if dups:
+ self.add(dups)
+ self.focus.flow = dups[0]
+ ctx.log.alert("Duplicated %s flows" % len(dups))
+
+ @command.command("view.remove")
+ def remove(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None:
+ """
+ Removes the flow from the underlying store and the view.
+ """
+ for f in flows:
+ if f.id in self._store:
+ if f.killable:
+ f.kill()
+ if f in self._view:
+ self._view.remove(f)
+ self.sig_view_remove.send(self, flow=f)
+ del self._store[f.id]
+ self.sig_store_remove.send(self, flow=f)
+
+ @command.command("view.resolve")
+ def resolve(self, spec: str) -> typing.Sequence[mitmproxy.flow.Flow]:
+ """
+ Resolve a flow list specification to an actual list of flows.
+ """
+ if spec == "@all":
+ return [i for i in self._store.values()]
+ if spec == "@focus":
+ return [self.focus.flow] if self.focus.flow else []
+ elif spec == "@shown":
+ return [i for i in self]
+ elif spec == "@hidden":
+ return [i for i in self._store.values() if i not in self._view]
+ elif spec == "@marked":
+ return [i for i in self._store.values() if i.marked]
+ elif spec == "@unmarked":
+ return [i for i in self._store.values() if not i.marked]
+ else:
+ filt = flowfilter.parse(spec)
+ if not filt:
+ raise exceptions.CommandError("Invalid flow filter: %s" % spec)
+ return [i for i in self._store.values() if filt(i)]
+
+ @command.command("view.create")
+ def create(self, method: str, url: str) -> None:
+ req = http.HTTPRequest.make(method.upper(), url)
+ c = connections.ClientConnection.make_dummy(("", 0))
+ s = connections.ServerConnection.make_dummy((req.host, req.port))
+ f = http.HTTPFlow(c, s)
+ f.request = req
+ f.request.headers["Host"] = req.host
+ self.add([f])
+
# Event handlers
- def configure(self, opts, updated):
- if "filter" in updated:
+ def configure(self, updated):
+ if "view_filter" in updated:
filt = None
- if opts.filter:
- filt = flowfilter.parse(opts.filter)
+ if ctx.options.view_filter:
+ filt = flowfilter.parse(ctx.options.view_filter)
if not filt:
raise exceptions.OptionsError(
- "Invalid interception filter: %s" % opts.filter
+ "Invalid interception filter: %s" % ctx.options.view_filter
)
self.set_filter(filt)
if "console_order" in updated:
- if opts.console_order is None:
- self.set_order(self.default_order)
- else:
- if opts.console_order not in self.orders:
- raise exceptions.OptionsError(
- "Unknown flow order: %s" % opts.console_order
- )
- self.set_order(self.orders[opts.console_order])
+ if ctx.options.console_order not in self.orders:
+ raise exceptions.OptionsError(
+ "Unknown flow order: %s" % ctx.options.console_order
+ )
+ self.set_order(self.orders[ctx.options.console_order])
if "console_order_reversed" in updated:
- self.set_reversed(opts.console_order_reversed)
+ self.set_reversed(ctx.options.console_order_reversed)
if "console_focus_follow" in updated:
- self.focus_follow = opts.console_focus_follow
+ self.focus_follow = ctx.options.console_focus_follow
def request(self, f):
- self.add(f)
+ self.add([f])
def error(self, f):
- self.update(f)
+ self.update([f])
def response(self, f):
- self.update(f)
+ self.update([f])
def intercept(self, f):
- self.update(f)
+ self.update([f])
def resume(self, f):
- self.update(f)
+ self.update([f])
def kill(self, f):
- self.update(f)
+ self.update([f])
+
+ def update(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None:
+ """
+ Updates a list of flows. If flow is not in the state, it's ignored.
+ """
+ for f in flows:
+ if f.id in self._store:
+ if self.filter(f):
+ if f not in self._view:
+ self._base_add(f)
+ if self.focus_follow:
+ self.focus.flow = f
+ self.sig_view_add.send(self, flow=f)
+ else:
+ # This is a tad complicated. The sortedcontainers
+ # implementation assumes that the order key is stable. If
+ # it changes mid-way Very Bad Things happen. We detect when
+ # this happens, and re-fresh the item.
+ self.order_key.refresh(f)
+ self.sig_view_update.send(self, flow=f)
+ else:
+ try:
+ self._view.remove(f)
+ self.sig_view_remove.send(self, flow=f)
+ except ValueError:
+ # The value was not in the view
+ pass
class Focus:
@@ -405,7 +553,7 @@ class Focus:
class Settings(collections.Mapping):
def __init__(self, view: View) -> None:
self.view = view
- self._values = {} # type: typing.MutableMapping[str, mitmproxy.flow.Flow]
+ self._values = {} # type: typing.MutableMapping[str, typing.Dict]
view.sig_store_remove.connect(self._sig_store_remove)
view.sig_store_refresh.connect(self._sig_store_refresh)
diff --git a/mitmproxy/addons/wsgiapp.py b/mitmproxy/addons/wsgiapp.py
index c37fcb7b..155444fc 100644
--- a/mitmproxy/addons/wsgiapp.py
+++ b/mitmproxy/addons/wsgiapp.py
@@ -13,6 +13,10 @@ class WSGIApp:
def __init__(self, app, host, port):
self.app, self.host, self.port = app, host, port
+ @property
+ def name(self):
+ return "wsgiapp:%s:%s" % (self.host, self.port)
+
def serve(self, app, flow):
"""
Serves app on flow, and prevents further handling of the flow.
diff --git a/mitmproxy/command.py b/mitmproxy/command.py
new file mode 100644
index 00000000..82b8fae4
--- /dev/null
+++ b/mitmproxy/command.py
@@ -0,0 +1,195 @@
+"""
+ This module manges and invokes typed commands.
+"""
+import inspect
+import typing
+import shlex
+import textwrap
+import functools
+import sys
+
+from mitmproxy.utils import typecheck
+from mitmproxy import exceptions
+from mitmproxy import flow
+
+
+Cuts = typing.Sequence[
+ typing.Sequence[typing.Union[str, bytes]]
+]
+
+
+def typename(t: type, ret: bool) -> str:
+ """
+ Translates a type to an explanatory string. If ret is True, we're
+ looking at a return type, else we're looking at a parameter type.
+ """
+ if issubclass(t, (str, int, bool)):
+ return t.__name__
+ elif t == typing.Sequence[flow.Flow]:
+ return "[flow]" if ret else "flowspec"
+ elif t == typing.Sequence[str]:
+ return "[str]"
+ elif t == Cuts:
+ return "[cuts]" if ret else "cutspec"
+ elif t == flow.Flow:
+ return "flow"
+ else: # pragma: no cover
+ raise NotImplementedError(t)
+
+
+class Command:
+ def __init__(self, manager, path, func) -> None:
+ self.path = path
+ self.manager = manager
+ self.func = func
+ sig = inspect.signature(self.func)
+ self.help = None
+ if func.__doc__:
+ txt = func.__doc__.strip()
+ self.help = "\n".join(textwrap.wrap(txt))
+
+ self.has_positional = False
+ for i in sig.parameters.values():
+ # This is the kind for *args paramters
+ if i.kind == i.VAR_POSITIONAL:
+ self.has_positional = True
+ self.paramtypes = [v.annotation for v in sig.parameters.values()]
+ self.returntype = sig.return_annotation
+
+ def paramnames(self) -> typing.Sequence[str]:
+ v = [typename(i, False) for i in self.paramtypes]
+ if self.has_positional:
+ v[-1] = "*" + v[-1][1:-1]
+ return v
+
+ def retname(self) -> str:
+ return typename(self.returntype, True) if self.returntype else ""
+
+ def signature_help(self) -> str:
+ params = " ".join(self.paramnames())
+ ret = self.retname()
+ if ret:
+ ret = " -> " + ret
+ return "%s %s%s" % (self.path, params, ret)
+
+ def call(self, args: typing.Sequence[str]):
+ """
+ Call the command with a set of arguments. At this point, all argumets are strings.
+ """
+ if not self.has_positional and (len(self.paramtypes) != len(args)):
+ raise exceptions.CommandError("Usage: %s" % self.signature_help())
+
+ remainder = [] # type: typing.Sequence[str]
+ if self.has_positional:
+ remainder = args[len(self.paramtypes) - 1:]
+ args = args[:len(self.paramtypes) - 1]
+
+ pargs = []
+ for i in range(len(args)):
+ if typecheck.check_command_type(args[i], self.paramtypes[i]):
+ pargs.append(args[i])
+ else:
+ pargs.append(parsearg(self.manager, args[i], self.paramtypes[i]))
+
+ if remainder:
+ if typecheck.check_command_type(remainder, self.paramtypes[-1]):
+ pargs.extend(remainder)
+ else:
+ raise exceptions.CommandError("Invalid value type.")
+
+ with self.manager.master.handlecontext():
+ ret = self.func(*pargs)
+
+ if not typecheck.check_command_type(ret, self.returntype):
+ raise exceptions.CommandError("Command returned unexpected data")
+
+ return ret
+
+
+class CommandManager:
+ def __init__(self, master):
+ self.master = master
+ self.commands = {}
+
+ def collect_commands(self, addon):
+ for i in dir(addon):
+ if not i.startswith("__"):
+ o = getattr(addon, i)
+ if hasattr(o, "command_path"):
+ self.add(o.command_path, o)
+
+ def add(self, path: str, func: typing.Callable):
+ self.commands[path] = Command(self, path, func)
+
+ def call_args(self, path, args):
+ """
+ Call a command using a list of string arguments. May raise CommandError.
+ """
+ if path not in self.commands:
+ raise exceptions.CommandError("Unknown command: %s" % path)
+ return self.commands[path].call(args)
+
+ def call(self, cmdstr: str):
+ """
+ Call a command using a string. May raise CommandError.
+ """
+ parts = shlex.split(cmdstr)
+ if not len(parts) >= 1:
+ raise exceptions.CommandError("Invalid command: %s" % cmdstr)
+ return self.call_args(parts[0], parts[1:])
+
+ def dump(self, out=sys.stdout) -> None:
+ cmds = list(self.commands.values())
+ cmds.sort(key=lambda x: x.signature_help())
+ for c in cmds:
+ for hl in (c.help or "").splitlines():
+ print("# " + hl, file=out)
+ print(c.signature_help(), file=out)
+ print(file=out)
+
+
+def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any:
+ """
+ Convert a string to a argument to the appropriate type.
+ """
+ if issubclass(argtype, str):
+ return spec
+ elif argtype == bool:
+ if spec == "true":
+ return True
+ elif spec == "false":
+ return False
+ else:
+ raise exceptions.CommandError(
+ "Booleans are 'true' or 'false', got %s" % spec
+ )
+ elif issubclass(argtype, int):
+ try:
+ return int(spec)
+ except ValueError as e:
+ raise exceptions.CommandError("Expected an integer, got %s." % spec)
+ elif argtype == typing.Sequence[flow.Flow]:
+ return manager.call_args("view.resolve", [spec])
+ elif argtype == Cuts:
+ return manager.call_args("cut", [spec])
+ elif argtype == flow.Flow:
+ flows = manager.call_args("view.resolve", [spec])
+ if len(flows) != 1:
+ raise exceptions.CommandError(
+ "Command requires one flow, specification matched %s." % len(flows)
+ )
+ return flows[0]
+ elif argtype == typing.Sequence[str]:
+ return [i.strip() for i in spec.split(",")]
+ else:
+ raise exceptions.CommandError("Unsupported argument type: %s" % argtype)
+
+
+def command(path):
+ def decorator(function):
+ @functools.wraps(function)
+ def wrapper(*args, **kwargs):
+ return function(*args, **kwargs)
+ wrapper.__dict__["command_path"] = path
+ return wrapper
+ return decorator
diff --git a/mitmproxy/contentviews/image/image_parser.py b/mitmproxy/contentviews/image/image_parser.py
index 062fb38e..7c74669a 100644
--- a/mitmproxy/contentviews/image/image_parser.py
+++ b/mitmproxy/contentviews/image/image_parser.py
@@ -37,7 +37,7 @@ def parse_gif(data: bytes) -> Metadata:
descriptor = img.logical_screen_descriptor
parts = [
('Format', 'Compuserve GIF'),
- ('Version', "GIF{}".format(img.header.version.decode('ASCII'))),
+ ('Version', "GIF{}".format(img.hdr.version)),
('Size', "{} x {} px".format(descriptor.screen_width, descriptor.screen_height)),
('background', str(descriptor.bg_color_index))
]
diff --git a/mitmproxy/contrib/kaitaistruct/exif.py b/mitmproxy/contrib/kaitaistruct/exif.py
index 6236a708..d99cceef 100644
--- a/mitmproxy/contrib/kaitaistruct/exif.py
+++ b/mitmproxy/contrib/kaitaistruct/exif.py
@@ -1,16 +1,20 @@
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
-# The source was exif.ksy from here - https://github.com/kaitai-io/kaitai_struct_formats/blob/24e2d00048b8084ceec30a187a79cb87a79a48ba/image/exif.ksy
import array
import struct
import zlib
from enum import Enum
+from pkg_resources import parse_version
-from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
+from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO
+if parse_version(ks_version) < parse_version('0.7'):
+ raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version))
+
from .exif_le import ExifLe
from .exif_be import ExifBe
+
class Exif(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
self._io = _io
diff --git a/mitmproxy/contrib/kaitaistruct/exif_be.py b/mitmproxy/contrib/kaitaistruct/exif_be.py
index 7980a9e8..8a6e7a2b 100644
--- a/mitmproxy/contrib/kaitaistruct/exif_be.py
+++ b/mitmproxy/contrib/kaitaistruct/exif_be.py
@@ -1,14 +1,17 @@
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
-# The source was exif_be.ksy from here - https://github.com/kaitai-io/kaitai_struct_formats/blob/24e2d00048b8084ceec30a187a79cb87a79a48ba/image/exif_be.ksy
import array
import struct
import zlib
from enum import Enum
+from pkg_resources import parse_version
-from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
+from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO
+if parse_version(ks_version) < parse_version('0.7'):
+ raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version))
+
class ExifBe(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
self._io = _io
@@ -569,3 +572,5 @@ class ExifBe(KaitaiStruct):
self._m_ifd0 = self._root.Ifd(self._io, self, self._root)
self._io.seek(_pos)
return self._m_ifd0 if hasattr(self, '_m_ifd0') else None
+
+
diff --git a/mitmproxy/contrib/kaitaistruct/exif_le.py b/mitmproxy/contrib/kaitaistruct/exif_le.py
index 207b3beb..84e53a38 100644
--- a/mitmproxy/contrib/kaitaistruct/exif_le.py
+++ b/mitmproxy/contrib/kaitaistruct/exif_le.py
@@ -1,14 +1,17 @@
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
-# The source was exif_le.ksy from here - https://github.com/kaitai-io/kaitai_struct_formats/blob/24e2d00048b8084ceec30a187a79cb87a79a48ba/image/exif_le.ksy
import array
import struct
import zlib
from enum import Enum
+from pkg_resources import parse_version
-from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
+from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO
+if parse_version(ks_version) < parse_version('0.7'):
+ raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version))
+
class ExifLe(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
self._io = _io
@@ -569,3 +572,5 @@ class ExifLe(KaitaiStruct):
self._m_ifd0 = self._root.Ifd(self._io, self, self._root)
self._io.seek(_pos)
return self._m_ifd0 if hasattr(self, '_m_ifd0') else None
+
+
diff --git a/mitmproxy/contrib/kaitaistruct/gif.py b/mitmproxy/contrib/kaitaistruct/gif.py
index 61499cc7..820df568 100644
--- a/mitmproxy/contrib/kaitaistruct/gif.py
+++ b/mitmproxy/contrib/kaitaistruct/gif.py
@@ -1,14 +1,17 @@
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
-# The source was png.ksy from here - https://github.com/kaitai-io/kaitai_struct_formats/blob/562154250bea0081fed4e232751b934bc270a0c7/image/gif.ksy
import array
import struct
import zlib
from enum import Enum
+from pkg_resources import parse_version
-from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
+from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO
+if parse_version(ks_version) < parse_version('0.7'):
+ raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version))
+
class Gif(KaitaiStruct):
class BlockType(Enum):
@@ -24,8 +27,8 @@ class Gif(KaitaiStruct):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
- self.header = self._root.Header(self._io, self, self._root)
- self.logical_screen_descriptor = self._root.LogicalScreenDescriptor(self._io, self, self._root)
+ self.hdr = self._root.Header(self._io, self, self._root)
+ self.logical_screen_descriptor = self._root.LogicalScreenDescriptorStruct(self._io, self, self._root)
if self.logical_screen_descriptor.has_color_table:
self._raw_global_color_table = self._io.read_bytes((self.logical_screen_descriptor.color_table_size * 3))
io = KaitaiStream(BytesIO(self._raw_global_color_table))
@@ -55,7 +58,7 @@ class Gif(KaitaiStruct):
self.blue = self._io.read_u1()
- class LogicalScreenDescriptor(KaitaiStruct):
+ class LogicalScreenDescriptorStruct(KaitaiStruct):
def __init__(self, _io, _parent=None, _root=None):
self._io = _io
self._parent = _parent
@@ -163,7 +166,7 @@ class Gif(KaitaiStruct):
self._parent = _parent
self._root = _root if _root else self
self.magic = self._io.ensure_fixed_contents(struct.pack('3b', 71, 73, 70))
- self.version = self._io.read_bytes(3)
+ self.version = (self._io.read_bytes(3)).decode(u"ASCII")
class ExtGraphicControl(KaitaiStruct):
@@ -245,3 +248,6 @@ class Gif(KaitaiStruct):
self.body = self._root.ExtGraphicControl(self._io, self, self._root)
else:
self.body = self._root.Subblocks(self._io, self, self._root)
+
+
+
diff --git a/mitmproxy/contrib/kaitaistruct/jpeg.py b/mitmproxy/contrib/kaitaistruct/jpeg.py
index 08e382a9..33fc012f 100644
--- a/mitmproxy/contrib/kaitaistruct/jpeg.py
+++ b/mitmproxy/contrib/kaitaistruct/jpeg.py
@@ -1,15 +1,19 @@
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
-# The source was jpeg.ksy from here - https://github.com/kaitai-io/kaitai_struct_formats/blob/24e2d00048b8084ceec30a187a79cb87a79a48ba/image/jpeg.ksy
import array
import struct
import zlib
from enum import Enum
+from pkg_resources import parse_version
-from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
+from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO
+if parse_version(ks_version) < parse_version('0.7'):
+ raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version))
+
from .exif import Exif
+
class Jpeg(KaitaiStruct):
class ComponentId(Enum):
@@ -127,7 +131,7 @@ class Jpeg(KaitaiStruct):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
- self.magic = self._io.read_strz("ASCII", 0, False, True, True)
+ self.magic = (self._io.read_bytes_term(0, False, True, True)).decode(u"ASCII")
_on = self.magic
if _on == u"Exif":
self.body = self._root.ExifInJpeg(self._io, self, self._root)
@@ -195,7 +199,7 @@ class Jpeg(KaitaiStruct):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
- self.magic = self._io.read_str_byte_limit(5, "ASCII")
+ self.magic = (self._io.read_bytes(5)).decode(u"ASCII")
self.version_major = self._io.read_u1()
self.version_minor = self._io.read_u1()
self.density_units = self._root.SegmentApp0.DensityUnit(self._io.read_u1())
diff --git a/mitmproxy/contrib/kaitaistruct/make.sh b/mitmproxy/contrib/kaitaistruct/make.sh
new file mode 100755
index 00000000..218d5198
--- /dev/null
+++ b/mitmproxy/contrib/kaitaistruct/make.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/exif_be.ksy
+wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/exif_le.ksy
+wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/exif.ksy
+wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/gif.ksy
+wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/jpeg.ksy
+wget -N https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/image/png.ksy
+
+kaitai-struct-compiler --target python --opaque-types=true *.ksy
diff --git a/mitmproxy/contrib/kaitaistruct/png.py b/mitmproxy/contrib/kaitaistruct/png.py
index 2f3c1a5c..98a70693 100644
--- a/mitmproxy/contrib/kaitaistruct/png.py
+++ b/mitmproxy/contrib/kaitaistruct/png.py
@@ -1,14 +1,17 @@
# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
-# The source was png.ksy from here - https://github.com/kaitai-io/kaitai_struct_formats/blob/9370c720b7d2ad329102d89bdc880ba6a706ef26/image/png.ksy
import array
import struct
import zlib
from enum import Enum
+from pkg_resources import parse_version
-from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
+from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO
+if parse_version(ks_version) < parse_version('0.7'):
+ raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version))
+
class Png(KaitaiStruct):
class ColorType(Enum):
@@ -51,7 +54,7 @@ class Png(KaitaiStruct):
self._parent = _parent
self._root = _root if _root else self
self.len = self._io.read_u4be()
- self.type = self._io.read_str_byte_limit(4, "UTF-8")
+ self.type = (self._io.read_bytes(4)).decode(u"UTF-8")
_on = self.type
if _on == u"iTXt":
self._raw_body = self._io.read_bytes(self.len)
@@ -194,7 +197,7 @@ class Png(KaitaiStruct):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
- self.keyword = self._io.read_strz("UTF-8", 0, False, True, True)
+ self.keyword = (self._io.read_bytes_term(0, False, True, True)).decode(u"UTF-8")
self.compression_method = self._io.read_u1()
self._raw_text_datastream = self._io.read_bytes_full()
self.text_datastream = zlib.decompress(self._raw_text_datastream)
@@ -259,12 +262,12 @@ class Png(KaitaiStruct):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
- self.keyword = self._io.read_strz("UTF-8", 0, False, True, True)
+ self.keyword = (self._io.read_bytes_term(0, False, True, True)).decode(u"UTF-8")
self.compression_flag = self._io.read_u1()
self.compression_method = self._io.read_u1()
- self.language_tag = self._io.read_strz("ASCII", 0, False, True, True)
- self.translated_keyword = self._io.read_strz("UTF-8", 0, False, True, True)
- self.text = self._io.read_str_eos("UTF-8")
+ self.language_tag = (self._io.read_bytes_term(0, False, True, True)).decode(u"ASCII")
+ self.translated_keyword = (self._io.read_bytes_term(0, False, True, True)).decode(u"UTF-8")
+ self.text = (self._io.read_bytes_full()).decode(u"UTF-8")
class TextChunk(KaitaiStruct):
@@ -272,8 +275,8 @@ class Png(KaitaiStruct):
self._io = _io
self._parent = _parent
self._root = _root if _root else self
- self.keyword = self._io.read_strz("iso8859-1", 0, False, True, True)
- self.text = self._io.read_str_eos("iso8859-1")
+ self.keyword = (self._io.read_bytes_term(0, False, True, True)).decode(u"iso8859-1")
+ self.text = (self._io.read_bytes_full()).decode(u"iso8859-1")
class TimeChunk(KaitaiStruct):
@@ -287,3 +290,6 @@ class Png(KaitaiStruct):
self.hour = self._io.read_u1()
self.minute = self._io.read_u1()
self.second = self._io.read_u1()
+
+
+
diff --git a/mitmproxy/ctx.py b/mitmproxy/ctx.py
index 7b5231e6..954edcb1 100644
--- a/mitmproxy/ctx.py
+++ b/mitmproxy/ctx.py
@@ -1,4 +1,7 @@
import mitmproxy.master # noqa
import mitmproxy.log # noqa
+import mitmproxy.options # noqa
+
master = None # type: "mitmproxy.master.Master"
log = None # type: "mitmproxy.log.Log"
+options = None # type: "mitmproxy.options.Options"
diff --git a/mitmproxy/eventsequence.py b/mitmproxy/eventsequence.py
index bc6660e0..4e199972 100644
--- a/mitmproxy/eventsequence.py
+++ b/mitmproxy/eventsequence.py
@@ -32,9 +32,10 @@ Events = frozenset([
"configure",
"done",
"log",
- "start",
+ "load",
"running",
"tick",
+ "update",
])
diff --git a/mitmproxy/exceptions.py b/mitmproxy/exceptions.py
index 9b6328ac..71517480 100644
--- a/mitmproxy/exceptions.py
+++ b/mitmproxy/exceptions.py
@@ -93,11 +93,15 @@ class SetServerNotAllowedException(MitmproxyException):
pass
+class CommandError(Exception):
+ pass
+
+
class OptionsError(MitmproxyException):
pass
-class AddonError(MitmproxyException):
+class AddonManagerError(MitmproxyException):
pass
diff --git a/mitmproxy/export.py b/mitmproxy/export.py
deleted file mode 100644
index 235e754a..00000000
--- a/mitmproxy/export.py
+++ /dev/null
@@ -1,188 +0,0 @@
-import io
-import json
-import pprint
-import re
-import textwrap
-from typing import Any
-
-from mitmproxy import http
-
-
-def _native(s):
- if isinstance(s, bytes):
- return s.decode()
- return s
-
-
-def dictstr(items, indent: str) -> str:
- lines = []
- for k, v in items:
- lines.append(indent + "%s: %s,\n" % (repr(_native(k)), repr(_native(v))))
- return "{\n%s}\n" % "".join(lines)
-
-
-def curl_command(flow: http.HTTPFlow) -> str:
- data = "curl "
-
- request = flow.request.copy()
- request.decode(strict=False)
-
- for k, v in request.headers.items(multi=True):
- data += "-H '%s:%s' " % (k, v)
-
- if request.method != "GET":
- data += "-X %s " % request.method
-
- data += "'%s'" % request.url
-
- if request.content:
- data += " --data-binary '%s'" % _native(request.content)
-
- return data
-
-
-def python_arg(arg: str, val: Any) -> str:
- if not val:
- return ""
- if arg:
- arg += "="
- arg_str = "{}{},\n".format(
- arg,
- pprint.pformat(val, 79 - len(arg))
- )
- return textwrap.indent(arg_str, " " * 4)
-
-
-def python_code(flow: http.HTTPFlow):
- code = io.StringIO()
-
- def writearg(arg, val):
- code.write(python_arg(arg, val))
-
- code.write("import requests\n")
- code.write("\n")
- if flow.request.method.lower() in ("get", "post", "put", "head", "delete", "patch"):
- code.write("response = requests.{}(\n".format(flow.request.method.lower()))
- else:
- code.write("response = requests.request(\n")
- writearg("", flow.request.method)
- url_without_query = flow.request.url.split("?", 1)[0]
- writearg("", url_without_query)
-
- writearg("params", list(flow.request.query.fields))
-
- headers = flow.request.headers.copy()
- # requests adds those by default.
- for x in (":authority", "host", "content-length"):
- headers.pop(x, None)
- writearg("headers", dict(headers))
- try:
- if "json" not in flow.request.headers.get("content-type", ""):
- raise ValueError()
- writearg("json", json.loads(flow.request.text))
- except ValueError:
- writearg("data", flow.request.content)
-
- code.seek(code.tell() - 2) # remove last comma
- code.write("\n)\n")
- code.write("\n")
- code.write("print(response.text)")
-
- return code.getvalue()
-
-
-def locust_code(flow):
- code = textwrap.dedent("""
- from locust import HttpLocust, TaskSet, task
-
- class UserBehavior(TaskSet):
- def on_start(self):
- ''' on_start is called when a Locust start before any task is scheduled '''
- self.{name}()
-
- @task()
- def {name}(self):
- url = self.locust.host + '{path}'
- {headers}{params}{data}
- self.response = self.client.request(
- method='{method}',
- url=url,{args}
- )
-
- ### Additional tasks can go here ###
-
-
- class WebsiteUser(HttpLocust):
- task_set = UserBehavior
- min_wait = 1000
- max_wait = 3000
-""").strip()
-
- name = re.sub('\W|^(?=\d)', '_', flow.request.path.strip("/").split("?", 1)[0])
- if not name:
- new_name = "_".join([str(flow.request.host), str(flow.request.timestamp_start)])
- name = re.sub('\W|^(?=\d)', '_', new_name)
-
- path_without_query = flow.request.path.split("?")[0]
-
- args = ""
- headers = ""
- if flow.request.headers:
- lines = [
- (_native(k), _native(v)) for k, v in flow.request.headers.fields
- if _native(k).lower() not in [":authority", "host", "cookie"]
- ]
- lines = [" '%s': '%s',\n" % (k, v) for k, v in lines]
- headers += "\n headers = {\n%s }\n" % "".join(lines)
- args += "\n headers=headers,"
-
- params = ""
- if flow.request.query:
- lines = [
- " %s: %s,\n" % (repr(k), repr(v))
- for k, v in
- flow.request.query.collect()
- ]
- params = "\n params = {\n%s }\n" % "".join(lines)
- args += "\n params=params,"
-
- data = ""
- if flow.request.content:
- data = "\n data = '''%s'''\n" % _native(flow.request.content)
- args += "\n data=data,"
-
- code = code.format(
- name=name,
- path=path_without_query,
- headers=headers,
- params=params,
- data=data,
- method=flow.request.method,
- args=args,
- )
-
- return code
-
-
-def locust_task(flow):
- code = locust_code(flow)
- start_task = len(code.split('@task')[0]) - 4
- end_task = -19 - len(code.split('### Additional')[1])
- task_code = code[start_task:end_task]
-
- return task_code
-
-
-def url(flow):
- return flow.request.url
-
-
-EXPORTERS = [
- ("content", "c", None),
- ("headers+content", "h", None),
- ("url", "u", url),
- ("as curl command", "r", curl_command),
- ("as python code", "p", python_code),
- ("as locust code", "l", locust_code),
- ("as locust task", "t", locust_task),
-]
diff --git a/mitmproxy/flowfilter.py b/mitmproxy/flowfilter.py
index 2c7fc52f..83c98bad 100644
--- a/mitmproxy/flowfilter.py
+++ b/mitmproxy/flowfilter.py
@@ -44,7 +44,7 @@ from mitmproxy import flow
from mitmproxy.utils import strutils
import pyparsing as pp
-from typing import Callable
+from typing import Callable, Sequence, Type # noqa
def only(*types):
@@ -69,6 +69,8 @@ class _Token:
class _Action(_Token):
+ code = None # type: str
+ help = None # type: str
@classmethod
def make(klass, s, loc, toks):
@@ -162,15 +164,14 @@ def _check_content_type(rex, message):
class FAsset(_Action):
code = "a"
help = "Match asset in response: CSS, Javascript, Flash, images."
- ASSET_TYPES = [
+ ASSET_TYPES = [re.compile(x) for x in [
b"text/javascript",
b"application/x-javascript",
b"application/javascript",
b"text/css",
b"image/.*",
b"application/x-shockwave-flash"
- ]
- ASSET_TYPES = [re.compile(x) for x in ASSET_TYPES]
+ ]]
@only(http.HTTPFlow)
def __call__(self, f):
@@ -436,7 +437,7 @@ filter_unary = [
FResp,
FTCP,
FWebSocket,
-]
+] # type: Sequence[Type[_Action]]
filter_rex = [
FBod,
FBodRequest,
@@ -452,7 +453,7 @@ filter_rex = [
FMethod,
FSrc,
FUrl,
-]
+] # type: Sequence[Type[_Rex]]
filter_int = [
FCode
]
@@ -538,17 +539,17 @@ def match(flt, flow):
help = []
-for i in filter_unary:
+for a in filter_unary:
help.append(
- ("~%s" % i.code, i.help)
+ ("~%s" % a.code, a.help)
)
-for i in filter_rex:
+for b in filter_rex:
help.append(
- ("~%s regex" % i.code, i.help)
+ ("~%s regex" % b.code, b.help)
)
-for i in filter_int:
+for c in filter_int:
help.append(
- ("~%s int" % i.code, i.help)
+ ("~%s int" % c.code, c.help)
)
help.sort()
help.extend(
diff --git a/mitmproxy/io/__init__.py b/mitmproxy/io/__init__.py
new file mode 100644
index 00000000..540e6871
--- /dev/null
+++ b/mitmproxy/io/__init__.py
@@ -0,0 +1,7 @@
+
+from .io import FlowWriter, FlowReader, FilteredFlowWriter, read_flows_from_paths
+
+
+__all__ = [
+ "FlowWriter", "FlowReader", "FilteredFlowWriter", "read_flows_from_paths"
+]
diff --git a/mitmproxy/io_compat.py b/mitmproxy/io/compat.py
index 7d839ffd..99da496d 100644
--- a/mitmproxy/io_compat.py
+++ b/mitmproxy/io/compat.py
@@ -2,7 +2,7 @@
This module handles the import of mitmproxy flows generated by old versions.
"""
import uuid
-from typing import Any, Dict
+from typing import Any, Dict, Mapping, Union # noqa
from mitmproxy import version
from mitmproxy.utils import strutils
@@ -75,6 +75,8 @@ def convert_018_019(data):
data["client_conn"]["cipher_name"] = None
data["client_conn"]["tls_version"] = None
data["server_conn"]["alpn_proto_negotiated"] = None
+ if data["server_conn"]["via"]:
+ data["server_conn"]["via"]["alpn_proto_negotiated"] = None
data["mode"] = "regular"
data["metadata"] = dict()
data["version"] = (0, 19)
@@ -96,6 +98,13 @@ def convert_100_200(data):
data["server_conn"]["source_address"] = data["server_conn"]["source_address"]["address"]
if data["server_conn"]["ip_address"]:
data["server_conn"]["ip_address"] = data["server_conn"]["ip_address"]["address"]
+
+ if data["server_conn"]["via"]:
+ data["server_conn"]["via"]["address"] = data["server_conn"]["via"]["address"]["address"]
+ data["server_conn"]["via"]["source_address"] = data["server_conn"]["via"]["source_address"]["address"]
+ if data["server_conn"]["via"]["ip_address"]:
+ data["server_conn"]["via"]["ip_address"] = data["server_conn"]["via"]["ip_address"]["address"]
+
return data
@@ -113,8 +122,8 @@ def convert_300_4(data):
return data
-client_connections = {}
-server_connections = {}
+client_connections = {} # type: Mapping[str, str]
+server_connections = {} # type: Mapping[str, str]
def convert_4_5(data):
@@ -129,6 +138,14 @@ def convert_4_5(data):
)
data["client_conn"]["id"] = client_connections.setdefault(client_conn_key, str(uuid.uuid4()))
data["server_conn"]["id"] = server_connections.setdefault(server_conn_key, str(uuid.uuid4()))
+
+ if data["server_conn"]["via"]:
+ server_conn_key = (
+ data["server_conn"]["via"]["timestamp_start"],
+ *data["server_conn"]["via"]["source_address"]
+ )
+ data["server_conn"]["via"]["id"] = server_connections.setdefault(server_conn_key, str(uuid.uuid4()))
+
return data
@@ -142,7 +159,7 @@ def _convert_dict_keys(o: Any) -> Any:
def _convert_dict_vals(o: dict, values_to_convert: dict) -> dict:
for k, v in values_to_convert.items():
if not o or k not in o:
- continue
+ continue # pragma: no cover
if v is True:
o[k] = strutils.always_str(o[k])
else:
@@ -187,7 +204,7 @@ converters = {
}
-def migrate_flow(flow_data: Dict[str, Any]) -> Dict[str, Any]:
+def migrate_flow(flow_data: Dict[Union[bytes, str], Any]) -> Dict[Union[bytes, str], Any]:
while True:
flow_version = flow_data.get(b"version", flow_data.get("version"))
diff --git a/mitmproxy/io.py b/mitmproxy/io/io.py
index 780955a4..50e26f49 100644
--- a/mitmproxy/io.py
+++ b/mitmproxy/io/io.py
@@ -1,5 +1,5 @@
import os
-from typing import Iterable
+from typing import Type, Iterable, Dict, Union, Any, cast # noqa
from mitmproxy import exceptions
from mitmproxy import flow
@@ -7,15 +7,15 @@ from mitmproxy import flowfilter
from mitmproxy import http
from mitmproxy import tcp
from mitmproxy import websocket
-from mitmproxy.contrib import tnetstring
-from mitmproxy import io_compat
+from mitmproxy.io import compat
+from mitmproxy.io import tnetstring
FLOW_TYPES = dict(
http=http.HTTPFlow,
websocket=websocket.WebSocketFlow,
tcp=tcp.TCPFlow,
-)
+) # type: Dict[str, Type[flow.Flow]]
class FlowWriter:
@@ -37,14 +37,18 @@ class FlowReader:
"""
try:
while True:
- data = tnetstring.load(self.fo)
+ # FIXME: This cast hides a lack of dynamic type checking
+ loaded = cast(
+ Dict[Union[bytes, str], Any],
+ tnetstring.load(self.fo),
+ )
try:
- data = io_compat.migrate_flow(data)
+ mdata = compat.migrate_flow(loaded)
except ValueError as e:
raise exceptions.FlowReadException(str(e))
- if data["type"] not in FLOW_TYPES:
- raise exceptions.FlowReadException("Unknown flow type: {}".format(data["type"]))
- yield FLOW_TYPES[data["type"]].from_state(data)
+ if mdata["type"] not in FLOW_TYPES:
+ raise exceptions.FlowReadException("Unknown flow type: {}".format(mdata["type"]))
+ yield FLOW_TYPES[mdata["type"]].from_state(mdata)
except ValueError as e:
if str(e) == "not a tnetstring: empty file":
return # Error is due to EOF
diff --git a/mitmproxy/contrib/tnetstring.py b/mitmproxy/io/tnetstring.py
index 24ce6ce8..82c92f33 100644
--- a/mitmproxy/contrib/tnetstring.py
+++ b/mitmproxy/io/tnetstring.py
@@ -41,9 +41,9 @@ all other strings are returned as plain bytes.
"""
import collections
-from typing import io, Union, Tuple
+import typing
-TSerializable = Union[None, bool, int, float, bytes, list, tuple, dict]
+TSerializable = typing.Union[None, str, bool, int, float, bytes, list, tuple, dict]
def dumps(value: TSerializable) -> bytes:
@@ -53,12 +53,12 @@ def dumps(value: TSerializable) -> bytes:
# This uses a deque to collect output fragments in reverse order,
# then joins them together at the end. It's measurably faster
# than creating all the intermediate strings.
- q = collections.deque()
+ q = collections.deque() # type: collections.deque
_rdumpq(q, 0, value)
return b''.join(q)
-def dump(value: TSerializable, file_handle: io.BinaryIO) -> None:
+def dump(value: TSerializable, file_handle: typing.BinaryIO) -> None:
"""
This function dumps a python object as a tnetstring and
writes it to the given file.
@@ -156,7 +156,7 @@ def loads(string: bytes) -> TSerializable:
return pop(string)[0]
-def load(file_handle: io.BinaryIO) -> TSerializable:
+def load(file_handle: typing.BinaryIO) -> TSerializable:
"""load(file) -> object
This function reads a tnetstring from a file and parses it into a
@@ -213,19 +213,19 @@ def parse(data_type: int, data: bytes) -> TSerializable:
l = []
while data:
item, data = pop(data)
- l.append(item)
+ l.append(item) # type: ignore
return l
if data_type == ord(b'}'):
d = {}
while data:
key, data = pop(data)
val, data = pop(data)
- d[key] = val
+ d[key] = val # type: ignore
return d
raise ValueError("unknown type tag: {}".format(data_type))
-def pop(data: bytes) -> Tuple[TSerializable, bytes]:
+def pop(data: bytes) -> typing.Tuple[TSerializable, bytes]:
"""
This function parses a tnetstring into a python object.
It returns a tuple giving the parsed object and a string
@@ -233,8 +233,8 @@ def pop(data: bytes) -> Tuple[TSerializable, bytes]:
"""
# Parse out data length, type and remaining string.
try:
- length, data = data.split(b':', 1)
- length = int(length)
+ blength, data = data.split(b':', 1)
+ length = int(blength)
except ValueError:
raise ValueError("not a tnetstring: missing or invalid length prefix: {}".format(data))
try:
diff --git a/mitmproxy/log.py b/mitmproxy/log.py
index c2456cf1..a2e7ea99 100644
--- a/mitmproxy/log.py
+++ b/mitmproxy/log.py
@@ -24,6 +24,15 @@ class Log:
"""
self(txt, "info")
+ def alert(self, txt):
+ """
+ Log with level alert. Alerts have the same urgency as info, but
+ signals to interctive tools that the user's attention should be
+ drawn to the output even if they're not currently looking at the
+ event log.
+ """
+ self(txt, "alert")
+
def warn(self, txt):
"""
Log with level warn.
@@ -41,4 +50,4 @@ class Log:
def log_tier(level):
- return dict(error=0, warn=1, info=2, debug=3).get(level)
+ return dict(error=0, warn=1, info=2, alert=2, debug=3).get(level)
diff --git a/mitmproxy/master.py b/mitmproxy/master.py
index 946b25a4..d21a323e 100644
--- a/mitmproxy/master.py
+++ b/mitmproxy/master.py
@@ -7,7 +7,7 @@ from mitmproxy import options
from mitmproxy import controller
from mitmproxy import eventsequence
from mitmproxy import exceptions
-from mitmproxy import connections
+from mitmproxy import command
from mitmproxy import http
from mitmproxy import log
from mitmproxy.proxy.protocol import http_replay
@@ -34,6 +34,7 @@ class Master:
"""
def __init__(self, opts, server):
self.options = opts or options.Options()
+ self.commands = command.CommandManager(self)
self.addons = addonmanager.AddonManager(self)
self.event_queue = queue.Queue()
self.should_exit = threading.Event()
@@ -50,11 +51,13 @@ class Master:
return
mitmproxy_ctx.master = self
mitmproxy_ctx.log = log.Log(self)
+ mitmproxy_ctx.options = self.options
try:
yield
finally:
mitmproxy_ctx.master = None
mitmproxy_ctx.log = None
+ mitmproxy_ctx.options = None
def tell(self, mtype, m):
m.reply = controller.DummyReply()
@@ -74,9 +77,6 @@ class Master:
self.start()
try:
while not self.should_exit.is_set():
- # Don't choose a very small timeout in Python 2:
- # https://github.com/mitmproxy/mitmproxy/issues/443
- # TODO: Lower the timeout value if we move to Python 3.
self.tick(0.1)
finally:
self.shutdown()
@@ -103,23 +103,7 @@ class Master:
def shutdown(self):
self.server.shutdown()
self.should_exit.set()
- self.addons.done()
-
- def create_request(self, method, url):
- """
- Create a new artificial and minimalist request also adds it to flowlist.
-
- Raises:
- ValueError, if the url is malformed.
- """
- req = http.HTTPRequest.make(method, url)
- c = connections.ClientConnection.make_dummy(("", 0))
- s = connections.ServerConnection.make_dummy((req.host, req.port))
-
- f = http.HTTPFlow(c, s)
- f.request = req
- self.load_flow(f)
- return f
+ self.addons.trigger("done")
def load_flow(self, f):
"""
diff --git a/mitmproxy/net/http/cookies.py b/mitmproxy/net/http/cookies.py
index 01d42d46..5b410acc 100644
--- a/mitmproxy/net/http/cookies.py
+++ b/mitmproxy/net/http/cookies.py
@@ -1,7 +1,7 @@
-import collections
import email.utils
import re
import time
+from typing import Tuple, List, Iterable
from mitmproxy.types import multidict
@@ -23,10 +23,7 @@ cookies to be set in a single header. Serialization follows RFC6265.
http://tools.ietf.org/html/rfc2965
"""
-_cookie_params = set((
- 'expires', 'path', 'comment', 'max-age',
- 'secure', 'httponly', 'version',
-))
+_cookie_params = {'expires', 'path', 'comment', 'max-age', 'secure', 'httponly', 'version'}
ESCAPE = re.compile(r"([\"\\])")
@@ -43,7 +40,8 @@ class CookieAttrs(multidict.MultiDict):
return values[-1]
-SetCookie = collections.namedtuple("SetCookie", ["value", "attrs"])
+TSetCookie = Tuple[str, str, CookieAttrs]
+TPairs = List[List[str]] # TODO: Should be List[Tuple[str,str]]?
def _read_until(s, start, term):
@@ -131,15 +129,15 @@ def _read_cookie_pairs(s, off=0):
return pairs, off
-def _read_set_cookie_pairs(s, off=0):
+def _read_set_cookie_pairs(s: str, off=0) -> Tuple[List[TPairs], int]:
"""
Read pairs of lhs=rhs values from SetCookie headers while handling multiple cookies.
off: start offset
specials: attributes that are treated specially
"""
- cookies = []
- pairs = []
+ cookies = [] # type: List[TPairs]
+ pairs = [] # type: TPairs
while True:
lhs, off = _read_key(s, off, ";=,")
@@ -182,7 +180,7 @@ def _read_set_cookie_pairs(s, off=0):
return cookies, off
-def _has_special(s):
+def _has_special(s: str) -> bool:
for i in s:
if i in '",;\\':
return True
@@ -238,41 +236,44 @@ def format_cookie_header(lst):
return _format_pairs(lst)
-def parse_set_cookie_header(line):
+def parse_set_cookie_header(line: str) -> List[TSetCookie]:
"""
- Parse a Set-Cookie header value
+ Parse a Set-Cookie header value
- Returns a list of (name, value, attrs) tuples, where attrs is a
+ Returns:
+ A list of (name, value, attrs) tuples, where attrs is a
CookieAttrs dict of attributes. No attempt is made to parse attribute
values - they are treated purely as strings.
"""
cookie_pairs, off = _read_set_cookie_pairs(line)
- cookies = [
- (pairs[0][0], pairs[0][1], CookieAttrs(tuple(x) for x in pairs[1:]))
- for pairs in cookie_pairs if pairs
- ]
+ cookies = []
+ for pairs in cookie_pairs:
+ if pairs:
+ cookie, *attrs = pairs
+ cookies.append((
+ cookie[0],
+ cookie[1],
+ CookieAttrs(attrs)
+ ))
return cookies
-def parse_set_cookie_headers(headers):
+def parse_set_cookie_headers(headers: Iterable[str]) -> List[TSetCookie]:
rv = []
for header in headers:
cookies = parse_set_cookie_header(header)
- if cookies:
- for name, value, attrs in cookies:
- rv.append((name, SetCookie(value, attrs)))
+ rv.extend(cookies)
return rv
-def format_set_cookie_header(set_cookies):
+def format_set_cookie_header(set_cookies: List[TSetCookie]) -> str:
"""
Formats a Set-Cookie header value.
"""
rv = []
- for set_cookie in set_cookies:
- name, value, attrs = set_cookie
+ for name, value, attrs in set_cookies:
pairs = [(name, value)]
pairs.extend(
@@ -284,37 +285,36 @@ def format_set_cookie_header(set_cookies):
return ", ".join(rv)
-def refresh_set_cookie_header(c, delta):
+def refresh_set_cookie_header(c: str, delta: int) -> str:
"""
Args:
c: A Set-Cookie string
delta: Time delta in seconds
Returns:
A refreshed Set-Cookie string
+ Raises:
+ ValueError, if the cookie is invalid.
"""
-
- name, value, attrs = parse_set_cookie_header(c)[0]
- if not name or not value:
- raise ValueError("Invalid Cookie")
-
- if "expires" in attrs:
- e = email.utils.parsedate_tz(attrs["expires"])
- if e:
- f = email.utils.mktime_tz(e) + delta
- attrs.set_all("expires", [email.utils.formatdate(f)])
- else:
- # This can happen when the expires tag is invalid.
- # reddit.com sends a an expires tag like this: "Thu, 31 Dec
- # 2037 23:59:59 GMT", which is valid RFC 1123, but not
- # strictly correct according to the cookie spec. Browsers
- # appear to parse this tolerantly - maybe we should too.
- # For now, we just ignore this.
- del attrs["expires"]
-
- rv = format_set_cookie_header([(name, value, attrs)])
- if not rv:
- raise ValueError("Invalid Cookie")
- return rv
+ cookies = parse_set_cookie_header(c)
+ for cookie in cookies:
+ name, value, attrs = cookie
+ if not name or not value:
+ raise ValueError("Invalid Cookie")
+
+ if "expires" in attrs:
+ e = email.utils.parsedate_tz(attrs["expires"])
+ if e:
+ f = email.utils.mktime_tz(e) + delta
+ attrs.set_all("expires", [email.utils.formatdate(f)])
+ else:
+ # This can happen when the expires tag is invalid.
+ # reddit.com sends a an expires tag like this: "Thu, 31 Dec
+ # 2037 23:59:59 GMT", which is valid RFC 1123, but not
+ # strictly correct according to the cookie spec. Browsers
+ # appear to parse this tolerantly - maybe we should too.
+ # For now, we just ignore this.
+ del attrs["expires"]
+ return format_set_cookie_header(cookies)
def get_expiration_ts(cookie_attrs):
diff --git a/mitmproxy/net/http/http1/read.py b/mitmproxy/net/http/http1/read.py
index ef88fd6c..491135ac 100644
--- a/mitmproxy/net/http/http1/read.py
+++ b/mitmproxy/net/http/http1/read.py
@@ -271,7 +271,9 @@ def _parse_authority_form(hostport):
ValueError, if the input is malformed
"""
try:
- host, port = hostport.split(b":")
+ host, port = hostport.rsplit(b":", 1)
+ if host.startswith(b"[") and host.endswith(b"]"):
+ host = host[1:-1]
port = int(port)
if not check.is_valid_host(host) or not check.is_valid_port(port):
raise ValueError()
diff --git a/mitmproxy/net/http/message.py b/mitmproxy/net/http/message.py
index 1040c6ce..cb32aee4 100644
--- a/mitmproxy/net/http/message.py
+++ b/mitmproxy/net/http/message.py
@@ -226,8 +226,9 @@ class Message(serializable.Serializable):
Raises:
ValueError, when the content-encoding is invalid and strict is True.
"""
- self.raw_content = self.get_content(strict)
+ decoded = self.get_content(strict)
self.headers.pop("content-encoding", None)
+ self.content = decoded
def encode(self, e):
"""
diff --git a/mitmproxy/net/http/response.py b/mitmproxy/net/http/response.py
index 8edd43b8..18950fc7 100644
--- a/mitmproxy/net/http/response.py
+++ b/mitmproxy/net/http/response.py
@@ -131,7 +131,11 @@ class Response(message.Message):
def _get_cookies(self):
h = self.headers.get_all("set-cookie")
- return tuple(cookies.parse_set_cookie_headers(h))
+ all_cookies = cookies.parse_set_cookie_headers(h)
+ return tuple(
+ (name, (value, attrs))
+ for name, value, attrs in all_cookies
+ )
def _set_cookies(self, value):
cookie_headers = []
diff --git a/mitmproxy/net/http/url.py b/mitmproxy/net/http/url.py
index f2c8c473..86f65cfd 100644
--- a/mitmproxy/net/http/url.py
+++ b/mitmproxy/net/http/url.py
@@ -5,22 +5,6 @@ from typing import Tuple
from mitmproxy.net import check
-# PY2 workaround
-def decode_parse_result(result, enc):
- if hasattr(result, "decode"):
- return result.decode(enc)
- else:
- return urllib.parse.ParseResult(*[x.decode(enc) for x in result])
-
-
-# PY2 workaround
-def encode_parse_result(result, enc):
- if hasattr(result, "encode"):
- return result.encode(enc)
- else:
- return urllib.parse.ParseResult(*[x.encode(enc) for x in result])
-
-
def parse(url):
"""
URL-parsing function that checks that
@@ -47,12 +31,12 @@ def parse(url):
# this should not raise a ValueError,
# but we try to be very forgiving here and accept just everything.
- # decode_parse_result(parsed, "ascii")
else:
host = parsed.hostname.encode("idna")
- parsed = encode_parse_result(parsed, "ascii")
+ if isinstance(parsed, urllib.parse.ParseResult):
+ parsed = parsed.encode("ascii")
- port = parsed.port
+ port = parsed.port # Returns None if port number invalid in Py3.5. Will throw ValueError in Py3.6
if not port:
port = 443 if parsed.scheme == b"https" else 80
@@ -64,8 +48,6 @@ def parse(url):
if not check.is_valid_host(host):
raise ValueError("Invalid Host")
- if not check.is_valid_port(port):
- raise ValueError("Invalid Port")
return parsed.scheme, host, port, full_path
diff --git a/mitmproxy/net/tcp.py b/mitmproxy/net/tcp.py
index dc5e2ee2..81568d24 100644
--- a/mitmproxy/net/tcp.py
+++ b/mitmproxy/net/tcp.py
@@ -503,8 +503,6 @@ class _Connection:
if cipher_list:
try:
context.set_cipher_list(cipher_list)
-
- # TODO: maybe change this to with newer pyOpenSSL APIs
context.set_tmp_ecdh(OpenSSL.crypto.get_elliptic_curve('prime256v1'))
except SSL.Error as v:
raise exceptions.TlsException("SSL cipher specification error: %s" % str(v))
@@ -513,7 +511,7 @@ class _Connection:
if log_ssl_key:
context.set_info_callback(log_ssl_key)
- if HAS_ALPN:
+ if HAS_ALPN: # pragma: openssl-old no cover
if alpn_protos is not None:
# advertise application layer protocols
context.set_alpn_protos(alpn_protos)
@@ -522,13 +520,15 @@ class _Connection:
def alpn_select_callback(conn_, options):
if alpn_select in options:
return bytes(alpn_select)
- else: # pragma no cover
+ else: # pragma: no cover
return options[0]
context.set_alpn_select_callback(alpn_select_callback)
elif alpn_select_callback is not None and alpn_select is None:
+ if not callable(alpn_select_callback):
+ raise exceptions.TlsException("ALPN error: alpn_select_callback must be a function.")
context.set_alpn_select_callback(alpn_select_callback)
elif alpn_select_callback is not None and alpn_select is not None:
- raise exceptions.TlsException("ALPN error: only define alpn_select (string) OR alpn_select_callback (method).")
+ raise exceptions.TlsException("ALPN error: only define alpn_select (string) OR alpn_select_callback (function).")
return context
@@ -617,11 +617,6 @@ class TCPClient(_Connection):
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:
- raise self.ssl_verification_error
self.cert = certs.SSLCert(self.connection.get_peer_certificate())
@@ -676,11 +671,11 @@ class TCPClient(_Connection):
if self.spoof_source_address:
try:
if not sock.getsockopt(socket.SOL_IP, socket.IP_TRANSPARENT):
- sock.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1)
+ sock.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1) # pragma: windows no cover pragma: osx no cover
except Exception as e:
# socket.IP_TRANSPARENT might not be available on every OS and Python version
raise exceptions.TcpException(
- "Failed to spoof the source address: " + e.strerror
+ "Failed to spoof the source address: " + str(e)
)
sock.connect(sa)
return sock
@@ -693,7 +688,7 @@ class TCPClient(_Connection):
if err is not None:
raise err
else:
- raise socket.error("getaddrinfo returns an empty list")
+ raise socket.error("getaddrinfo returns an empty list") # pragma: no cover
def connect(self):
try:
@@ -716,7 +711,7 @@ class TCPClient(_Connection):
return self.connection.gettimeout()
def get_alpn_proto_negotiated(self):
- if HAS_ALPN and self.ssl_established:
+ if HAS_ALPN and self.ssl_established: # pragma: openssl-old no cover
return self.connection.get_alpn_proto_negotiated()
else:
return b""
@@ -823,7 +818,7 @@ class BaseHandler(_Connection):
self.connection.settimeout(n)
def get_alpn_proto_negotiated(self):
- if HAS_ALPN and self.ssl_established:
+ if HAS_ALPN and self.ssl_established: # pragma: openssl-old no cover
return self.connection.get_alpn_proto_negotiated()
else:
return b""
@@ -854,9 +849,10 @@ class TCPServer:
def __init__(self, address):
self.address = address
self.__is_shut_down = threading.Event()
+ self.__is_shut_down.set()
self.__shutdown_request = False
- if self.address == 'localhost':
+ if self.address[0] == 'localhost':
raise socket.error("Binding to 'localhost' is prohibited. Please use '::1' or '127.0.0.1' directly.")
try:
diff --git a/mitmproxy/options.py b/mitmproxy/options.py
index b87d5dbd..59d44a5d 100644
--- a/mitmproxy/options.py
+++ b/mitmproxy/options.py
@@ -1,6 +1,7 @@
from typing import Optional, Sequence
from mitmproxy import optmanager
+from mitmproxy import contentviews
from mitmproxy.net import tcp
# We redefine these here for now to avoid importing Urwid-related guff on
@@ -20,25 +21,22 @@ view_orders = [
"url",
"size",
]
+console_layouts = [
+ "single",
+ "vertical",
+ "horizontal",
+]
APP_HOST = "mitm.it"
APP_PORT = 80
CA_DIR = "~/.mitmproxy"
LISTEN_PORT = 8080
-# We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default.
-# https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=apache-2.2.15&openssl=1.0.2&hsts=yes&profile=old
-DEFAULT_CLIENT_CIPHERS = (
- "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:"
- "ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:"
- "ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:"
- "ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:"
- "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:"
- "DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:"
- "AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:"
- "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:"
- "!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"
-)
+# Some help text style guidelines:
+#
+# - Should be a single paragraph with no linebreaks. Help will be reflowed by
+# tools.
+# - Avoid adding information about the data type - we can generate that.
class Options(optmanager.OptManager):
@@ -51,8 +49,9 @@ class Options(optmanager.OptManager):
self.add_option(
"onboarding_host", str, APP_HOST,
"""
- Domain to serve the onboarding app from. For transparent mode, use
- an IP when a DNS entry for the app domain is not present. """
+ Onboarding app domain. For transparent mode, use an IP when a DNS
+ entry for the app domain is not present.
+ """
)
self.add_option(
"onboarding_port", int, APP_PORT,
@@ -80,8 +79,9 @@ class Options(optmanager.OptManager):
self.add_option(
"keepserving", bool, False,
"""
- Instructs mitmdump to continue serving after client playback,
- server playback or file read. This option is ignored by interactive tools, which always keep serving.
+ Continue serving after client playback, server playback or file
+ read. This option is ignored by interactive tools, which always keep
+ serving.
"""
)
self.add_option(
@@ -91,8 +91,8 @@ class Options(optmanager.OptManager):
self.add_option(
"server_replay_nopop", bool, False,
"""
- Disable response pop from response flow. This makes it possible to
- replay same response multiple times.
+ Don't remove flows from server replay state after use. This makes it
+ possible to replay same response multiple times.
"""
)
self.add_option(
@@ -160,11 +160,16 @@ class Options(optmanager.OptManager):
)
self.add_option(
"default_contentview", str, "auto",
- "The default content view mode."
+ "The default content view mode.",
+ choices = [i.name.lower() for i in contentviews.views]
+ )
+ self.add_option(
+ "save_stream_file", Optional[str], None,
+ "Stream flows to file as they arrive. Prefix path with + to append."
)
self.add_option(
- "streamfile", Optional[str], None,
- "Write flows to file. Prefix path with + to append."
+ "save_stream_filter", Optional[str], None,
+ "Filter which flows are written to file."
)
self.add_option(
"server_replay_ignore_content", bool, False,
@@ -174,7 +179,7 @@ class Options(optmanager.OptManager):
"server_replay_ignore_params", Sequence[str], [],
"""
Request's parameters to be ignored while searching for a saved flow
- to replay. Can be passed multiple times.
+ to replay.
"""
)
self.add_option(
@@ -197,11 +202,15 @@ class Options(optmanager.OptManager):
self.add_option(
"proxyauth", Optional[str], None,
"""
- Require authentication before proxying requests. If the value is
- "any", we prompt for authentication, but permit any values. If it
- starts with an "@", it is treated as a path to an Apache htpasswd
- file. If its is of the form "username:password", it is treated as a
- single-user credential.
+ Require proxy authentication. Value may be "any" to require
+ authenticaiton but accept any credentials, start with "@" to specify
+ a path to an Apache htpasswd file, be of the form
+ "username:password", or be of the form
+ "ldap[s]:url_server_ldap:dn_auth:password:dn_subtree",
+ the dn_auth & password is the dn/pass used to authenticate
+ the dn subtree is the subtree that we will search to find the username
+ an example would be
+ "ldap:localhost:cn=default,dc=example,dc=com:password:ou=application,dc=example,dc=com".
"""
)
self.add_option(
@@ -225,17 +234,16 @@ class Options(optmanager.OptManager):
self.add_option(
"certs", Sequence[str], [],
"""
- SSL certificates. SPEC is of the form "[domain=]path". The
- domain may include a wildcard, and is equal to "*" if not specified.
- The file at path is a certificate in PEM format. If a private key is
- included in the PEM, it is used, else the default key in the conf
- dir is used. The PEM file should contain the full certificate chain,
- with the leaf certificate as the first entry. Can be passed multiple
- times.
+ SSL certificates of the form "[domain=]path". The domain may include
+ a wildcard, and is equal to "*" if not specified. The file at path
+ is a certificate in PEM format. If a private key is included in the
+ PEM, it is used, else the default key in the conf dir is used. The
+ PEM file should contain the full certificate chain, with the leaf
+ certificate as the first entry.
"""
)
self.add_option(
- "ciphers_client", str, DEFAULT_CLIENT_CIPHERS,
+ "ciphers_client", Optional[str], None,
"Set supported ciphers for client connections using OpenSSL syntax."
)
self.add_option(
@@ -373,8 +381,9 @@ class Options(optmanager.OptManager):
# Console options
self.add_option(
- "console_eventlog", bool, False,
- "Show event log."
+ "console_layout", str, "single",
+ "Console layout.",
+ choices=sorted(console_layouts),
)
self.add_option(
"console_focus_follow", bool, False,
@@ -394,7 +403,7 @@ class Options(optmanager.OptManager):
"Console mouse interaction."
)
self.add_option(
- "console_order", Optional[str], None,
+ "console_order", str, "time",
"Flow sort order.",
choices=view_orders,
)
@@ -404,8 +413,8 @@ class Options(optmanager.OptManager):
)
self.add_option(
- "filter", Optional[str], None,
- "Filter view expression."
+ "view_filter", Optional[str], None,
+ "Limit which flows are displayed."
)
# Web options
@@ -428,10 +437,6 @@ class Options(optmanager.OptManager):
# Dump options
self.add_option(
- "filtstr", Optional[str], None,
- "The filter string for mitmdump."
- )
- self.add_option(
"flow_detail", int, 1,
"""
The display detail level for flows in mitmdump: 0 (almost quiet) to 3 (very verbose).
diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py
index 495354f4..70f60bb6 100644
--- a/mitmproxy/optmanager.py
+++ b/mitmproxy/optmanager.py
@@ -1,9 +1,9 @@
import contextlib
import blinker
+import blinker._saferef
import pprint
import copy
import functools
-import weakref
import os
import typing
import textwrap
@@ -31,12 +31,12 @@ class _Option:
help: str,
choices: typing.Optional[typing.Sequence[str]]
) -> None:
- typecheck.check_type(name, default, typespec)
+ typecheck.check_option_type(name, default, typespec)
self.name = name
self.typespec = typespec
self._default = default
self.value = unset
- self.help = help
+ self.help = textwrap.dedent(help).strip().replace("\n", " ")
self.choices = choices
def __repr__(self):
@@ -54,14 +54,14 @@ class _Option:
return copy.deepcopy(v)
def set(self, value: typing.Any) -> None:
- typecheck.check_type(self.name, value, self.typespec)
+ typecheck.check_option_type(self.name, value, self.typespec)
self.value = value
def reset(self) -> None:
self.value = unset
def has_changed(self) -> bool:
- return self.value is not unset
+ return self.current() != self.default
def __eq__(self, other) -> bool:
for i in self.__slots__:
@@ -104,8 +104,6 @@ class OptManager:
help: str,
choices: typing.Optional[typing.Sequence[str]] = None
) -> None:
- if name in self._options:
- raise ValueError("Option %s already exists" % name)
self._options[name] = _Option(name, typespec, default, help, choices)
@contextlib.contextmanager
@@ -127,15 +125,24 @@ class OptManager:
Subscribe a callable to the .changed signal, but only for a
specified list of options. The callable should accept arguments
(options, updated), and may raise an OptionsError.
+
+ The event will automatically be unsubscribed if the callable goes out of scope.
"""
- func = weakref.proxy(func)
+ for i in opts:
+ if i not in self._options:
+ raise exceptions.OptionsError("No such option: %s" % i)
+
+ # We reuse blinker's safe reference functionality to cope with weakrefs
+ # to bound methods.
+ func = blinker._saferef.safe_ref(func)
@functools.wraps(func)
def _call(options, updated):
if updated.intersection(set(opts)):
- try:
- func(options, updated)
- except ReferenceError:
+ f = func()
+ if f:
+ f(options, updated)
+ else:
self.changed.disconnect(_call)
# Our wrapper function goes out of scope immediately, so we have to set
@@ -172,7 +179,7 @@ class OptManager:
"""
for o in self._options.values():
o.reset()
- self.changed.send(self._options.keys())
+ self.changed.send(self, updated=set(self._options.keys()))
def update_known(self, **kwargs):
"""
@@ -187,7 +194,7 @@ class OptManager:
unknown[k] = v
updated = set(known.keys())
if updated:
- with self.rollback(updated):
+ with self.rollback(updated, reraise=True):
for k, v in known.items():
self._options[k].set(v)
self.changed.send(self, updated=updated)
@@ -265,44 +272,52 @@ class OptManager:
vals.update(self._setspec(i))
self.update(**vals)
- def _setspec(self, spec):
- d = {}
-
- parts = spec.split("=", maxsplit=1)
- if len(parts) == 1:
- optname, optval = parts[0], None
- else:
- optname, optval = parts[0], parts[1]
+ def parse_setval(self, optname: str, optstr: typing.Optional[str]) -> typing.Any:
+ """
+ Convert a string to a value appropriate for the option type.
+ """
if optname not in self._options:
raise exceptions.OptionsError("No such option %s" % optname)
o = self._options[optname]
if o.typespec in (str, typing.Optional[str]):
- d[optname] = optval
+ return optstr
elif o.typespec in (int, typing.Optional[int]):
- if optval:
+ if optstr:
try:
- optval = int(optval)
+ return int(optstr)
except ValueError:
- raise exceptions.OptionsError("Not an integer: %s" % optval)
- d[optname] = optval
+ raise exceptions.OptionsError("Not an integer: %s" % optstr)
+ elif o.typespec == int:
+ raise exceptions.OptionsError("Option is required: %s" % optname)
+ else:
+ return None
elif o.typespec == bool:
- if not optval or optval == "true":
- v = True
- elif optval == "false":
- v = False
+ if optstr == "toggle":
+ return not o.current()
+ if not optstr or optstr == "true":
+ return True
+ elif optstr == "false":
+ return False
else:
raise exceptions.OptionsError(
"Boolean must be \"true\", \"false\", or have the value " "omitted (a synonym for \"true\")."
)
- d[optname] = v
elif o.typespec == typing.Sequence[str]:
- if not optval:
- d[optname] = []
+ if not optstr:
+ return []
else:
- d[optname] = getattr(self, optname) + [optval]
- else: # pragma: no cover
- raise NotImplementedError("Unsupported option type: %s", o.typespec)
+ return getattr(self, optname) + [optstr]
+ raise NotImplementedError("Unsupported option type: %s", o.typespec)
+
+ def _setspec(self, spec):
+ d = {}
+ parts = spec.split("=", maxsplit=1)
+ if len(parts) == 1:
+ optname, optval = parts[0], None
+ else:
+ optname, optval = parts[0], parts[1]
+ d[optname] = self.parse_setval(optname, optval)
return d
def make_parser(self, parser, optname, metavar=None, short=None):
@@ -396,11 +411,7 @@ def dump_defaults(opts):
raise NotImplementedError
txt += " Type %s." % t
- txt = "\n".join(
- textwrap.wrap(
- textwrap.dedent(txt)
- )
- )
+ txt = "\n".join(textwrap.wrap(txt))
s.yaml_set_comment_before_after_key(k, before = "\n" + txt)
return ruamel.yaml.round_trip_dump(s)
@@ -411,11 +422,14 @@ def parse(text):
try:
data = ruamel.yaml.load(text, ruamel.yaml.RoundTripLoader)
except ruamel.yaml.error.YAMLError as v:
- snip = v.problem_mark.get_snippet()
- raise exceptions.OptionsError(
- "Config error at line %s:\n%s\n%s" %
- (v.problem_mark.line + 1, snip, v.problem)
- )
+ if hasattr(v, "problem_mark"):
+ snip = v.problem_mark.get_snippet()
+ raise exceptions.OptionsError(
+ "Config error at line %s:\n%s\n%s" %
+ (v.problem_mark.line + 1, snip, v.problem)
+ )
+ else:
+ raise exceptions.OptionsError("Could not parse options.")
if isinstance(data, str):
raise exceptions.OptionsError("Config error - no keys found.")
return data
@@ -444,8 +458,13 @@ def load_paths(opts, *paths):
for p in paths:
p = os.path.expanduser(p)
if os.path.exists(p) and os.path.isfile(p):
- with open(p, "r") as f:
- txt = f.read()
+ with open(p, "rt", encoding="utf8") as f:
+ try:
+ txt = f.read()
+ except UnicodeDecodeError as e:
+ raise exceptions.OptionsError(
+ "Error reading %s: %s" % (p, e)
+ )
try:
ret.update(load(opts, txt))
except exceptions.OptionsError as e:
@@ -479,12 +498,19 @@ def serialize(opts, text, defaults=False):
def save(opts, path, defaults=False):
"""
Save to path. If the destination file exists, modify it in-place.
+
+ Raises OptionsError if the existing data is corrupt.
"""
if os.path.exists(path) and os.path.isfile(path):
- with open(path, "r") as f:
- data = f.read()
+ with open(path, "rt", encoding="utf8") as f:
+ try:
+ data = f.read()
+ except UnicodeDecodeError as e:
+ raise exceptions.OptionsError(
+ "Error trying to modify %s: %s" % (path, e)
+ )
else:
data = ""
data = serialize(opts, data, defaults)
- with open(path, "w") as f:
+ with open(path, "wt", encoding="utf8") as f:
f.write(data)
diff --git a/mitmproxy/proxy/protocol/http.py b/mitmproxy/proxy/protocol/http.py
index d9e53fed..45870830 100644
--- a/mitmproxy/proxy/protocol/http.py
+++ b/mitmproxy/proxy/protocol/http.py
@@ -143,9 +143,11 @@ def validate_request_form(mode, request):
if request.first_line_format not in allowed_request_forms:
if mode == HTTPMode.transparent:
err_message = (
- "Mitmproxy received an {} request even though it is not running in regular mode. "
- "This usually indicates a misconfiguration, please see "
- "http://docs.mitmproxy.org/en/stable/modes.html for details."
+ """
+ Mitmproxy received an {} request even though it is not running
+ in regular mode. This usually indicates a misconfiguration,
+ please see the mitmproxy mode documentation for details.
+ """
).format("HTTP CONNECT" if request.first_line_format == "authority" else "absolute-form")
else:
err_message = "Invalid HTTP request form (expected: %s, got: %s)" % (
@@ -260,7 +262,10 @@ class HttpLayer(base.Layer):
self.send_error_response(400, msg)
raise exceptions.ProtocolException(msg)
+ validate_request_form(self.mode, request)
self.channel.ask("requestheaders", f)
+ # Re-validate request form in case the user has changed something.
+ validate_request_form(self.mode, request)
if request.headers.get("expect", "").lower() == "100-continue":
# TODO: We may have to use send_response_headers for HTTP2
@@ -270,12 +275,12 @@ class HttpLayer(base.Layer):
request.data.content = b"".join(self.read_request_body(request))
request.timestamp_end = time.time()
-
- validate_request_form(self.mode, request)
except exceptions.HttpException as e:
# We optimistically guess there might be an HTTP client on the
# other end
self.send_error_response(400, repr(e))
+ # Request may be malformed at this point, so we unset it.
+ f.request = None
f.error = flow.Error(str(e))
self.channel.ask("error", f)
raise exceptions.ProtocolException(
diff --git a/mitmproxy/proxy/protocol/http2.py b/mitmproxy/proxy/protocol/http2.py
index a6e8a4dd..2191b54b 100644
--- a/mitmproxy/proxy/protocol/http2.py
+++ b/mitmproxy/proxy/protocol/http2.py
@@ -62,7 +62,7 @@ class SafeH2Connection(connection.H2Connection):
raise_zombie(self.lock.release)
max_outbound_frame_size = self.max_outbound_frame_size
frame_chunk = chunk[position:position + max_outbound_frame_size]
- if self.local_flow_control_window(stream_id) < len(frame_chunk):
+ if self.local_flow_control_window(stream_id) < len(frame_chunk): # pragma: no cover
self.lock.release()
time.sleep(0.1)
continue
@@ -188,7 +188,7 @@ class Http2Layer(base.Layer):
self.streams[eid].kill()
self.connections[source_conn].safe_reset_stream(
event.stream_id,
- h2.errors.REFUSED_STREAM
+ h2.errors.ErrorCodes.REFUSED_STREAM
)
self.log("HTTP body too large. Limit is {}.".format(bsl), "info")
else:
@@ -207,7 +207,7 @@ class Http2Layer(base.Layer):
def _handle_stream_reset(self, eid, event, is_server, other_conn):
self.streams[eid].kill()
- if eid in self.streams and event.error_code == h2.errors.CANCEL:
+ if eid in self.streams and event.error_code == h2.errors.ErrorCodes.CANCEL:
if is_server:
other_stream_id = self.streams[eid].client_stream_id
else:
@@ -228,7 +228,7 @@ class Http2Layer(base.Layer):
event.last_stream_id,
event.additional_data), "info")
- if event.error_code != h2.errors.NO_ERROR:
+ if event.error_code != h2.errors.ErrorCodes.NO_ERROR:
# Something terrible has happened - kill everything!
self.connections[self.client_conn].close_connection(
error_code=event.error_code,
@@ -362,7 +362,7 @@ class Http2Layer(base.Layer):
self._kill_all_streams()
-def detect_zombie_stream(func):
+def detect_zombie_stream(func): # pragma: no cover
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
self.raise_zombie()
@@ -454,7 +454,7 @@ class Http2SingleStreamLayer(httpbase._HttpTransmissionLayer, basethread.BaseThr
else:
return self.request_data_finished
- def raise_zombie(self, pre_command=None):
+ def raise_zombie(self, pre_command=None): # pragma: no cover
connection_closed = self.h2_connection.state_machine.state == h2.connection.ConnectionState.CLOSED
if self.zombie is not None or connection_closed:
if pre_command is not None:
@@ -626,7 +626,7 @@ class Http2SingleStreamLayer(httpbase._HttpTransmissionLayer, basethread.BaseThr
self.log(repr(e), "info")
except exceptions.SetServerNotAllowedException as e: # pragma: no cover
self.log("Changing the Host server for HTTP/2 connections not allowed: {}".format(e), "info")
- except exceptions.Kill:
+ except exceptions.Kill: # pragma: no cover
self.log("Connection killed", "info")
self.kill()
diff --git a/mitmproxy/proxy/protocol/tls.py b/mitmproxy/proxy/protocol/tls.py
index acc0c6e3..f55855f0 100644
--- a/mitmproxy/proxy/protocol/tls.py
+++ b/mitmproxy/proxy/protocol/tls.py
@@ -200,6 +200,21 @@ CIPHER_ID_NAME_MAP = {
}
+# We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default.
+# https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=apache-2.2.15&openssl=1.0.2&hsts=yes&profile=old
+DEFAULT_CLIENT_CIPHERS = (
+ "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:"
+ "ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:"
+ "ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:"
+ "ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:"
+ "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:"
+ "DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:"
+ "AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:"
+ "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:"
+ "!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"
+)
+
+
def is_tls_record_magic(d):
"""
Returns:
@@ -475,7 +490,7 @@ class TlsLayer(base.Layer):
cert, key,
method=self.config.openssl_method_client,
options=self.config.openssl_options_client,
- cipher_list=self.config.options.ciphers_client,
+ cipher_list=self.config.options.ciphers_client or DEFAULT_CLIENT_CIPHERS,
dhparams=self.config.certstore.dhparams,
chain_file=chain_file,
alpn_select_callback=self.__alpn_select_callback,
diff --git a/mitmproxy/proxy/server.py b/mitmproxy/proxy/server.py
index 9f783bc3..50a2b76b 100644
--- a/mitmproxy/proxy/server.py
+++ b/mitmproxy/proxy/server.py
@@ -12,6 +12,7 @@ from mitmproxy.proxy import modes
from mitmproxy.proxy import root_context
from mitmproxy.net import tcp
from mitmproxy.net.http import http1
+from mitmproxy.utils import human
class DummyServer:
@@ -152,5 +153,5 @@ class ConnectionHandler:
self.client_conn.finish()
def log(self, msg, level):
- msg = "{}: {}".format(repr(self.client_conn.address), msg)
+ msg = "{}: {}".format(human.format_address(self.client_conn.address), msg)
self.channel.tell("log", log.LogEntry(msg, level))
diff --git a/mitmproxy/script/concurrent.py b/mitmproxy/script/concurrent.py
index 7573f2a5..cbb3beb0 100644
--- a/mitmproxy/script/concurrent.py
+++ b/mitmproxy/script/concurrent.py
@@ -12,7 +12,7 @@ class ScriptThread(basethread.BaseThread):
def concurrent(fn):
- if fn.__name__ not in eventsequence.Events - {"start", "configure", "tick"}:
+ if fn.__name__ not in eventsequence.Events - {"load", "configure", "tick"}:
raise NotImplementedError(
"Concurrent decorator not supported for '%s' method." % fn.__name__
)
diff --git a/mitmproxy/test/taddons.py b/mitmproxy/test/taddons.py
index 8bc174c7..5680e847 100644
--- a/mitmproxy/test/taddons.py
+++ b/mitmproxy/test/taddons.py
@@ -1,3 +1,4 @@
+import sys
import contextlib
import mitmproxy.master
@@ -5,6 +6,8 @@ import mitmproxy.options
from mitmproxy import proxy
from mitmproxy import addonmanager
from mitmproxy import eventsequence
+from mitmproxy import command
+from mitmproxy.addons import script
class TestAddons(addonmanager.AddonManager):
@@ -26,6 +29,10 @@ class RecordingMaster(mitmproxy.master.Master):
self.events = []
self.logs = []
+ def dump_log(self, outf=sys.stdout):
+ for i in self.logs:
+ print("%s: %s" % (i.level, i.msg), file=outf)
+
def has_log(self, txt, level=None):
for i in self.logs:
if level and i.level != level:
@@ -51,14 +58,21 @@ class context:
provides a number of helper methods for common testing scenarios.
"""
def __init__(self, master = None, options = None):
- self.options = options or mitmproxy.options.Options()
+ options = options or mitmproxy.options.Options()
self.master = master or RecordingMaster(
options, proxy.DummyServer(options)
)
+ self.options = self.master.options
self.wrapped = None
+ def ctx(self):
+ """
+ Returns a new handler context.
+ """
+ return self.master.handlecontext()
+
def __enter__(self):
- self.wrapped = self.master.handlecontext()
+ self.wrapped = self.ctx()
self.wrapped.__enter__()
return self
@@ -75,11 +89,13 @@ class context:
"""
f.reply._state = "start"
for evt, arg in eventsequence.iterate(f):
- h = getattr(addon, evt, None)
- if h:
- h(arg)
- if f.reply.state == "taken":
- return
+ self.master.addons.invoke_addon(
+ addon,
+ evt,
+ arg
+ )
+ if f.reply.state == "taken":
+ return
def configure(self, addon, **kwargs):
"""
@@ -89,4 +105,32 @@ class context:
"""
with self.options.rollback(kwargs.keys(), reraise=True):
self.options.update(**kwargs)
- addon.configure(self.options, kwargs.keys())
+ self.master.addons.invoke_addon(
+ addon,
+ "configure",
+ kwargs.keys()
+ )
+
+ def script(self, path):
+ """
+ Loads a script from path, and returns the enclosed addon.
+ """
+ sc = script.Script(path)
+ loader = addonmanager.Loader(self.master)
+ self.master.addons.invoke_addon(sc, "load", loader)
+ self.configure(sc)
+ self.master.addons.invoke_addon(sc, "tick")
+ return sc.addons[0] if sc.addons else None
+
+ def invoke(self, addon, event, *args, **kwargs):
+ """
+ Recursively invoke an event on an addon and all its children.
+ """
+ return self.master.addons.invoke_addon(addon, event, *args, **kwargs)
+
+ def command(self, func, *args):
+ """
+ Invoke a command function with a list of string arguments within a command context, mimicing the actual command environment.
+ """
+ cmd = command.Command(self.master.commands, "test.command", func)
+ return cmd.call(args)
diff --git a/mitmproxy/test/tflow.py b/mitmproxy/test/tflow.py
index 270021cb..9004df2f 100644
--- a/mitmproxy/test/tflow.py
+++ b/mitmproxy/test/tflow.py
@@ -174,7 +174,7 @@ def tserver_conn():
id=str(uuid.uuid4()),
address=("address", 22),
source_address=("address", 22),
- ip_address=None,
+ ip_address=("192.168.0.1", 22),
cert=None,
timestamp_start=1,
timestamp_tcp_setup=2,
@@ -183,7 +183,7 @@ def tserver_conn():
ssl_established=False,
sni="address",
alpn_proto_negotiated=None,
- tls_version=None,
+ tls_version="TLSv1.2",
via=None,
))
c.reply = controller.DummyReply()
diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py
index da091c12..5711ce73 100644
--- a/mitmproxy/tools/cmdline.py
+++ b/mitmproxy/tools/cmdline.py
@@ -26,6 +26,11 @@ def common_options(parser, opts):
help="Show all options and their default values",
)
parser.add_argument(
+ '--commands',
+ action='store_true',
+ help="Show all commands and their signatures",
+ )
+ parser.add_argument(
"--conf",
type=str, dest="conf", default=CONFIG_PATH,
metavar="PATH",
@@ -39,7 +44,7 @@ def common_options(parser, opts):
help="""
Set an option. When the value is omitted, booleans are set to true,
strings and integers are set to None (if permitted), and sequences
- are emptied.
+ are emptied. Boolean values can be true, false or toggle.
"""
)
parser.add_argument(
@@ -61,7 +66,7 @@ def common_options(parser, opts):
opts.make_parser(parser, "scripts", metavar="SCRIPT", short="s")
opts.make_parser(parser, "stickycookie", metavar="FILTER")
opts.make_parser(parser, "stickyauth", metavar="FILTER")
- opts.make_parser(parser, "streamfile", metavar="PATH", short="w")
+ opts.make_parser(parser, "save_stream_file", metavar="PATH", short="w")
opts.make_parser(parser, "anticomp")
# Proxy options
@@ -103,13 +108,13 @@ def mitmproxy(opts):
parser = argparse.ArgumentParser(usage="%(prog)s [options]")
common_options(parser, opts)
- opts.make_parser(parser, "console_eventlog")
+ opts.make_parser(parser, "console_layout")
group = parser.add_argument_group(
"Filters",
"See help in mitmproxy for filter expression syntax."
)
opts.make_parser(group, "intercept", metavar="FILTER")
- opts.make_parser(group, "filter", metavar="FILTER")
+ opts.make_parser(group, "view_filter", metavar="FILTER")
return parser
@@ -122,8 +127,8 @@ def mitmdump(opts):
'filter_args',
nargs="...",
help="""
- Filter view expression, used to only show flows that match a certain
- filter. See help in mitmproxy for filter expression syntax.
+ Filter expression, equivalent to setting both the view_filter
+ and save_stream_filter options.
"""
)
return parser
diff --git a/mitmproxy/tools/console/commandeditor.py b/mitmproxy/tools/console/commandeditor.py
new file mode 100644
index 00000000..17d1506b
--- /dev/null
+++ b/mitmproxy/tools/console/commandeditor.py
@@ -0,0 +1,38 @@
+import typing
+import urwid
+
+from mitmproxy import exceptions
+from mitmproxy import flow
+from mitmproxy.tools.console import signals
+
+
+class CommandEdit(urwid.Edit):
+ def __init__(self, partial):
+ urwid.Edit.__init__(self, ":", partial)
+
+ def keypress(self, size, key):
+ return urwid.Edit.keypress(self, size, key)
+
+
+class CommandExecutor:
+ def __init__(self, master):
+ self.master = master
+
+ def __call__(self, cmd):
+ if cmd.strip():
+ try:
+ ret = self.master.commands.call(cmd)
+ except exceptions.CommandError as v:
+ signals.status_message.send(message=str(v))
+ else:
+ if ret:
+ if type(ret) == typing.Sequence[flow.Flow]:
+ signals.status_message.send(
+ message="Command returned %s flows" % len(ret)
+ )
+ elif len(str(ret)) < 50:
+ signals.status_message.send(message=str(ret))
+ else:
+ signals.status_message.send(
+ message="Command returned too much data to display."
+ )
diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py
new file mode 100644
index 00000000..76827a99
--- /dev/null
+++ b/mitmproxy/tools/console/commands.py
@@ -0,0 +1,182 @@
+import urwid
+import blinker
+import textwrap
+from mitmproxy.tools.console import common
+from mitmproxy.tools.console import signals
+
+HELP_HEIGHT = 5
+
+
+footer = [
+ ('heading_key', "enter"), ":edit ",
+ ('heading_key', "?"), ":help ",
+]
+
+
+def _mkhelp():
+ text = []
+ keys = [
+ ("enter", "execute command"),
+ ]
+ text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
+ return text
+
+
+help_context = _mkhelp()
+
+
+def fcol(s, width, attr):
+ s = str(s)
+ return (
+ "fixed",
+ width,
+ urwid.Text((attr, s))
+ )
+
+
+command_focus_change = blinker.Signal()
+
+
+class CommandItem(urwid.WidgetWrap):
+ def __init__(self, walker, cmd, focused):
+ self.walker, self.cmd, self.focused = walker, cmd, focused
+ super().__init__(None)
+ self._w = self.get_widget()
+
+ def get_widget(self):
+ parts = [
+ ("focus", ">> " if self.focused else " "),
+ ("title", self.cmd.path),
+ ("text", " "),
+ ("text", " ".join(self.cmd.paramnames())),
+ ]
+ if self.cmd.returntype:
+ parts.append([
+ ("title", " -> "),
+ ("text", self.cmd.retname()),
+ ])
+
+ return urwid.AttrMap(
+ urwid.Padding(urwid.Text(parts)),
+ "text"
+ )
+
+ def get_edit_text(self):
+ return self._w[1].get_edit_text()
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ return key
+
+
+class CommandListWalker(urwid.ListWalker):
+ def __init__(self, master):
+ self.master = master
+
+ self.index = 0
+ self.focusobj = None
+ self.cmds = list(master.commands.commands.values())
+ self.cmds.sort(key=lambda x: x.signature_help())
+ self.set_focus(0)
+
+ def get_edit_text(self):
+ return self.focus_obj.get_edit_text()
+
+ def _get(self, pos):
+ cmd = self.cmds[pos]
+ return CommandItem(self, cmd, pos == self.index)
+
+ def get_focus(self):
+ return self.focus_obj, self.index
+
+ def set_focus(self, index):
+ cmd = self.cmds[index]
+ self.index = index
+ self.focus_obj = self._get(self.index)
+ command_focus_change.send(cmd.help or "")
+
+ def get_next(self, pos):
+ if pos >= len(self.cmds) - 1:
+ return None, None
+ pos = pos + 1
+ return self._get(pos), pos
+
+ def get_prev(self, pos):
+ pos = pos - 1
+ if pos < 0:
+ return None, None
+ return self._get(pos), pos
+
+
+class CommandsList(urwid.ListBox):
+ def __init__(self, master):
+ self.master = master
+ self.walker = CommandListWalker(master)
+ super().__init__(self.walker)
+
+ def keypress(self, size, key):
+ if key == "enter":
+ foc, idx = self.get_focus()
+ signals.status_prompt_command.send(partial=foc.cmd.path + " ")
+ elif key == "m_start":
+ self.set_focus(0)
+ self.walker._modified()
+ elif key == "m_end":
+ self.set_focus(len(self.walker.cmds) - 1)
+ self.walker._modified()
+ return super().keypress(size, key)
+
+
+class CommandHelp(urwid.Frame):
+ def __init__(self, master):
+ self.master = master
+ super().__init__(self.widget(""))
+ self.set_active(False)
+ command_focus_change.connect(self.sig_mod)
+
+ def set_active(self, val):
+ h = urwid.Text("Command Help")
+ style = "heading" if val else "heading_inactive"
+ self.header = urwid.AttrWrap(h, style)
+
+ def widget(self, txt):
+ cols, _ = self.master.ui.get_cols_rows()
+ return urwid.ListBox(
+ [urwid.Text(i) for i in textwrap.wrap(txt, cols)]
+ )
+
+ def sig_mod(self, txt):
+ self.set_body(self.widget(txt))
+
+
+class Commands(urwid.Pile):
+ keyctx = "commands"
+
+ def __init__(self, master):
+ oh = CommandHelp(master)
+ super().__init__(
+ [
+ CommandsList(master),
+ (HELP_HEIGHT, oh),
+ ]
+ )
+ self.master = master
+
+ def keypress(self, size, key):
+ if key == "tab":
+ self.focus_position = (
+ self.focus_position + 1
+ ) % len(self.widget_list)
+ self.widget_list[1].set_active(self.focus_position == 1)
+ key = None
+
+ # This is essentially a copypasta from urwid.Pile's keypress handler.
+ # So much for "closed for modification, but open for extension".
+ item_rows = None
+ if len(size) == 2:
+ item_rows = self.get_item_rows(size, focus = True)
+ i = self.widget_list.index(self.focus_item)
+ tsize = self.get_item_size(size, i, True, item_rows)
+ return self.focus_item.keypress(tsize, key)
diff --git a/mitmproxy/tools/console/common.py b/mitmproxy/tools/console/common.py
index ec637cbc..de024d1a 100644
--- a/mitmproxy/tools/console/common.py
+++ b/mitmproxy/tools/console/common.py
@@ -1,25 +1,9 @@
-# -*- coding: utf-8 -*-
-
-
-import os
-
import urwid
import urwid.util
-import mitmproxy.net
from functools import lru_cache
-from mitmproxy.tools.console import signals
-from mitmproxy import export
from mitmproxy.utils import human
-try:
- import pyperclip
-except:
- pyperclip = False
-
-
-VIEW_FLOW_REQUEST = 0
-VIEW_FLOW_RESPONSE = 1
METHOD_OPTIONS = [
("get", "g"),
@@ -93,20 +77,6 @@ def format_keyvals(lst, key="key", val="text", indent=0):
return ret
-def shortcuts(k):
- if k == " ":
- k = "page down"
- elif k == "ctrl f":
- k = "page down"
- elif k == "ctrl b":
- k = "page up"
- elif k == "j":
- k = "down"
- elif k == "k":
- k = "up"
- return k
-
-
def fcol(s, attr):
s = str(s)
return (
@@ -134,200 +104,6 @@ else:
SYMBOL_DOWN = " "
-# Save file to disk
-def save_data(path, data):
- if not path:
- return
- try:
- if isinstance(data, bytes):
- mode = "wb"
- else:
- mode = "w"
- with open(path, mode) as f:
- f.write(data)
- except IOError as v:
- signals.status_message.send(message=v.strerror)
-
-
-def ask_save_overwrite(path, data):
- if os.path.exists(path):
- def save_overwrite(k):
- if k == "y":
- save_data(path, data)
-
- signals.status_prompt_onekey.send(
- prompt = "'" + path + "' already exists. Overwrite?",
- keys = (
- ("yes", "y"),
- ("no", "n"),
- ),
- callback = save_overwrite
- )
- else:
- save_data(path, data)
-
-
-def ask_save_path(data, prompt="File path"):
- signals.status_prompt_path.send(
- prompt = prompt,
- callback = ask_save_overwrite,
- args = (data, )
- )
-
-
-def ask_scope_and_callback(flow, cb, *args):
- request_has_content = flow.request and flow.request.raw_content
- response_has_content = flow.response and flow.response.raw_content
-
- if request_has_content and response_has_content:
- signals.status_prompt_onekey.send(
- prompt = "Save",
- keys = (
- ("request", "q"),
- ("response", "s"),
- ("both", "b"),
- ),
- callback = cb,
- args = (flow,) + args
- )
- elif response_has_content:
- cb("s", flow, *args)
- else:
- cb("q", flow, *args)
-
-
-def copy_to_clipboard_or_prompt(data):
- # pyperclip calls encode('utf-8') on data to be copied without checking.
- # if data are already encoded that way UnicodeDecodeError is thrown.
- if isinstance(data, bytes):
- toclip = data.decode("utf8", "replace")
- else:
- toclip = data
-
- try:
- pyperclip.copy(toclip)
- except (RuntimeError, UnicodeDecodeError, AttributeError, TypeError):
- def save(k):
- if k == "y":
- ask_save_path(data, "Save data")
- signals.status_prompt_onekey.send(
- prompt = "Cannot copy data to clipboard. Save as file?",
- keys = (
- ("yes", "y"),
- ("no", "n"),
- ),
- callback = save
- )
-
-
-def format_flow_data(key, scope, flow):
- data = b""
- if scope in ("q", "b"):
- request = flow.request.copy()
- request.decode(strict=False)
- if request.content is None:
- return None, "Request content is missing"
- if key == "h":
- data += mitmproxy.net.http.http1.assemble_request(request)
- elif key == "c":
- data += request.get_content(strict=False)
- else:
- raise ValueError("Unknown key: {}".format(key))
- if scope == "b" and flow.request.raw_content and flow.response:
- # Add padding between request and response
- data += b"\r\n" * 2
- if scope in ("s", "b") and flow.response:
- response = flow.response.copy()
- response.decode(strict=False)
- if response.content is None:
- return None, "Response content is missing"
- if key == "h":
- data += mitmproxy.net.http.http1.assemble_response(response)
- elif key == "c":
- data += response.get_content(strict=False)
- else:
- raise ValueError("Unknown key: {}".format(key))
- return data, False
-
-
-def handle_flow_data(scope, flow, key, writer):
- """
- key: _c_ontent, _h_eaders+content, _u_rl
- scope: re_q_uest, re_s_ponse, _b_oth
- writer: copy_to_clipboard_or_prompt, ask_save_path
- """
- data, err = format_flow_data(key, scope, flow)
-
- if err:
- signals.status_message.send(message=err)
- return
-
- if not data:
- if scope == "q":
- signals.status_message.send(message="No request content.")
- elif scope == "s":
- signals.status_message.send(message="No response content.")
- else:
- signals.status_message.send(message="No content.")
- return
-
- writer(data)
-
-
-def ask_save_body(scope, flow):
- """
- Save either the request or the response body to disk.
-
- scope: re_q_uest, re_s_ponse, _b_oth, None (ask user if necessary)
- """
-
- request_has_content = flow.request and flow.request.raw_content
- response_has_content = flow.response and flow.response.raw_content
-
- if scope is None:
- ask_scope_and_callback(flow, ask_save_body)
- elif scope == "q" and request_has_content:
- ask_save_path(
- flow.request.get_content(strict=False),
- "Save request content to"
- )
- elif scope == "s" and response_has_content:
- ask_save_path(
- flow.response.get_content(strict=False),
- "Save response content to"
- )
- elif scope == "b" and request_has_content and response_has_content:
- ask_save_path(
- (flow.request.get_content(strict=False) + b"\n" +
- flow.response.get_content(strict=False)),
- "Save request & response content to"
- )
- else:
- signals.status_message.send(message="No content.")
-
-
-def export_to_clip_or_file(key, scope, flow, writer):
- """
- Export selected flow to clipboard or a file.
-
- key: _c_ontent, _h_eaders+content, _u_rl,
- cu_r_l_command, _p_ython_code,
- _l_ocust_code, locust_t_ask
- scope: None, _a_ll, re_q_uest, re_s_ponse
- writer: copy_to_clipboard_or_prompt, ask_save_path
- """
-
- for _, exp_key, exporter in export.EXPORTERS:
- if key == exp_key:
- if exporter is None: # 'c' & 'h'
- if scope is None:
- ask_scope_and_callback(flow, handle_flow_data, key, writer)
- else:
- handle_flow_data(scope, flow, key, writer)
- else: # other keys
- writer(exporter(flow))
-
-
@lru_cache(maxsize=800)
def raw_format_flow(f, flow):
f = dict(f)
@@ -418,13 +194,16 @@ def raw_format_flow(f, flow):
def format_flow(f, focus, extended=False, hostheader=False, max_url_len=False):
+ acked = False
+ if f.reply and f.reply.state == "committed":
+ acked = True
d = dict(
focus=focus,
extended=extended,
max_url_len=max_url_len,
intercepted = f.intercepted,
- acked = f.reply.state == "committed",
+ acked = acked,
req_timestamp = f.request.timestamp_start,
req_is_replay = f.request.is_replay,
diff --git a/mitmproxy/tools/console/eventlog.py b/mitmproxy/tools/console/eventlog.py
new file mode 100644
index 00000000..0b8a3f8c
--- /dev/null
+++ b/mitmproxy/tools/console/eventlog.py
@@ -0,0 +1,47 @@
+import urwid
+from mitmproxy.tools.console import signals
+
+EVENTLOG_SIZE = 10000
+
+
+class LogBufferWalker(urwid.SimpleListWalker):
+ pass
+
+
+class EventLog(urwid.ListBox):
+ keyctx = "eventlog"
+
+ def __init__(self, master):
+ self.walker = LogBufferWalker([])
+ self.master = master
+ urwid.ListBox.__init__(self, self.walker)
+ signals.sig_add_log.connect(self.sig_add_log)
+
+ def set_focus(self, index):
+ if 0 <= index < len(self.walker):
+ super().set_focus(index)
+
+ def keypress(self, size, key):
+ if key == "z":
+ self.master.clear_events()
+ key = None
+ elif key == "m_end":
+ self.set_focus(len(self.walker) - 1)
+ elif key == "m_start":
+ self.set_focus(0)
+ return urwid.ListBox.keypress(self, size, key)
+
+ def sig_add_log(self, sender, e, level):
+ txt = "%s: %s" % (level, str(e))
+ if level in ("error", "warn"):
+ e = urwid.Text((level, txt))
+ else:
+ e = urwid.Text(txt)
+ self.walker.append(e)
+ if len(self.walker) > EVENTLOG_SIZE:
+ self.walker.pop(0)
+ if self.master.options.console_focus_follow:
+ self.walker.set_focus(len(self.walker) - 1)
+
+ def clear_events(self):
+ self.walker[:] = []
diff --git a/mitmproxy/tools/console/flowdetailview.py b/mitmproxy/tools/console/flowdetailview.py
index 691f19a5..28fe1fbc 100644
--- a/mitmproxy/tools/console/flowdetailview.py
+++ b/mitmproxy/tools/console/flowdetailview.py
@@ -27,12 +27,13 @@ def flowdetails(state, flow: http.HTTPFlow):
text.append(urwid.Text([("head", "Metadata:")]))
text.extend(common.format_keyvals(parts, key="key", val="text", indent=4))
- if sc is not None:
+ if sc is not None and sc.ip_address:
text.append(urwid.Text([("head", "Server Connection:")]))
parts = [
- ["Address", "{}:{}".format(sc.address[0], sc.address[1])],
- ["Resolved Address", "{}:{}".format(sc.ip_address[0], sc.ip_address[1])],
+ ["Address", human.format_address(sc.address)],
]
+ if sc.ip_address:
+ parts.append(["Resolved Address", human.format_address(sc.ip_address)])
if resp:
parts.append(["HTTP Version", resp.http_version])
if sc.alpn_proto_negotiated:
@@ -182,4 +183,4 @@ def flowdetails(state, flow: http.HTTPFlow):
text.append(urwid.Text([("head", "Timing:")]))
text.extend(common.format_keyvals(parts, key="key", val="text", indent=4))
- return searchable.Searchable(state, text)
+ return searchable.Searchable(text)
diff --git a/mitmproxy/tools/console/flowlist.py b/mitmproxy/tools/console/flowlist.py
index 5fe86975..4184eeb4 100644
--- a/mitmproxy/tools/console/flowlist.py
+++ b/mitmproxy/tools/console/flowlist.py
@@ -1,10 +1,7 @@
import urwid
-from mitmproxy import exceptions
from mitmproxy.tools.console import common
-from mitmproxy.tools.console import signals
-from mitmproxy.addons import view
-from mitmproxy import export
+import mitmproxy.tools.console.master # noqa
def _mkhelp():
@@ -50,71 +47,6 @@ footer = [
]
-class LogBufferBox(urwid.ListBox):
-
- def __init__(self, master):
- self.master = master
- urwid.ListBox.__init__(self, master.logbuffer)
-
- def keypress(self, size, key):
- key = common.shortcuts(key)
- if key == "z":
- self.master.clear_events()
- key = None
- elif key == "G":
- self.set_focus(len(self.master.logbuffer) - 1)
- elif key == "g":
- self.set_focus(0)
- elif key == "F":
- o = self.master.options
- o.console_focus_follow = not o.console_focus_follow
- return urwid.ListBox.keypress(self, size, key)
-
-
-class BodyPile(urwid.Pile):
-
- def __init__(self, master):
- h = urwid.Text("Event log")
- h = urwid.Padding(h, align="left", width=("relative", 100))
-
- self.inactive_header = urwid.AttrWrap(h, "heading_inactive")
- self.active_header = urwid.AttrWrap(h, "heading")
-
- urwid.Pile.__init__(
- self,
- [
- FlowListBox(master),
- urwid.Frame(
- LogBufferBox(master),
- header = self.inactive_header
- )
- ]
- )
- self.master = master
-
- def keypress(self, size, key):
- if key == "tab":
- self.focus_position = (
- self.focus_position + 1) % len(self.widget_list)
- if self.focus_position == 1:
- self.widget_list[1].header = self.active_header
- else:
- self.widget_list[1].header = self.inactive_header
- key = None
- elif key == "e":
- self.master.toggle_eventlog()
- key = None
-
- # This is essentially a copypasta from urwid.Pile's keypress handler.
- # So much for "closed for modification, but open for extension".
- item_rows = None
- if len(size) == 2:
- item_rows = self.get_item_rows(size, focus = True)
- i = self.widget_list.index(self.focus_item)
- tsize = self.get_item_size(size, i, True, item_rows)
- return self.focus_item.keypress(tsize, key)
-
-
class FlowItem(urwid.WidgetWrap):
def __init__(self, master, flow):
@@ -134,27 +66,6 @@ class FlowItem(urwid.WidgetWrap):
def selectable(self):
return True
- def save_flows_prompt(self, k):
- if k == "l":
- signals.status_prompt_path.send(
- prompt = "Save listed flows to",
- callback = self.master.save_flows
- )
- else:
- signals.status_prompt_path.send(
- prompt = "Save this flow to",
- callback = self.master.save_one_flow,
- args = (self.flow,)
- )
-
- def server_replay_prompt(self, k):
- a = self.master.addons.get("serverplayback")
- if k == "a":
- a.load([i.copy() for i in self.master.view])
- elif k == "t":
- a.load([self.flow.copy()])
- signals.update_settings.send(self)
-
def mouse_event(self, size, event, button, col, row, focus):
if event == "mouse press" and button == 1:
if self.flow.request:
@@ -162,119 +73,15 @@ class FlowItem(urwid.WidgetWrap):
return True
def keypress(self, xxx_todo_changeme, key):
- (maxcol,) = xxx_todo_changeme
- key = common.shortcuts(key)
- if key == "a":
- self.flow.resume()
- self.master.view.update(self.flow)
- elif key == "d":
- if self.flow.killable:
- self.flow.kill()
- self.master.view.remove(self.flow)
- elif key == "D":
- cp = self.flow.copy()
- self.master.view.add(cp)
- self.master.view.focus.flow = cp
- elif key == "m":
- self.flow.marked = not self.flow.marked
- signals.flowlist_change.send(self)
- elif key == "r":
- try:
- self.master.replay_request(self.flow)
- except exceptions.ReplayException as e:
- signals.add_log("Replay error: %s" % e, "warn")
- signals.flowlist_change.send(self)
- elif key == "S":
- def stop_server_playback(response):
- if response == "y":
- self.master.options.server_replay = []
- a = self.master.addons.get("serverplayback")
- if a.count():
- signals.status_prompt_onekey.send(
- prompt = "Stop current server replay?",
- keys = (
- ("yes", "y"),
- ("no", "n"),
- ),
- callback = stop_server_playback,
- )
- else:
- signals.status_prompt_onekey.send(
- prompt = "Server Replay",
- keys = (
- ("all flows", "a"),
- ("this flow", "t"),
- ),
- callback = self.server_replay_prompt,
- )
- elif key == "U":
- for f in self.master.view:
- f.marked = False
- signals.flowlist_change.send(self)
- elif key == "V":
- if not self.flow.modified():
- signals.status_message.send(message="Flow not modified.")
- return
- self.flow.revert()
- signals.flowlist_change.send(self)
- signals.status_message.send(message="Reverted.")
- elif key == "w":
- signals.status_prompt_onekey.send(
- self,
- prompt = "Save",
- keys = (
- ("listed flows", "l"),
- ("this flow", "t"),
- ),
- callback = self.save_flows_prompt,
- )
- elif key == "X":
- if self.flow.killable:
- self.flow.kill()
- self.master.view.update(self.flow)
- elif key == "enter":
- if self.flow.request:
- self.master.view_flow(self.flow)
- elif key == "|":
- signals.status_prompt_path.send(
- prompt = "Send flow to script",
- callback = self.master.run_script_once,
- args = (self.flow,)
- )
- elif key == "E":
- signals.status_prompt_onekey.send(
- self,
- prompt = "Export to file",
- keys = [(e[0], e[1]) for e in export.EXPORTERS],
- callback = common.export_to_clip_or_file,
- args = (None, self.flow, common.ask_save_path)
- )
- elif key == "C":
- signals.status_prompt_onekey.send(
- self,
- prompt = "Export to clipboard",
- keys = [(e[0], e[1]) for e in export.EXPORTERS],
- callback = common.export_to_clip_or_file,
- args = (None, self.flow, common.copy_to_clipboard_or_prompt)
- )
- elif key == "b":
- common.ask_save_body(None, self.flow)
- else:
- return key
+ return key
class FlowListWalker(urwid.ListWalker):
def __init__(self, master):
self.master = master
- self.master.view.sig_view_refresh.connect(self.sig_mod)
- self.master.view.sig_view_add.connect(self.sig_mod)
- self.master.view.sig_view_remove.connect(self.sig_mod)
- self.master.view.sig_view_update.connect(self.sig_mod)
- self.master.view.focus.sig_change.connect(self.sig_mod)
- signals.flowlist_change.connect(self.sig_mod)
- def sig_mod(self, *args, **kwargs):
+ def view_changed(self):
self._modified()
def get_focus(self):
@@ -286,7 +93,6 @@ class FlowListWalker(urwid.ListWalker):
def set_focus(self, index):
if self.master.view.inbounds(index):
self.master.view.focus.index = index
- signals.flowlist_change.send(self)
def get_next(self, pos):
pos = pos + 1
@@ -304,111 +110,20 @@ class FlowListWalker(urwid.ListWalker):
class FlowListBox(urwid.ListBox):
+ keyctx = "flowlist"
- def __init__(self, master: "mitmproxy.tools.console.master.ConsoleMaster"):
+ def __init__(
+ self, master: "mitmproxy.tools.console.master.ConsoleMaster"
+ ) -> None:
self.master = master # type: "mitmproxy.tools.console.master.ConsoleMaster"
super().__init__(FlowListWalker(master))
- def get_method_raw(self, k):
- if k:
- self.get_url(k)
-
- def get_method(self, k):
- if k == "e":
- signals.status_prompt.send(
- self,
- prompt = "Method",
- text = "",
- callback = self.get_method_raw
- )
- else:
- method = ""
- for i in common.METHOD_OPTIONS:
- if i[1] == k:
- method = i[0].upper()
- self.get_url(method)
-
- def get_url(self, method):
- signals.status_prompt.send(
- prompt = "URL",
- text = "http://www.example.com/",
- callback = self.new_request,
- args = (method,)
- )
-
- def new_request(self, url, method):
- try:
- f = self.master.create_request(method, url)
- except ValueError as e:
- signals.status_message.send(message = "Invalid URL: " + str(e))
- return
- self.master.view.focus.flow = f
-
def keypress(self, size, key):
- key = common.shortcuts(key)
- if key == "A":
- for f in self.master.view:
- if f.intercepted:
- f.resume()
- self.master.view.update(f)
- elif key == "z":
- self.master.view.clear()
- elif key == "Z":
- self.master.view.clear_not_marked()
- elif key == "e":
- self.master.toggle_eventlog()
- elif key == "g":
- if len(self.master.view):
- self.master.view.focus.index = 0
- elif key == "G":
- if len(self.master.view):
- self.master.view.focus.index = len(self.master.view) - 1
- elif key == "f":
- signals.status_prompt.send(
- prompt = "Filter View",
- text = self.master.options.filter,
- callback = self.master.options.setter("filter")
- )
- elif key == "L":
- signals.status_prompt_path.send(
- self,
- prompt = "Load flows",
- callback = self.master.load_flows_callback
- )
- elif key == "M":
- self.master.view.toggle_marked()
- elif key == "n":
- signals.status_prompt_onekey.send(
- prompt = "Method",
- keys = common.METHOD_OPTIONS,
- callback = self.get_method
- )
- elif key == "o":
- orders = [(i[1], i[0]) for i in view.orders]
- lookup = dict([(i[0], i[1]) for i in view.orders])
-
- def change_order(k):
- self.master.options.console_order = lookup[k]
+ if key == "m_start":
+ self.master.commands.call("view.go 0")
+ elif key == "m_end":
+ self.master.commands.call("view.go -1")
+ return urwid.ListBox.keypress(self, size, key)
- signals.status_prompt_onekey.send(
- prompt = "Order",
- keys = orders,
- callback = change_order
- )
- elif key == "F":
- o = self.master.options
- o.console_focus_follow = not o.console_focus_follow
- elif key == "v":
- val = not self.master.options.console_order_reversed
- self.master.options.console_order_reversed = val
- elif key == "W":
- if self.master.options.streamfile:
- self.master.options.streamfile = None
- else:
- signals.status_prompt_path.send(
- self,
- prompt="Stream flows to",
- callback= lambda path: self.master.options.update(streamfile=path)
- )
- else:
- return urwid.ListBox.keypress(self, size, key)
+ def view_changed(self):
+ self.body.view_changed()
diff --git a/mitmproxy/tools/console/flowview.py b/mitmproxy/tools/console/flowview.py
index 90cca1c5..00951610 100644
--- a/mitmproxy/tools/console/flowview.py
+++ b/mitmproxy/tools/console/flowview.py
@@ -1,5 +1,4 @@
import math
-import os
import sys
from functools import lru_cache
from typing import Optional, Union # noqa
@@ -7,17 +6,13 @@ from typing import Optional, Union # noqa
import urwid
from mitmproxy import contentviews
-from mitmproxy import exceptions
-from mitmproxy import export
from mitmproxy import http
-from mitmproxy.net.http import Headers
-from mitmproxy.net.http import status_codes
from mitmproxy.tools.console import common
from mitmproxy.tools.console import flowdetailview
-from mitmproxy.tools.console import grideditor
from mitmproxy.tools.console import searchable
from mitmproxy.tools.console import signals
from mitmproxy.tools.console import tabs
+import mitmproxy.tools.console.master # noqa
class SearchError(Exception):
@@ -102,48 +97,48 @@ footer = [
class FlowViewHeader(urwid.WidgetWrap):
- def __init__(self, master: "mitmproxy.console.master.ConsoleMaster", f: http.HTTPFlow):
+ def __init__(
+ self,
+ master: "mitmproxy.tools.console.master.ConsoleMaster",
+ ) -> None:
self.master = master
- self.flow = f
- self._w = common.format_flow(
- f,
- False,
- extended=True,
- hostheader=self.master.options.showhost
- )
- signals.flow_change.connect(self.sig_flow_change)
+ self.focus_changed()
- def sig_flow_change(self, sender, flow):
- if flow == self.flow:
+ def focus_changed(self):
+ if self.master.view.focus.flow:
self._w = common.format_flow(
- flow,
+ self.master.view.focus.flow,
False,
extended=True,
hostheader=self.master.options.showhost
)
+ else:
+ self._w = urwid.Pile([])
-TAB_REQ = 0
-TAB_RESP = 1
-
-
-class FlowView(tabs.Tabs):
- highlight_color = "focusfield"
+class FlowDetails(tabs.Tabs):
+ def __init__(self, master):
+ self.master = master
+ super().__init__([])
+ self.show()
+ self.last_displayed_body = None
- def __init__(self, master, view, flow, tab_offset):
- self.master, self.view, self.flow = master, view, flow
- super().__init__(
- [
+ def focus_changed(self):
+ if self.master.view.focus.flow:
+ self.tabs = [
(self.tab_request, self.view_request),
(self.tab_response, self.view_response),
(self.tab_details, self.view_details),
- ],
- tab_offset
- )
-
+ ]
self.show()
- self.last_displayed_body = None
- signals.flow_change.connect(self.sig_flow_change)
+
+ @property
+ def view(self):
+ return self.master.view
+
+ @property
+ def flow(self):
+ return self.master.view.focus.flow
def tab_request(self):
if self.flow.intercepted and not self.flow.response:
@@ -169,18 +164,13 @@ class FlowView(tabs.Tabs):
def view_details(self):
return flowdetailview.flowdetails(self.view, self.flow)
- def sig_flow_change(self, sender, flow):
- if flow == self.flow:
- self.show()
-
def content_view(self, viewmode, message):
if message.raw_content is None:
msg, body = "", [urwid.Text([("error", "[content missing]")])]
return msg, body
else:
- s = self.view.settings[self.flow]
- full = s.get((self.tab_offset, "fullcontents"), False)
- if full:
+ full = self.master.commands.call("view.getval @focus fullcontents false")
+ if full == "true":
limit = sys.maxsize
else:
limit = contentviews.VIEW_CUTOFF
@@ -236,13 +226,6 @@ class FlowView(tabs.Tabs):
return description, text_objects
- def viewmode_get(self):
- override = self.view.settings[self.flow].get(
- (self.tab_offset, "prettyview"),
- None
- )
- return self.master.options.default_contentview if override is None else override
-
def conn_text(self, conn):
if conn:
txt = common.format_keyvals(
@@ -250,7 +233,7 @@ class FlowView(tabs.Tabs):
key = "header",
val = "text"
)
- viewmode = self.viewmode_get()
+ viewmode = self.master.commands.call("console.flowview.mode")
msg, body = self.content_view(viewmode, conn)
cols = [
@@ -284,404 +267,23 @@ class FlowView(tabs.Tabs):
]
)
]
- return searchable.Searchable(self.view, txt)
-
- def set_method_raw(self, m):
- if m:
- self.flow.request.method = m
- signals.flow_change.send(self, flow = self.flow)
-
- def edit_method(self, m):
- if m == "e":
- signals.status_prompt.send(
- prompt = "Method",
- text = self.flow.request.method,
- callback = self.set_method_raw
- )
- else:
- for i in common.METHOD_OPTIONS:
- if i[1] == m:
- self.flow.request.method = i[0].upper()
- signals.flow_change.send(self, flow = self.flow)
-
- def set_url(self, url):
- request = self.flow.request
- try:
- request.url = str(url)
- except ValueError:
- return "Invalid URL."
- signals.flow_change.send(self, flow = self.flow)
-
- def set_resp_status_code(self, status_code):
- try:
- status_code = int(status_code)
- except ValueError:
- return None
- self.flow.response.status_code = status_code
- if status_code in status_codes.RESPONSES:
- self.flow.response.reason = status_codes.RESPONSES[status_code]
- signals.flow_change.send(self, flow = self.flow)
-
- def set_resp_reason(self, reason):
- self.flow.response.reason = reason
- signals.flow_change.send(self, flow = self.flow)
-
- def set_headers(self, fields, conn):
- conn.headers = Headers(fields)
- signals.flow_change.send(self, flow = self.flow)
-
- def set_query(self, lst, conn):
- conn.query = lst
- signals.flow_change.send(self, flow = self.flow)
-
- def set_path_components(self, lst, conn):
- conn.path_components = lst
- signals.flow_change.send(self, flow = self.flow)
-
- def set_form(self, lst, conn):
- conn.urlencoded_form = lst
- signals.flow_change.send(self, flow = self.flow)
-
- def edit_form(self, conn):
- self.master.view_grideditor(
- grideditor.URLEncodedFormEditor(
- self.master,
- conn.urlencoded_form.items(multi=True),
- self.set_form,
- conn
- )
- )
-
- def edit_form_confirm(self, key, conn):
- if key == "y":
- self.edit_form(conn)
-
- def set_cookies(self, lst, conn):
- conn.cookies = lst
- signals.flow_change.send(self, flow = self.flow)
-
- def set_setcookies(self, data, conn):
- conn.cookies = data
- signals.flow_change.send(self, flow = self.flow)
-
- def edit(self, part):
- if self.tab_offset == TAB_REQ:
- message = self.flow.request
- else:
- if not self.flow.response:
- self.flow.response = http.HTTPResponse.make(200, b"")
- message = self.flow.response
-
- self.flow.backup()
- if message == self.flow.request and part == "c":
- self.master.view_grideditor(
- grideditor.CookieEditor(
- self.master,
- message.cookies.items(multi=True),
- self.set_cookies,
- message
- )
- )
- if message == self.flow.response and part == "c":
- self.master.view_grideditor(
- grideditor.SetCookieEditor(
- self.master,
- message.cookies.items(multi=True),
- self.set_setcookies,
- message
- )
- )
- if part == "r":
- # Fix an issue caused by some editors when editing a
- # request/response body. Many editors make it hard to save a
- # file without a terminating newline on the last line. When
- # editing message bodies, this can cause problems. For now, I
- # just strip the newlines off the end of the body when we return
- # from an editor.
- c = self.master.spawn_editor(message.get_content(strict=False) or b"")
- message.content = c.rstrip(b"\n")
- elif part == "f":
- if not message.urlencoded_form and message.raw_content:
- signals.status_prompt_onekey.send(
- prompt = "Existing body is not a URL-encoded form. Clear and edit?",
- keys = [
- ("yes", "y"),
- ("no", "n"),
- ],
- callback = self.edit_form_confirm,
- args = (message,)
- )
- else:
- self.edit_form(message)
- elif part == "h":
- self.master.view_grideditor(
- grideditor.HeaderEditor(
- self.master,
- message.headers.fields,
- self.set_headers,
- message
- )
- )
- elif part == "p":
- p = message.path_components
- self.master.view_grideditor(
- grideditor.PathEditor(
- self.master,
- p,
- self.set_path_components,
- message
- )
- )
- elif part == "q":
- self.master.view_grideditor(
- grideditor.QueryEditor(
- self.master,
- message.query.items(multi=True),
- self.set_query, message
- )
- )
- elif part == "u":
- signals.status_prompt.send(
- prompt = "URL",
- text = message.url,
- callback = self.set_url
- )
- elif part == "m" and message == self.flow.request:
- signals.status_prompt_onekey.send(
- prompt = "Method",
- keys = common.METHOD_OPTIONS,
- callback = self.edit_method
- )
- elif part == "o":
- signals.status_prompt.send(
- prompt = "Code",
- text = str(message.status_code),
- callback = self.set_resp_status_code
- )
- elif part == "m" and message == self.flow.response:
- signals.status_prompt.send(
- prompt = "Message",
- text = message.reason,
- callback = self.set_resp_reason
- )
- signals.flow_change.send(self, flow = self.flow)
-
- def view_flow(self, flow):
- signals.pop_view_state.send(self)
- self.master.view_flow(flow, self.tab_offset)
-
- def _view_nextprev_flow(self, idx, flow):
- if not self.view.inbounds(idx):
- signals.status_message.send(message="No more flows")
- return
- self.view_flow(self.view[idx])
+ return searchable.Searchable(txt)
- def view_next_flow(self, flow):
- return self._view_nextprev_flow(self.view.index(flow) + 1, flow)
+ def keypress(self, size, key):
+ key = super().keypress(size, key)
+ return self._w.keypress(size, key)
- def view_prev_flow(self, flow):
- return self._view_nextprev_flow(self.view.index(flow) - 1, flow)
- def change_this_display_mode(self, t):
- view = contentviews.get_by_shortcut(t)
- if view:
- self.view.settings[self.flow][(self.tab_offset, "prettyview")] = view.name
- else:
- self.view.settings[self.flow][(self.tab_offset, "prettyview")] = None
- signals.flow_change.send(self, flow=self.flow)
-
- def keypress(self, size, key):
- conn = None # type: Optional[Union[http.HTTPRequest, http.HTTPResponse]]
- if self.tab_offset == TAB_REQ:
- conn = self.flow.request
- elif self.tab_offset == TAB_RESP:
- conn = self.flow.response
+class FlowView(urwid.Frame):
+ keyctx = "flowview"
- key = super().keypress(size, key)
+ def __init__(self, master):
+ super().__init__(
+ FlowDetails(master),
+ header = FlowViewHeader(master),
+ )
+ self.master = master
- # Special case: Space moves over to the next flow.
- # We need to catch that before applying common.shortcuts()
- if key == " ":
- self.view_next_flow(self.flow)
- return
-
- key = common.shortcuts(key)
- if key in ("up", "down", "page up", "page down"):
- # Pass scroll events to the wrapped widget
- self._w.keypress(size, key)
- elif key == "a":
- self.flow.resume()
- self.master.view.update(self.flow)
- elif key == "A":
- for f in self.view:
- if f.intercepted:
- f.resume()
- self.master.view.update(self.flow)
- elif key == "d":
- if self.flow.killable:
- self.flow.kill()
- self.view.remove(self.flow)
- if not self.view.focus.flow:
- self.master.view_flowlist()
- else:
- self.view_flow(self.view.focus.flow)
- elif key == "D":
- cp = self.flow.copy()
- self.master.view.add(cp)
- self.master.view.focus.flow = cp
- self.view_flow(cp)
- signals.status_message.send(message="Duplicated.")
- elif key == "p":
- self.view_prev_flow(self.flow)
- elif key == "r":
- try:
- self.master.replay_request(self.flow)
- except exceptions.ReplayException as e:
- signals.add_log("Replay error: %s" % e, "warn")
- signals.flow_change.send(self, flow = self.flow)
- elif key == "V":
- if self.flow.modified():
- self.flow.revert()
- signals.flow_change.send(self, flow = self.flow)
- signals.status_message.send(message="Reverted.")
- else:
- signals.status_message.send(message="Flow not modified.")
- elif key == "W":
- signals.status_prompt_path.send(
- prompt = "Save this flow",
- callback = self.master.save_one_flow,
- args = (self.flow,)
- )
- elif key == "|":
- signals.status_prompt_path.send(
- prompt = "Send flow to script",
- callback = self.master.run_script_once,
- args = (self.flow,)
- )
- elif key == "e":
- if self.tab_offset == TAB_REQ:
- signals.status_prompt_onekey.send(
- prompt="Edit request",
- keys=(
- ("cookies", "c"),
- ("query", "q"),
- ("path", "p"),
- ("url", "u"),
- ("header", "h"),
- ("form", "f"),
- ("raw body", "r"),
- ("method", "m"),
- ),
- callback=self.edit
- )
- elif self.tab_offset == TAB_RESP:
- signals.status_prompt_onekey.send(
- prompt="Edit response",
- keys=(
- ("cookies", "c"),
- ("code", "o"),
- ("message", "m"),
- ("header", "h"),
- ("raw body", "r"),
- ),
- callback=self.edit
- )
- else:
- signals.status_message.send(
- message="Tab to the request or response",
- expire=1
- )
- elif key in set("bfgmxvzEC") and not conn:
- signals.status_message.send(
- message = "Tab to the request or response",
- expire = 1
- )
- return
- elif key == "b":
- if self.tab_offset == TAB_REQ:
- common.ask_save_body("q", self.flow)
- else:
- common.ask_save_body("s", self.flow)
- elif key == "f":
- self.view.settings[self.flow][(self.tab_offset, "fullcontents")] = True
- signals.flow_change.send(self, flow = self.flow)
- signals.status_message.send(message="Loading all body data...")
- elif key == "m":
- p = list(contentviews.view_prompts)
- p.insert(0, ("Clear", "C"))
- signals.status_prompt_onekey.send(
- self,
- prompt = "Display mode",
- keys = p,
- callback = self.change_this_display_mode
- )
- elif key == "E":
- if self.tab_offset == TAB_REQ:
- scope = "q"
- else:
- scope = "s"
- signals.status_prompt_onekey.send(
- self,
- prompt = "Export to file",
- keys = [(e[0], e[1]) for e in export.EXPORTERS],
- callback = common.export_to_clip_or_file,
- args = (scope, self.flow, common.ask_save_path)
- )
- elif key == "C":
- if self.tab_offset == TAB_REQ:
- scope = "q"
- else:
- scope = "s"
- signals.status_prompt_onekey.send(
- self,
- prompt = "Export to clipboard",
- keys = [(e[0], e[1]) for e in export.EXPORTERS],
- callback = common.export_to_clip_or_file,
- args = (scope, self.flow, common.copy_to_clipboard_or_prompt)
- )
- elif key == "x":
- conn.content = None
- signals.flow_change.send(self, flow=self.flow)
- elif key == "v":
- if conn.raw_content:
- t = conn.headers.get("content-type")
- if "EDITOR" in os.environ or "PAGER" in os.environ:
- self.master.spawn_external_viewer(conn.get_content(strict=False), t)
- else:
- signals.status_message.send(
- message = "Error! Set $EDITOR or $PAGER."
- )
- elif key == "z":
- self.flow.backup()
- e = conn.headers.get("content-encoding", "identity")
- if e != "identity":
- try:
- conn.decode()
- except ValueError:
- signals.status_message.send(
- message = "Could not decode - invalid data?"
- )
- else:
- signals.status_prompt_onekey.send(
- prompt = "Select encoding: ",
- keys = (
- ("gzip", "z"),
- ("deflate", "d"),
- ("brotli", "b"),
- ),
- callback = self.encode_callback,
- args = (conn,)
- )
- signals.flow_change.send(self, flow = self.flow)
- else:
- # Key is not handled here.
- return key
-
- def encode_callback(self, key, conn):
- encoding_map = {
- "z": "gzip",
- "d": "deflate",
- "b": "br",
- }
- conn.encode(encoding_map[key])
- signals.flow_change.send(self, flow = self.flow)
+ def focus_changed(self, *args, **kwargs):
+ self.body.focus_changed()
+ self.header.focus_changed()
diff --git a/mitmproxy/tools/console/grideditor/base.py b/mitmproxy/tools/console/grideditor/base.py
index 4505bb97..35ae655f 100644
--- a/mitmproxy/tools/console/grideditor/base.py
+++ b/mitmproxy/tools/console/grideditor/base.py
@@ -7,10 +7,12 @@ from typing import Iterable
from typing import Optional
from typing import Sequence
from typing import Tuple
+from typing import Set # noqa
import urwid
from mitmproxy.tools.console import common
from mitmproxy.tools.console import signals
+import mitmproxy.tools.console.master # noqa
FOOTER = [
('heading_key', "enter"), ":edit ",
@@ -34,7 +36,7 @@ class Cell(urwid.WidgetWrap):
class Column(metaclass=abc.ABCMeta):
- subeditor = None
+ subeditor = None # type: urwid.Edit
def __init__(self, heading):
self.heading = heading
@@ -62,13 +64,13 @@ class GridRow(urwid.WidgetWrap):
editing: bool,
editor: "GridEditor",
values: Tuple[Iterable[bytes], Container[int]]
- ):
+ ) -> None:
self.focused = focused
self.editor = editor
self.edit_col = None # type: Optional[Cell]
errors = values[1]
- self.fields = []
+ self.fields = [] # type: Sequence[Any]
for i, v in enumerate(values[0]):
if focused == i and editing:
self.edit_col = self.editor.columns[i].Edit(v)
@@ -116,8 +118,8 @@ class GridWalker(urwid.ListWalker):
self,
lst: Iterable[list],
editor: "GridEditor"
- ):
- self.lst = [(i, set()) for i in lst]
+ ) -> None:
+ self.lst = [(i, set()) for i in lst] # type: Sequence[Tuple[Any, Set]]
self.editor = editor
self.focus = 0
self.focus_col = 0
@@ -182,12 +184,12 @@ class GridWalker(urwid.ListWalker):
self.edit_row = GridRow(
self.focus_col, True, self.editor, self.lst[self.focus]
)
- self.editor.master.loop.widget.footer.update(FOOTER_EDITING)
+ signals.footer_help.send(self, helptext=FOOTER_EDITING)
self._modified()
def stop_edit(self):
if self.edit_row:
- self.editor.master.loop.widget.footer.update(FOOTER)
+ signals.footer_help.send(self, helptext=FOOTER)
try:
val = self.edit_row.edit_col.get_data()
except ValueError:
@@ -250,20 +252,22 @@ FIRST_WIDTH_MAX = 40
FIRST_WIDTH_MIN = 20
-class GridEditor(urwid.WidgetWrap):
- title = None # type: str
- columns = None # type: Sequence[Column]
+class BaseGridEditor(urwid.WidgetWrap):
def __init__(
self,
- master: "mitmproxy.console.master.ConsoleMaster",
+ master: "mitmproxy.tools.console.master.ConsoleMaster",
+ title,
+ columns,
value: Any,
callback: Callable[..., None],
*cb_args,
**cb_kwargs
- ):
+ ) -> None:
value = self.data_in(copy.deepcopy(value))
self.master = master
+ self.title = title
+ self.columns = columns
self.value = value
self.callback = callback
self.cb_args = cb_args
@@ -276,9 +280,11 @@ class GridEditor(urwid.WidgetWrap):
first_width = max(len(r), first_width)
self.first_width = min(first_width, FIRST_WIDTH_MAX)
- title = urwid.Text(self.title)
- title = urwid.Padding(title, align="left", width=("relative", 100))
- title = urwid.AttrWrap(title, "heading")
+ title = None
+ if self.title:
+ title = urwid.Text(self.title)
+ title = urwid.Padding(title, align="left", width=("relative", 100))
+ title = urwid.AttrWrap(title, "heading")
headings = []
for i, col in enumerate(self.columns):
@@ -297,12 +303,19 @@ class GridEditor(urwid.WidgetWrap):
self.lb = GridListBox(self.walker)
w = urwid.Frame(
self.lb,
- header=urwid.Pile([title, h])
+ header=urwid.Pile([title, h]) if title else None
)
super().__init__(w)
- self.master.loop.widget.footer.update("")
+ signals.footer_help.send(self, helptext="")
self.show_empty_msg()
+ def view_popping(self):
+ res = []
+ for i in self.walker.lst:
+ if not i[1] and any([x for x in i[0]]):
+ res.append(i[0])
+ self.callback(self.data_out(res), *self.cb_args, **self.cb_kwargs)
+
def show_empty_msg(self):
if self.walker.lst:
self._w.set_footer(None)
@@ -333,22 +346,14 @@ class GridEditor(urwid.WidgetWrap):
self._w.keypress(size, key)
return None
- key = common.shortcuts(key)
column = self.columns[self.walker.focus_col]
- if key in ["q", "esc"]:
- res = []
- for i in self.walker.lst:
- if not i[1] and any([x for x in i[0]]):
- res.append(i[0])
- self.callback(self.data_out(res), *self.cb_args, **self.cb_kwargs)
- signals.pop_view_state.send(self)
- elif key == "g":
+ if key == "m_start":
self.walker.set_focus(0)
- elif key == "G":
+ elif key == "m_end":
self.walker.set_focus(len(self.walker.lst) - 1)
- elif key in ["h", "left"]:
+ elif key == "left":
self.walker.left()
- elif key in ["l", "right"]:
+ elif key == "right":
self.walker.right()
elif key == "tab":
self.walker.tab_next()
@@ -378,7 +383,7 @@ class GridEditor(urwid.WidgetWrap):
"""
Return None, or a string error message.
"""
- return False
+ return None
def handle_key(self, key):
return False
@@ -411,3 +416,74 @@ class GridEditor(urwid.WidgetWrap):
)
)
return text
+
+
+class GridEditor(urwid.WidgetWrap):
+ title = None # type: str
+ columns = None # type: Sequence[Column]
+
+ def __init__(
+ self,
+ master: "mitmproxy.tools.console.master.ConsoleMaster",
+ value: Any,
+ callback: Callable[..., None],
+ *cb_args,
+ **cb_kwargs
+ ) -> None:
+ super().__init__(
+ master,
+ value,
+ self.title,
+ self.columns,
+ callback,
+ *cb_args,
+ **cb_kwargs
+ )
+
+
+class FocusEditor(urwid.WidgetWrap):
+ """
+ A specialised GridEditor that edits the current focused flow.
+ """
+ keyctx = "grideditor"
+
+ def __init__(self, master):
+ self.master = master
+ self.focus_changed()
+
+ def focus_changed(self):
+ if self.master.view.focus.flow:
+ self._w = BaseGridEditor(
+ self.master.view.focus.flow,
+ self.title,
+ self.columns,
+ self.get_data(self.master.view.focus.flow),
+ self.set_data_update,
+ self.master.view.focus.flow,
+ )
+ else:
+ self._w = urwid.Pile([])
+
+ def call(self, v, name, *args, **kwargs):
+ f = getattr(v, name, None)
+ if f:
+ f(*args, **kwargs)
+
+ def view_popping(self):
+ self.call(self._w, "view_popping")
+
+ def get_data(self, flow):
+ """
+ Retrieve the data to edit from the current flow.
+ """
+ raise NotImplementedError
+
+ def set_data(self, vals, flow):
+ """
+ Set the current data on the flow.
+ """
+ raise NotImplementedError
+
+ def set_data_update(self, vals, flow):
+ self.set_data(vals, flow)
+ signals.flow_change.send(self, flow = flow)
diff --git a/mitmproxy/tools/console/grideditor/col_bytes.py b/mitmproxy/tools/console/grideditor/col_bytes.py
index f580e947..e4a53453 100644
--- a/mitmproxy/tools/console/grideditor/col_bytes.py
+++ b/mitmproxy/tools/console/grideditor/col_bytes.py
@@ -9,7 +9,7 @@ from mitmproxy.utils import strutils
def read_file(filename: str, callback: Callable[..., None], escaped: bool) -> Optional[str]:
if not filename:
- return
+ return None
filename = os.path.expanduser(filename)
try:
@@ -26,6 +26,7 @@ def read_file(filename: str, callback: Callable[..., None], escaped: bool) -> Op
# TODO: Refactor the status_prompt_path signal so that we
# can raise exceptions here and return the content instead.
callback(d)
+ return None
class Column(base.Column):
@@ -68,7 +69,7 @@ class Column(base.Column):
class Display(base.Cell):
- def __init__(self, data: bytes):
+ def __init__(self, data: bytes) -> None:
self.data = data
escaped = strutils.bytes_to_escaped_str(data)
w = urwid.Text(escaped, wrap="any")
@@ -79,7 +80,7 @@ class Display(base.Cell):
class Edit(base.Cell):
- def __init__(self, data: bytes):
+ def __init__(self, data: bytes) -> None:
data = strutils.bytes_to_escaped_str(data)
w = urwid.Edit(edit_text=data, wrap="any", multiline=True)
w = urwid.AttrWrap(w, "editfield")
diff --git a/mitmproxy/tools/console/grideditor/col_text.py b/mitmproxy/tools/console/grideditor/col_text.py
index 430ad037..f0ac06f8 100644
--- a/mitmproxy/tools/console/grideditor/col_text.py
+++ b/mitmproxy/tools/console/grideditor/col_text.py
@@ -26,12 +26,11 @@ class Column(col_bytes.Column):
# This is the same for both edit and display.
class EncodingMixin:
- def __init__(self, data: str, encoding_args) -> "TDisplay":
+ def __init__(self, data, encoding_args):
self.encoding_args = encoding_args
- data = data.encode(*self.encoding_args)
- super().__init__(data)
+ super().__init__(data.encode(*self.encoding_args))
- def get_data(self) -> str:
+ def get_data(self):
data = super().get_data()
try:
return data.decode(*self.encoding_args)
diff --git a/mitmproxy/tools/console/grideditor/editors.py b/mitmproxy/tools/console/grideditor/editors.py
index 0d9929ae..671e91fb 100644
--- a/mitmproxy/tools/console/grideditor/editors.py
+++ b/mitmproxy/tools/console/grideditor/editors.py
@@ -1,4 +1,3 @@
-import os
import re
import urwid
@@ -13,18 +12,24 @@ from mitmproxy.tools.console.grideditor import col_bytes
from mitmproxy.tools.console.grideditor import col_subgrid
from mitmproxy.tools.console import signals
from mitmproxy.net.http import user_agents
+from mitmproxy.net.http import Headers
-class QueryEditor(base.GridEditor):
+class QueryEditor(base.FocusEditor):
title = "Editing query"
columns = [
col_text.Column("Key"),
col_text.Column("Value")
]
+ def get_data(self, flow):
+ return flow.request.query.items(multi=True)
-class HeaderEditor(base.GridEditor):
- title = "Editing headers"
+ def set_data(self, vals, flow):
+ flow.request.query = vals
+
+
+class HeaderEditor(base.FocusEditor):
columns = [
col_bytes.Column("Key"),
col_bytes.Column("Value")
@@ -65,35 +70,38 @@ class HeaderEditor(base.GridEditor):
return True
-class URLEncodedFormEditor(base.GridEditor):
+class RequestHeaderEditor(HeaderEditor):
+ title = "Editing request headers"
+
+ def get_data(self, flow):
+ return flow.request.headers.fields
+
+ def set_data(self, vals, flow):
+ flow.request.headers = Headers(vals)
+
+
+class ResponseHeaderEditor(HeaderEditor):
+ title = "Editing response headers"
+
+ def get_data(self, flow):
+ return flow.response.headers.fields
+
+ def set_data(self, vals, flow):
+ flow.response.headers = Headers(vals)
+
+
+class RequestFormEditor(base.FocusEditor):
title = "Editing URL-encoded form"
columns = [
col_text.Column("Key"),
col_text.Column("Value")
]
+ def get_data(self, flow):
+ return flow.request.urlencoded_form.items(multi=True)
-class ReplaceEditor(base.GridEditor):
- title = "Editing replacement patterns"
- columns = [
- col_text.Column("Filter"),
- col_text.Column("Regex"),
- col_text.Column("Replacement"),
- ]
-
- def is_error(self, col, val):
- if col == 0:
- if not flowfilter.parse(val):
- return "Invalid filter specification."
- elif col == 1:
- try:
- re.compile(val)
- except re.error:
- return "Invalid regular expression."
- elif col == 2:
- if val.startswith("@") and not os.path.isfile(os.path.expanduser(val[1:])):
- return "Invalid file path"
- return False
+ def set_data(self, vals, flow):
+ flow.request.urlencoded_form = vals
class SetHeadersEditor(base.GridEditor):
@@ -146,7 +154,7 @@ class SetHeadersEditor(base.GridEditor):
return True
-class PathEditor(base.GridEditor):
+class PathEditor(base.FocusEditor):
# TODO: Next row on enter?
title = "Editing URL path components"
@@ -160,6 +168,12 @@ class PathEditor(base.GridEditor):
def data_out(self, data):
return [i[0] for i in data]
+ def get_data(self, flow):
+ return self.data_in(flow.request.path_components)
+
+ def set_data(self, vals, flow):
+ flow.request.path_components = self.data_out(vals)
+
class ScriptEditor(base.GridEditor):
title = "Editing scripts"
@@ -193,13 +207,19 @@ class HostPatternEditor(base.GridEditor):
return [i[0] for i in data]
-class CookieEditor(base.GridEditor):
+class CookieEditor(base.FocusEditor):
title = "Editing request Cookie header"
columns = [
col_text.Column("Name"),
col_text.Column("Value"),
]
+ def get_data(self, flow):
+ return flow.request.cookies.items(multi=True)
+
+ def set_data(self, vals, flow):
+ flow.request.cookies = vals
+
class CookieAttributeEditor(base.GridEditor):
title = "Editing Set-Cookie attributes"
@@ -221,7 +241,7 @@ class CookieAttributeEditor(base.GridEditor):
return ret
-class SetCookieEditor(base.GridEditor):
+class SetCookieEditor(base.FocusEditor):
title = "Editing response SetCookie header"
columns = [
col_text.Column("Name"),
@@ -245,3 +265,29 @@ class SetCookieEditor(base.GridEditor):
]
)
return vals
+
+ def get_data(self, flow):
+ return self.data_in(flow.response.cookies.items(multi=True))
+
+ def set_data(self, vals, flow):
+ flow.response.cookies = self.data_out(vals)
+
+
+class OptionsEditor(base.GridEditor):
+ title = None # type: str
+ columns = [
+ col_text.Column("")
+ ]
+
+ def __init__(self, master, name, vals):
+ self.name = name
+ super().__init__(master, [[i] for i in vals], self.callback)
+
+ def callback(self, vals):
+ try:
+ setattr(self.master.options, self.name, [i[0] for i in vals])
+ except exceptions.OptionsError as v:
+ signals.status_message.send(message=str(v))
+
+ def is_error(self, col, val):
+ pass
diff --git a/mitmproxy/tools/console/help.py b/mitmproxy/tools/console/help.py
index 282f374d..ec0c95d9 100644
--- a/mitmproxy/tools/console/help.py
+++ b/mitmproxy/tools/console/help.py
@@ -4,7 +4,6 @@ import urwid
from mitmproxy import flowfilter
from mitmproxy.tools.console import common
-from mitmproxy.tools.console import signals
from mitmproxy import version
@@ -15,6 +14,7 @@ footer = [
class HelpView(urwid.ListBox):
+ keyctx = "help"
def __init__(self, help_context):
self.help_context = help_context or []
@@ -84,14 +84,8 @@ class HelpView(urwid.ListBox):
return text
def keypress(self, size, key):
- key = common.shortcuts(key)
- if key == "q":
- signals.pop_view_state.send(self)
- return None
- elif key == "?":
- key = None
- elif key == "g":
+ if key == "m_start":
self.set_focus(0)
- elif key == "G":
+ elif key == "m_end":
self.set_focus(len(self.body.contents))
return urwid.ListBox.keypress(self, size, key)
diff --git a/mitmproxy/tools/console/keymap.py b/mitmproxy/tools/console/keymap.py
new file mode 100644
index 00000000..62e2dcfb
--- /dev/null
+++ b/mitmproxy/tools/console/keymap.py
@@ -0,0 +1,61 @@
+import typing
+import collections
+from mitmproxy.tools.console import commandeditor
+
+
+SupportedContexts = {
+ "chooser",
+ "commands",
+ "flowlist",
+ "flowview",
+ "global",
+ "grideditor",
+ "help",
+ "options",
+}
+
+
+Binding = collections.namedtuple("Binding", ["key", "command", "contexts"])
+
+
+class Keymap:
+ def __init__(self, master):
+ self.executor = commandeditor.CommandExecutor(master)
+ self.keys = {}
+ self.bindings = []
+
+ def add(self, key: str, command: str, contexts: typing.Sequence[str]) -> None:
+ """
+ Add a key to the key map. If context is empty, it's considered to be
+ a global binding.
+ """
+ if not contexts:
+ raise ValueError("Must specify at least one context.")
+ for c in contexts:
+ if c not in SupportedContexts:
+ raise ValueError("Unsupported context: %s" % c)
+
+ b = Binding(key=key, command=command, contexts=contexts)
+ self.bindings.append(b)
+ self.bind(b)
+
+ def bind(self, binding):
+ for c in binding.contexts:
+ d = self.keys.setdefault(c, {})
+ d[binding.key] = binding.command
+
+ def get(self, context: str, key: str) -> typing.Optional[str]:
+ if context in self.keys:
+ return self.keys[context].get(key, None)
+ return None
+
+ def handle(self, context: str, key: str) -> typing.Optional[str]:
+ """
+ Returns the key if it has not been handled, or None.
+ """
+ cmd = self.get(context, key)
+ if not cmd:
+ cmd = self.get("global", key)
+ if cmd:
+ return self.executor(cmd)
+ return key
diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py
index d0e23712..d1d470e1 100644
--- a/mitmproxy/tools/console/master.py
+++ b/mitmproxy/tools/console/master.py
@@ -9,34 +9,37 @@ import subprocess
import sys
import tempfile
import traceback
+import typing
import urwid
+from mitmproxy import ctx
from mitmproxy import addons
+from mitmproxy import command
from mitmproxy import exceptions
from mitmproxy import master
-from mitmproxy import io
from mitmproxy import log
-from mitmproxy.addons import view
+from mitmproxy import flow
from mitmproxy.addons import intercept
-from mitmproxy.tools.console import flowlist
-from mitmproxy.tools.console import flowview
-from mitmproxy.tools.console import grideditor
-from mitmproxy.tools.console import help
-from mitmproxy.tools.console import options
-from mitmproxy.tools.console import palettepicker
+from mitmproxy.addons import readfile
+from mitmproxy.addons import view
+from mitmproxy.tools.console import keymap
+from mitmproxy.tools.console import overlay
from mitmproxy.tools.console import palettes
from mitmproxy.tools.console import signals
-from mitmproxy.tools.console import statusbar
from mitmproxy.tools.console import window
+from mitmproxy import contentviews
from mitmproxy.utils import strutils
-EVENTLOG_SIZE = 10000
-
class Logger:
def log(self, evt):
signals.add_log(evt.msg, evt.level)
+ if evt.level == "alert":
+ signals.status_message.send(
+ message=str(evt.msg),
+ expire=2
+ )
class UnsupportedLog:
@@ -68,36 +71,445 @@ class UnsupportedLog:
signals.add_log(strutils.bytes_to_escaped_str(message.content), "debug")
+class ConsoleAddon:
+ """
+ An addon that exposes console-specific commands, and hooks into required
+ events.
+ """
+ def __init__(self, master):
+ self.master = master
+ self.started = False
+
+ @command.command("console.layout.options")
+ def layout_options(self) -> typing.Sequence[str]:
+ """
+ Returns the valid options for console layout. Use these by setting
+ the console_layout option.
+ """
+ return ["single", "vertical", "horizontal"]
+
+ @command.command("console.layout.cycle")
+ def layout_cycle(self) -> None:
+ """
+ Cycle through the console layout options.
+ """
+ opts = self.layout_options()
+ off = self.layout_options().index(ctx.options.console_layout)
+ ctx.options.update(
+ console_layout = opts[(off + 1) % len(opts)]
+ )
+
+ @command.command("console.panes.next")
+ def panes_next(self) -> None:
+ """
+ Go to the next layout pane.
+ """
+ self.master.window.switch()
+
+ @command.command("console.options.reset.current")
+ def options_reset_current(self) -> None:
+ """
+ Reset the current option in the options editor.
+ """
+ fv = self.master.window.current("options")
+ if not fv:
+ raise exceptions.CommandError("Not viewing options.")
+ self.master.commands.call("options.reset.one %s" % fv.current_name())
+
+ @command.command("console.nav.start")
+ def nav_start(self) -> None:
+ """
+ Go to the start of a list or scrollable.
+ """
+ self.master.inject_key("m_start")
+
+ @command.command("console.nav.end")
+ def nav_end(self) -> None:
+ """
+ Go to the end of a list or scrollable.
+ """
+ self.master.inject_key("m_end")
+
+ @command.command("console.nav.up")
+ def nav_up(self) -> None:
+ """
+ Go up.
+ """
+ self.master.inject_key("up")
+
+ @command.command("console.nav.down")
+ def nav_down(self) -> None:
+ """
+ Go down.
+ """
+ self.master.inject_key("down")
+
+ @command.command("console.nav.pageup")
+ def nav_pageup(self) -> None:
+ """
+ Go up.
+ """
+ self.master.inject_key("page up")
+
+ @command.command("console.nav.pagedown")
+ def nav_pagedown(self) -> None:
+ """
+ Go down.
+ """
+ self.master.inject_key("page down")
+
+ @command.command("console.nav.left")
+ def nav_left(self) -> None:
+ """
+ Go left.
+ """
+ self.master.inject_key("left")
+
+ @command.command("console.nav.right")
+ def nav_right(self) -> None:
+ """
+ Go right.
+ """
+ self.master.inject_key("right")
+
+ @command.command("console.choose")
+ def console_choose(
+ self, prompt: str, choices: typing.Sequence[str], *cmd: typing.Sequence[str]
+ ) -> None:
+ """
+ Prompt the user to choose from a specified list of strings, then
+ invoke another command with all occurances of {choice} replaced by
+ the choice the user made.
+ """
+ def callback(opt):
+ # We're now outside of the call context...
+ repl = " ".join(cmd)
+ repl = repl.replace("{choice}", opt)
+ try:
+ self.master.commands.call(repl)
+ except exceptions.CommandError as e:
+ signals.status_message.send(message=str(e))
+
+ self.master.overlay(
+ overlay.Chooser(self.master, prompt, choices, "", callback)
+ )
+
+ @command.command("console.choose.cmd")
+ def console_choose_cmd(
+ self, prompt: str, choicecmd: str, *cmd: typing.Sequence[str]
+ ) -> None:
+ """
+ Prompt the user to choose from a list of strings returned by a
+ command, then invoke another command with all occurances of {choice}
+ replaced by the choice the user made.
+ """
+ choices = ctx.master.commands.call_args(choicecmd, [])
+
+ def callback(opt):
+ # We're now outside of the call context...
+ repl = " ".join(cmd)
+ repl = repl.replace("{choice}", opt)
+ try:
+ self.master.commands.call(repl)
+ except exceptions.CommandError as e:
+ signals.status_message.send(message=str(e))
+
+ self.master.overlay(
+ overlay.Chooser(self.master, prompt, choices, "", callback)
+ )
+
+ @command.command("console.command")
+ def console_command(self, *partial: typing.Sequence[str]) -> None:
+ """
+ Prompt the user to edit a command with a (possilby empty) starting value.
+ """
+ signals.status_prompt_command.send(partial=" ".join(partial) + " ") # type: ignore
+
+ @command.command("console.view.commands")
+ def view_commands(self) -> None:
+ """View the commands list."""
+ self.master.switch_view("commands")
+
+ @command.command("console.view.options")
+ def view_options(self) -> None:
+ """View the options editor."""
+ self.master.switch_view("options")
+
+ @command.command("console.view.eventlog")
+ def view_eventlog(self) -> None:
+ """View the options editor."""
+ self.master.switch_view("eventlog")
+
+ @command.command("console.view.help")
+ def view_help(self) -> None:
+ """View help."""
+ self.master.switch_view("help")
+
+ @command.command("console.view.flow")
+ def view_flow(self, flow: flow.Flow) -> None:
+ """View a flow."""
+ if hasattr(flow, "request"):
+ # FIME: Also set focus?
+ self.master.switch_view("flowview")
+
+ @command.command("console.exit")
+ def exit(self) -> None:
+ """Exit mitmproxy."""
+ raise urwid.ExitMainLoop
+
+ @command.command("console.view.pop")
+ def view_pop(self) -> None:
+ """
+ Pop a view off the console stack. At the top level, this prompts the
+ user to exit mitmproxy.
+ """
+ signals.pop_view_state.send(self)
+
+ @command.command("console.bodyview")
+ def bodyview(self, f: flow.Flow, part: str) -> None:
+ """
+ Spawn an external viewer for a flow request or response body based
+ on the detected MIME type. We use the mailcap system to find the
+ correct viewier, and fall back to the programs in $PAGER or $EDITOR
+ if necessary.
+ """
+ fpart = getattr(f, part)
+ if not fpart:
+ raise exceptions.CommandError("Could not view part %s." % part)
+ t = fpart.headers.get("content-type")
+ content = fpart.get_content(strict=False)
+ if not content:
+ raise exceptions.CommandError("No content to view.")
+ self.master.spawn_external_viewer(content, t)
+
+ @command.command("console.edit.focus.options")
+ def edit_focus_options(self) -> typing.Sequence[str]:
+ return [
+ "cookies",
+ "form",
+ "path",
+ "method",
+ "query",
+ "reason",
+ "request-headers",
+ "response-headers",
+ "status_code",
+ "set-cookies",
+ "url",
+ ]
+
+ @command.command("console.edit.focus")
+ def edit_focus(self, part: str) -> None:
+ """
+ Edit the query of the current focus.
+ """
+ if part == "cookies":
+ self.master.switch_view("edit_focus_cookies")
+ elif part == "form":
+ self.master.switch_view("edit_focus_form")
+ elif part == "path":
+ self.master.switch_view("edit_focus_path")
+ elif part == "query":
+ self.master.switch_view("edit_focus_query")
+ elif part == "request-headers":
+ self.master.switch_view("edit_focus_request_headers")
+ elif part == "response-headers":
+ self.master.switch_view("edit_focus_response_headers")
+ elif part == "set-cookies":
+ self.master.switch_view("edit_focus_setcookies")
+ elif part in ["url", "method", "status_code", "reason"]:
+ self.master.commands.call(
+ "console.command flow.set @focus %s " % part
+ )
+
+ @command.command("console.flowview.mode.set")
+ def flowview_mode_set(self) -> None:
+ """
+ Set the display mode for the current flow view.
+ """
+ fv = self.master.window.current("flowview")
+ if not fv:
+ raise exceptions.CommandError("Not viewing a flow.")
+ idx = fv.body.tab_offset
+
+ def callback(opt):
+ try:
+ self.master.commands.call_args(
+ "view.setval",
+ ["@focus", "flowview_mode_%s" % idx, opt]
+ )
+ except exceptions.CommandError as e:
+ signals.status_message.send(message=str(e))
+
+ opts = [i.name.lower() for i in contentviews.views]
+ self.master.overlay(overlay.Chooser(self.master, "Mode", opts, "", callback))
+
+ @command.command("console.flowview.mode")
+ def flowview_mode(self) -> str:
+ """
+ Get the display mode for the current flow view.
+ """
+ fv = self.master.window.any("flowview")
+ if not fv:
+ raise exceptions.CommandError("Not viewing a flow.")
+ idx = fv.body.tab_offset
+ return self.master.commands.call_args(
+ "view.getval",
+ [
+ "@focus",
+ "flowview_mode_%s" % idx,
+ self.master.options.default_contentview,
+ ]
+ )
+
+ def running(self):
+ self.started = True
+
+ def update(self, flows):
+ if not flows:
+ signals.update_settings.send(self)
+ for f in flows:
+ signals.flow_change.send(self, flow=f)
+
+
+def default_keymap(km):
+ km.add(":", "console.command ''", ["global"])
+ km.add("?", "console.view.help", ["global"])
+ km.add("C", "console.view.commands", ["global"])
+ km.add("O", "console.view.options", ["global"])
+ km.add("E", "console.view.eventlog", ["global"])
+ km.add("Q", "console.exit", ["global"])
+ km.add("q", "console.view.pop", ["global"])
+ km.add("-", "console.layout.cycle", ["global"])
+ km.add("shift tab", "console.panes.next", ["global"])
+ km.add("P", "console.view.flow @focus", ["global"])
+
+ km.add("g", "console.nav.start", ["global"])
+ km.add("G", "console.nav.end", ["global"])
+ km.add("k", "console.nav.up", ["global"])
+ km.add("j", "console.nav.down", ["global"])
+ km.add("l", "console.nav.right", ["global"])
+ km.add("h", "console.nav.left", ["global"])
+ km.add(" ", "console.nav.pagedown", ["global"])
+ km.add("ctrl f", "console.nav.pagedown", ["global"])
+ km.add("ctrl b", "console.nav.pageup", ["global"])
+
+ km.add("i", "console.command set intercept=", ["global"])
+ km.add("W", "console.command set save_stream_file=", ["global"])
+ km.add("A", "flow.resume @all", ["flowlist", "flowview"])
+ km.add("a", "flow.resume @focus", ["flowlist", "flowview"])
+ km.add(
+ "b", "console.command cut.save s.content|@focus ''",
+ ["flowlist", "flowview"]
+ )
+ km.add("d", "view.remove @focus", ["flowlist", "flowview"])
+ km.add("D", "view.duplicate @focus", ["flowlist", "flowview"])
+ km.add(
+ "e",
+ "console.choose.cmd Format export.formats "
+ "console.command export.file {choice} @focus ''",
+ ["flowlist", "flowview"]
+ )
+ km.add("f", "console.command set view_filter=", ["flowlist"])
+ km.add("F", "set console_focus_follow=toggle", ["flowlist"])
+ km.add("ctrl l", "console.command cut.clip ", ["flowlist", "flowview"])
+ km.add("L", "console.command view.load ", ["flowlist"])
+ km.add("m", "flow.mark.toggle @focus", ["flowlist"])
+ km.add("M", "view.marked.toggle", ["flowlist"])
+ km.add(
+ "n",
+ "console.command view.create get https://google.com",
+ ["flowlist"]
+ )
+ km.add(
+ "o",
+ "console.choose.cmd Order view.order.options "
+ "set console_order={choice}",
+ ["flowlist"]
+ )
+ km.add("r", "replay.client @focus", ["flowlist", "flowview"])
+ km.add("S", "console.command replay.server ", ["flowlist"])
+ km.add("v", "set console_order_reversed=toggle", ["flowlist"])
+ km.add("U", "flow.mark @all false", ["flowlist"])
+ km.add("w", "console.command save.file @shown ", ["flowlist"])
+ km.add("V", "flow.revert @focus", ["flowlist", "flowview"])
+ km.add("X", "flow.kill @focus", ["flowlist"])
+ km.add("z", "view.remove @all", ["flowlist"])
+ km.add("Z", "view.remove @hidden", ["flowlist"])
+ km.add("|", "console.command script.run @focus ", ["flowlist", "flowview"])
+ km.add("enter", "console.view.flow @focus", ["flowlist"])
+
+ km.add(
+ "e",
+ "console.choose.cmd Part console.edit.focus.options "
+ "console.edit.focus {choice}",
+ ["flowview"]
+ )
+ km.add("f", "view.setval.toggle @focus fullcontents", ["flowview"])
+ km.add("w", "console.command save.file @focus ", ["flowview"])
+ km.add(" ", "view.focus.next", ["flowview"])
+ km.add(
+ "o",
+ "console.choose.cmd Order view.order.options "
+ "set console_order={choice}",
+ ["flowlist"]
+ )
+
+ km.add(
+ "v",
+ "console.choose \"View Part\" request,response "
+ "console.bodyview @focus {choice}",
+ ["flowview"]
+ )
+ km.add("p", "view.focus.prev", ["flowview"])
+ km.add("m", "console.flowview.mode.set", ["flowview"])
+ km.add("tab", "console.nav.right", ["flowview"])
+ km.add(
+ "z",
+ "console.choose \"Part\" request,response "
+ "flow.encode.toggle @focus {choice}",
+ ["flowview"]
+ )
+
+ km.add("L", "console.command options.load ", ["options"])
+ km.add("S", "console.command options.save ", ["options"])
+ km.add("D", "options.reset", ["options"])
+ km.add("d", "console.options.reset.current", ["options"])
+
+
class ConsoleMaster(master.Master):
- palette = []
def __init__(self, options, server):
super().__init__(options, server)
self.view = view.View() # type: view.View
- self.view.sig_view_update.connect(signals.flow_change.send)
self.stream_path = None
# This line is just for type hinting
self.options = self.options # type: Options
+ self.keymap = keymap.Keymap(self)
+ default_keymap(self.keymap)
self.options.errored.connect(self.options_error)
- self.logbuffer = urwid.SimpleListWalker([])
-
self.view_stack = []
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(Logger())
self.addons.add(*addons.default_addons())
- self.addons.add(intercept.Intercept(), self.view, UnsupportedLog())
+ self.addons.add(
+ intercept.Intercept(),
+ self.view,
+ UnsupportedLog(),
+ readfile.ReadFile(),
+ ConsoleAddon(self),
+ )
def sigint_handler(*args, **kwargs):
self.prompt_for_exit()
signal.signal(signal.SIGINT, sigint_handler)
+ self.window = None
+
def __setattr__(self, name, value):
self.__dict__[name] = value
signals.update_settings.send(self)
@@ -122,78 +534,16 @@ class ConsoleMaster(master.Master):
def sig_add_log(self, sender, e, level):
if self.options.verbosity < log.log_tier(level):
return
-
if level in ("error", "warn"):
signals.status_message.send(
message = "{}: {}".format(level.title(), e)
)
- e = urwid.Text((level, str(e)))
- else:
- e = urwid.Text(str(e))
- self.logbuffer.append(e)
- if len(self.logbuffer) > EVENTLOG_SIZE:
- self.logbuffer.pop(0)
- if self.options.console_focus_follow:
- self.logbuffer.set_focus(len(self.logbuffer) - 1)
def sig_call_in(self, sender, seconds, callback, args=()):
def cb(*_):
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]
- else:
- self.prompt_for_exit()
-
- 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()
-
- def run_script_once(self, command, f):
- sc = self.addons.get("scriptloader")
- try:
- with self.handlecontext():
- sc.run_once(command, [f])
- except ValueError as e:
- signals.add_log("Input error: %s" % e, "warn")
-
- def toggle_eventlog(self):
- self.options.console_eventlog = not self.options.console_eventlog
- self.view_flowlist()
- signals.replace_view_state.send(self)
-
- def _readflows(self, path):
- """
- Utitility function that reads a list of flows
- or prints an error to the UI if that fails.
- Returns
- - None, if there was an error.
- - a list of flows, otherwise.
- """
- try:
- return io.read_flows_from_paths(path)
- except exceptions.FlowReadException as e:
- signals.status_message.send(message=str(e))
-
def spawn_editor(self, data):
text = not isinstance(data, bytes)
fd, name = tempfile.mkstemp('', "mproxy", text=text)
@@ -269,27 +619,28 @@ class ConsoleMaster(master.Master):
self.loop.draw_screen()
self.loop.set_alarm_in(0.01, self.ticker)
+ def inject_key(self, key):
+ self.loop.process_input([key])
+
def run(self):
self.ui = urwid.raw_display.Screen()
self.ui.set_terminal_properties(256)
self.set_palette(self.options, None)
self.options.subscribe(
self.set_palette,
- ["palette", "palette_transparent"]
+ ["console_palette", "console_palette_transparent"]
)
self.loop = urwid.MainLoop(
urwid.SolidFill("x"),
screen = self.ui,
handle_mouse = self.options.console_mouse,
)
- self.ab = statusbar.ActionBar()
- self.loop.set_alarm_in(0.01, self.ticker)
+ self.window = window.Window(self)
+ self.loop.widget = self.window
+ self.window.refresh()
- self.loop.set_alarm_in(
- 0.0001,
- lambda *args: self.view_flowlist()
- )
+ self.loop.set_alarm_in(0.01, self.ticker)
self.start()
try:
@@ -309,118 +660,12 @@ class ConsoleMaster(master.Master):
def shutdown(self):
raise urwid.ExitMainLoop
- def view_help(self, helpctx):
- signals.push_view_state.send(
- self,
- window = window.Window(
- self,
- help.HelpView(helpctx),
- None,
- statusbar.StatusBar(self, help.footer),
- None
- )
- )
+ def overlay(self, widget, **kwargs):
+ self.window.set_overlay(widget, **kwargs)
- def view_options(self):
- for i in self.view_stack:
- if isinstance(i["body"], options.Options):
- return
- signals.push_view_state.send(
- self,
- window = window.Window(
- self,
- options.Options(self),
- None,
- statusbar.StatusBar(self, options.footer),
- options.help_context,
- )
- )
-
- def view_palette_picker(self):
- signals.push_view_state.send(
- self,
- window = window.Window(
- self,
- palettepicker.PalettePicker(self),
- None,
- statusbar.StatusBar(self, palettepicker.footer),
- palettepicker.help_context,
- )
- )
-
- def view_grideditor(self, ge):
- signals.push_view_state.send(
- self,
- window = window.Window(
- self,
- ge,
- None,
- statusbar.StatusBar(self, grideditor.base.FOOTER),
- ge.make_help()
- )
- )
-
- def view_flowlist(self):
- if self.ui.started:
- self.ui.clear()
-
- if self.options.console_eventlog:
- body = flowlist.BodyPile(self)
- else:
- body = flowlist.FlowListBox(self)
-
- signals.push_view_state.send(
- self,
- window = window.Window(
- self,
- body,
- None,
- statusbar.StatusBar(self, flowlist.footer),
- flowlist.help_context
- )
- )
-
- def view_flow(self, flow, tab_offset=0):
- self.view.focus.flow = flow
- signals.push_view_state.send(
- self,
- window = window.Window(
- self,
- flowview.FlowView(self, self.view, flow, tab_offset),
- flowview.FlowViewHeader(self, flow),
- statusbar.StatusBar(self, flowview.footer),
- flowview.help_context
- )
- )
-
- def _write_flows(self, path, flows):
- with open(path, "wb") as f:
- fw = io.FlowWriter(f)
- for i in flows:
- fw.add(i)
-
- def save_one_flow(self, path, flow):
- return self._write_flows(path, [flow])
-
- def save_flows(self, path):
- return self._write_flows(path, self.view)
-
- def load_flows_callback(self, path):
- ret = self.load_flows_path(path)
- return ret or "Flows loaded from %s" % path
-
- def load_flows_path(self, path):
- reterr = None
- try:
- master.Master.load_flows_file(self, path)
- except exceptions.FlowReadException as e:
- reterr = str(e)
- signals.flowlist_change.send(self)
- return reterr
+ def switch_view(self, name):
+ self.window.push(name)
def quit(self, a):
if a != "n":
self.shutdown()
-
- def clear_events(self):
- self.logbuffer[:] = []
diff --git a/mitmproxy/tools/console/options.py b/mitmproxy/tools/console/options.py
index 79bb53c2..fee61fe5 100644
--- a/mitmproxy/tools/console/options.py
+++ b/mitmproxy/tools/console/options.py
@@ -1,28 +1,39 @@
import urwid
+import blinker
+import textwrap
+import pprint
+from typing import Optional, Sequence
-from mitmproxy import contentviews
+from mitmproxy import exceptions
from mitmproxy import optmanager
from mitmproxy.tools.console import common
-from mitmproxy.tools.console import grideditor
-from mitmproxy.tools.console import select
from mitmproxy.tools.console import signals
+from mitmproxy.tools.console import overlay
+
+HELP_HEIGHT = 5
+
+
+def can_edit_inplace(opt):
+ if opt.choices:
+ return False
+ if opt.typespec in [str, int, Optional[str], Optional[int]]:
+ return True
-from mitmproxy.addons import replace
-from mitmproxy.addons import setheaders
footer = [
- ('heading_key', "enter/space"), ":toggle ",
- ('heading_key', "C"), ":clear all ",
- ('heading_key', "W"), ":save ",
+ ('heading_key', "enter"), ":edit ",
+ ('heading_key', "?"), ":help ",
]
def _mkhelp():
text = []
keys = [
- ("enter/space", "activate option"),
- ("C", "clear all options"),
- ("w", "save options"),
+ ("enter", "edit option"),
+ ("D", "reset all to defaults"),
+ ("d", "reset this option to default"),
+ ("l", "load options from file"),
+ ("w", "save options to file"),
]
text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
return text
@@ -31,251 +42,258 @@ def _mkhelp():
help_context = _mkhelp()
-def checker(opt, options):
- def _check():
- return options.has_changed(opt)
- return _check
+def fcol(s, width, attr):
+ s = str(s)
+ return (
+ "fixed",
+ width,
+ urwid.Text((attr, s))
+ )
-class Options(urwid.WidgetWrap):
+option_focus_change = blinker.Signal()
- def __init__(self, master):
- self.master = master
- self.lb = select.Select(
- [
- select.Heading("Traffic Manipulation"),
- select.Option(
- "Header Set Patterns",
- "H",
- checker("setheaders", master.options),
- self.setheaders
- ),
- select.Option(
- "Ignore Patterns",
- "I",
- checker("ignore_hosts", master.options),
- self.ignore_hosts
- ),
- select.Option(
- "Replacement Patterns",
- "R",
- checker("replacements", master.options),
- self.replacepatterns
- ),
- select.Option(
- "Scripts",
- "S",
- checker("scripts", master.options),
- self.scripts
- ),
- select.Heading("Interface"),
- select.Option(
- "Default Display Mode",
- "M",
- checker("default_contentview", master.options),
- self.default_displaymode
- ),
- select.Option(
- "Palette",
- "P",
- checker("console_palette", master.options),
- self.palette
- ),
- select.Option(
- "Show Host",
- "w",
- checker("showhost", master.options),
- master.options.toggler("showhost")
- ),
+class OptionItem(urwid.WidgetWrap):
+ def __init__(self, walker, opt, focused, namewidth, editing):
+ self.walker, self.opt, self.focused = walker, opt, focused
+ self.namewidth = namewidth
+ self.editing = editing
+ super().__init__(None)
+ self._w = self.get_widget()
- select.Heading("Network"),
- select.Option(
- "Upstream Certs",
- "U",
- checker("upstream_cert", master.options),
- master.options.toggler("upstream_cert")
- ),
- select.Option(
- "TCP Proxying",
- "T",
- checker("tcp_hosts", master.options),
- self.tcp_hosts
- ),
- select.Option(
- "Don't Verify SSL/TLS Certificates",
- "V",
- checker("ssl_insecure", master.options),
- master.options.toggler("ssl_insecure")
- ),
+ def get_widget(self):
+ val = self.opt.current()
+ if self.opt.typespec == bool:
+ displayval = "true" if val else "false"
+ elif not val:
+ displayval = ""
+ elif self.opt.typespec == Sequence[str]:
+ displayval = pprint.pformat(val, indent=1)
+ else:
+ displayval = str(val)
- select.Heading("Utility"),
- select.Option(
- "Anti-Cache",
- "a",
- checker("anticache", master.options),
- master.options.toggler("anticache")
- ),
- select.Option(
- "Anti-Compression",
- "o",
- checker("anticomp", master.options),
- master.options.toggler("anticomp")
- ),
- select.Option(
- "Kill Extra",
- "x",
- checker("replay_kill_extra", master.options),
- master.options.toggler("replay_kill_extra")
- ),
- select.Option(
- "No Refresh",
- "f",
- checker("refresh_server_playback", master.options),
- master.options.toggler("refresh_server_playback")
- ),
- select.Option(
- "Sticky Auth",
- "A",
- checker("stickyauth", master.options),
- self.sticky_auth
+ changed = self.walker.master.options.has_changed(self.opt.name)
+ if self.focused:
+ valstyle = "option_active_selected" if changed else "option_selected"
+ else:
+ valstyle = "option_active" if changed else "text"
+
+ if self.editing:
+ valw = urwid.Edit(edit_text=displayval)
+ else:
+ valw = urwid.AttrMap(
+ urwid.Padding(
+ urwid.Text([(valstyle, displayval)])
),
- select.Option(
- "Sticky Cookies",
- "t",
- checker("stickycookie", master.options),
- self.sticky_cookie
+ valstyle
+ )
+
+ return urwid.Columns(
+ [
+ (
+ self.namewidth,
+ urwid.Text([("title", self.opt.name.ljust(self.namewidth))])
),
- ]
- )
- title = urwid.Text("Options")
- title = urwid.Padding(title, align="left", width=("relative", 100))
- title = urwid.AttrWrap(title, "heading")
- w = urwid.Frame(
- self.lb,
- header = title
+ valw
+ ],
+ dividechars=2,
+ focus_column=1
)
- super().__init__(w)
- self.master.loop.widget.footer.update("")
- signals.update_settings.connect(self.sig_update_settings)
- master.options.changed.connect(self.sig_update_settings)
+ def get_edit_text(self):
+ return self._w[1].get_edit_text()
- def sig_update_settings(self, sender, updated=None):
- self.lb.walker._modified()
+ def selectable(self):
+ return True
def keypress(self, size, key):
- if key == "C":
- self.clearall()
- return None
- if key == "W":
- self.save()
- return None
- return super().keypress(size, key)
+ if self.editing:
+ self._w[1].keypress(size, key)
+ return
+ return key
- def do_save(self, path):
- optmanager.save(self.master.options, path)
- return "Saved"
- def save(self):
- signals.status_prompt_path.send(
- prompt = "Save options to file",
- callback = self.do_save
- )
+class OptionListWalker(urwid.ListWalker):
+ def __init__(self, master):
+ self.master = master
- def clearall(self):
- self.master.options.reset()
- signals.update_settings.send(self)
- signals.status_message.send(
- message = "Options cleared",
- expire = 1
- )
+ self.index = 0
+ self.focusobj = None
- def setheaders(self):
- data = []
- for d in self.master.options.setheaders:
- if isinstance(d, str):
- data.append(setheaders.parse_setheader(d))
- else:
- data.append(d)
- self.master.view_grideditor(
- grideditor.SetHeadersEditor(
- self.master,
- data,
- self.master.options.setter("setheaders")
- )
- )
+ self.opts = sorted(master.options.keys())
+ self.maxlen = max(len(i) for i in self.opts)
+ self.editing = False
+ self.set_focus(0)
+ self.master.options.changed.connect(self.sig_mod)
- def tcp_hosts(self):
- self.master.view_grideditor(
- grideditor.HostPatternEditor(
- self.master,
- self.master.options.tcp_hosts,
- self.master.options.setter("tcp_hosts")
- )
- )
+ def sig_mod(self, *args, **kwargs):
+ self._modified()
+ self.set_focus(self.index)
- def ignore_hosts(self):
- self.master.view_grideditor(
- grideditor.HostPatternEditor(
- self.master,
- self.master.options.ignore_hosts,
- self.master.options.setter("ignore_hosts")
- )
- )
+ def start_editing(self):
+ self.editing = True
+ self.focus_obj = self._get(self.index, True)
+ self._modified()
- def replacepatterns(self):
- data = []
- for d in self.master.options.replacements:
- if isinstance(d, str):
- data.append(replace.parse_hook(d))
- else:
- data.append(d)
- self.master.view_grideditor(
- grideditor.ReplaceEditor(
- self.master,
- data,
- self.master.options.setter("replacements")
- )
- )
+ def stop_editing(self):
+ self.editing = False
+ self.focus_obj = self._get(self.index, False)
+ self._modified()
- def scripts(self):
- def edit_scripts(scripts):
- self.master.options.scripts = [x[0] for x in scripts]
- self.master.view_grideditor(
- grideditor.ScriptEditor(
- self.master,
- [[i] for i in self.master.options.scripts],
- edit_scripts
- )
- )
+ def get_edit_text(self):
+ return self.focus_obj.get_edit_text()
- def default_displaymode(self):
- signals.status_prompt_onekey.send(
- prompt = "Global default display mode",
- keys = contentviews.view_prompts,
- callback = self.change_default_display_mode
+ def _get(self, pos, editing):
+ name = self.opts[pos]
+ opt = self.master.options._options[name]
+ return OptionItem(
+ self, opt, pos == self.index, self.maxlen, editing
)
- def change_default_display_mode(self, t):
- v = contentviews.get_by_shortcut(t)
- self.master.options.default_contentview = v.name
- if self.master.view.focus.flow:
- signals.flow_change.send(self, flow = self.master.view.focus.flow)
-
- def sticky_auth(self):
- signals.status_prompt.send(
- prompt = "Sticky auth filter",
- text = self.master.options.stickyauth,
- callback = self.master.options.setter("stickyauth")
+ def get_focus(self):
+ return self.focus_obj, self.index
+
+ def set_focus(self, index):
+ self.editing = False
+ name = self.opts[index]
+ opt = self.master.options._options[name]
+ self.index = index
+ self.focus_obj = self._get(self.index, self.editing)
+ option_focus_change.send(opt.help)
+
+ def get_next(self, pos):
+ if pos >= len(self.opts) - 1:
+ return None, None
+ pos = pos + 1
+ return self._get(pos, False), pos
+
+ def get_prev(self, pos):
+ pos = pos - 1
+ if pos < 0:
+ return None, None
+ return self._get(pos, False), pos
+
+
+class OptionsList(urwid.ListBox):
+ def __init__(self, master):
+ self.master = master
+ self.walker = OptionListWalker(master)
+ super().__init__(self.walker)
+
+ def save_config(self, path):
+ try:
+ optmanager.save(self.master.options, path)
+ except exceptions.OptionsError as e:
+ signals.status_message.send(message=str(e))
+
+ def keypress(self, size, key):
+ if self.walker.editing:
+ if key == "enter":
+ foc, idx = self.get_focus()
+ v = self.walker.get_edit_text()
+ try:
+ d = self.master.options.parse_setval(foc.opt.name, v)
+ self.master.options.update(**{foc.opt.name: d})
+ except exceptions.OptionsError as v:
+ signals.status_message.send(message=str(v))
+ self.walker.stop_editing()
+ elif key == "esc":
+ self.walker.stop_editing()
+ else:
+ if key == "m_start":
+ self.set_focus(0)
+ self.walker._modified()
+ elif key == "m_end":
+ self.set_focus(len(self.walker.opts) - 1)
+ self.walker._modified()
+ elif key == "enter":
+ foc, idx = self.get_focus()
+ if foc.opt.typespec == bool:
+ self.master.options.toggler(foc.opt.name)()
+ # Bust the focus widget cache
+ self.set_focus(self.walker.index)
+ elif can_edit_inplace(foc.opt):
+ self.walker.start_editing()
+ self.walker._modified()
+ elif foc.opt.choices:
+ self.master.overlay(
+ overlay.Chooser(
+ self.master,
+ foc.opt.name,
+ foc.opt.choices,
+ foc.opt.current(),
+ self.master.options.setter(foc.opt.name)
+ )
+ )
+ elif foc.opt.typespec == Sequence[str]:
+ self.master.overlay(
+ overlay.OptionsOverlay(
+ self.master,
+ foc.opt.name,
+ foc.opt.current(),
+ HELP_HEIGHT + 5
+ ),
+ valign="top"
+ )
+ else:
+ raise NotImplementedError()
+ return super().keypress(size, key)
+
+
+class OptionHelp(urwid.Frame):
+ def __init__(self, master):
+ self.master = master
+ super().__init__(self.widget(""))
+ self.set_active(False)
+ option_focus_change.connect(self.sig_mod)
+
+ def set_active(self, val):
+ h = urwid.Text("Option Help")
+ style = "heading" if val else "heading_inactive"
+ self.header = urwid.AttrWrap(h, style)
+
+ def widget(self, txt):
+ cols, _ = self.master.ui.get_cols_rows()
+ return urwid.ListBox(
+ [urwid.Text(i) for i in textwrap.wrap(txt, cols)]
)
- def sticky_cookie(self):
- signals.status_prompt.send(
- prompt = "Sticky cookie filter",
- text = self.master.options.stickycookie,
- callback = self.master.options.setter("stickycookie")
+ def sig_mod(self, txt):
+ self.set_body(self.widget(txt))
+
+
+class Options(urwid.Pile):
+ keyctx = "options"
+
+ def __init__(self, master):
+ oh = OptionHelp(master)
+ self.optionslist = OptionsList(master)
+ super().__init__(
+ [
+ self.optionslist,
+ (HELP_HEIGHT, oh),
+ ]
)
+ self.master = master
+
+ def current_name(self):
+ foc, idx = self.optionslist.get_focus()
+ return foc.opt.name
+
+ def keypress(self, size, key):
+ if key == "tab":
+ self.focus_position = (
+ self.focus_position + 1
+ ) % len(self.widget_list)
+ self.widget_list[1].set_active(self.focus_position == 1)
+ key = None
- def palette(self):
- self.master.view_palette_picker()
+ # This is essentially a copypasta from urwid.Pile's keypress handler.
+ # So much for "closed for modification, but open for extension".
+ item_rows = None
+ if len(size) == 2:
+ item_rows = self.get_item_rows(size, focus = True)
+ i = self.widget_list.index(self.focus_item)
+ tsize = self.get_item_size(size, i, True, item_rows)
+ return self.focus_item.keypress(tsize, key)
diff --git a/mitmproxy/tools/console/overlay.py b/mitmproxy/tools/console/overlay.py
new file mode 100644
index 00000000..abfb3909
--- /dev/null
+++ b/mitmproxy/tools/console/overlay.py
@@ -0,0 +1,144 @@
+import math
+
+import urwid
+
+from mitmproxy.tools.console import common
+from mitmproxy.tools.console import signals
+from mitmproxy.tools.console import grideditor
+
+
+class SimpleOverlay(urwid.Overlay):
+ keyctx = "overlay"
+
+ def __init__(self, master, widget, parent, width, valign="middle"):
+ self.widget = widget
+ self.master = master
+ super().__init__(
+ widget,
+ parent,
+ align="center",
+ width=width,
+ valign=valign,
+ height="pack"
+ )
+
+ def keypress(self, size, key):
+ key = super().keypress(size, key)
+ if key == "esc":
+ signals.pop_view_state.send(self)
+ if key == "?":
+ self.master.view_help(self.widget.make_help())
+ else:
+ return key
+
+
+class Choice(urwid.WidgetWrap):
+ def __init__(self, txt, focus, current):
+ if current:
+ s = "option_active_selected" if focus else "option_active"
+ else:
+ s = "option_selected" if focus else "text"
+ return super().__init__(
+ urwid.AttrWrap(
+ urwid.Padding(urwid.Text(txt)),
+ s,
+ )
+ )
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ return key
+
+
+class ChooserListWalker(urwid.ListWalker):
+ def __init__(self, choices, current):
+ self.index = 0
+ self.choices = choices
+ self.current = current
+
+ def _get(self, idx, focus):
+ c = self.choices[idx]
+ return Choice(c, focus, c == self.current)
+
+ def set_focus(self, index):
+ self.index = index
+
+ def get_focus(self):
+ return self._get(self.index, True), self.index
+
+ def get_next(self, pos):
+ if pos >= len(self.choices) - 1:
+ return None, None
+ pos = pos + 1
+ return self._get(pos, False), pos
+
+ def get_prev(self, pos):
+ pos = pos - 1
+ if pos < 0:
+ return None, None
+ return self._get(pos, False), pos
+
+
+class Chooser(urwid.WidgetWrap):
+ def __init__(self, master, title, choices, current, callback):
+ self.master = master
+ self.choices = choices
+ self.callback = callback
+ choicewidth = max([len(i) for i in choices])
+ self.width = max(choicewidth, len(title)) + 5
+ self.walker = ChooserListWalker(choices, current)
+ super().__init__(
+ urwid.AttrWrap(
+ urwid.LineBox(
+ urwid.BoxAdapter(
+ urwid.ListBox(self.walker),
+ len(choices)
+ ),
+ title= title
+ ),
+ "background"
+ )
+ )
+
+ def selectable(self):
+ return True
+
+ def keypress(self, size, key):
+ key = self.master.keymap.handle("chooser", key)
+ if key == "enter":
+ self.callback(self.choices[self.walker.index])
+ signals.pop_view_state.send(self)
+ return super().keypress(size, key)
+
+ def make_help(self):
+ text = []
+ keys = [
+ ("enter", "choose option"),
+ ("esc", "exit chooser"),
+ ]
+ text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
+ return text
+
+
+class OptionsOverlay(urwid.WidgetWrap):
+ def __init__(self, master, name, vals, vspace):
+ """
+ vspace: how much vertical space to keep clear
+ """
+ cols, rows = master.ui.get_cols_rows()
+ self.ge = grideditor.OptionsEditor(master, name, vals)
+ super().__init__(
+ urwid.AttrWrap(
+ urwid.LineBox(
+ urwid.BoxAdapter(self.ge, rows - vspace),
+ title=name
+ ),
+ "background"
+ )
+ )
+ self.width = math.ceil(cols * 0.8)
+
+ def make_help(self):
+ return self.ge.make_help()
diff --git a/mitmproxy/tools/console/palettepicker.py b/mitmproxy/tools/console/palettepicker.py
deleted file mode 100644
index 1f238b0d..00000000
--- a/mitmproxy/tools/console/palettepicker.py
+++ /dev/null
@@ -1,78 +0,0 @@
-import urwid
-
-from mitmproxy.tools.console import common
-from mitmproxy.tools.console import palettes
-from mitmproxy.tools.console import select
-
-footer = [
- ('heading_key', "enter/space"), ":select",
-]
-
-
-def _mkhelp():
- text = []
- keys = [
- ("enter/space", "select"),
- ]
- text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
- return text
-
-
-help_context = _mkhelp()
-
-
-class PalettePicker(urwid.WidgetWrap):
-
- def __init__(self, master):
- self.master = master
- low, high = [], []
- for k, v in palettes.palettes.items():
- if v.high:
- high.append(k)
- else:
- low.append(k)
- high.sort()
- low.sort()
-
- options = [
- select.Heading("High Colour")
- ]
-
- def mkopt(name):
- return select.Option(
- i,
- None,
- lambda: self.master.options.console_palette == name,
- lambda: setattr(self.master.options, "console_palette", name)
- )
-
- for i in high:
- options.append(mkopt(i))
- options.append(select.Heading("Low Colour"))
- for i in low:
- options.append(mkopt(i))
-
- options.extend(
- [
- select.Heading("Options"),
- select.Option(
- "Transparent",
- "T",
- lambda: master.options.console_palette_transparent,
- master.options.toggler("console_palette_transparent")
- )
- ]
- )
-
- self.lb = select.Select(options)
- title = urwid.Text("Palettes")
- title = urwid.Padding(title, align="left", width=("relative", 100))
- title = urwid.AttrWrap(title, "heading")
- self._w = urwid.Frame(
- self.lb,
- header = title
- )
- master.options.changed.connect(self.sig_options_changed)
-
- def sig_options_changed(self, options, updated):
- self.lb.walker._modified()
diff --git a/mitmproxy/tools/console/palettes.py b/mitmproxy/tools/console/palettes.py
index 7b15f98f..7fbdcfd8 100644
--- a/mitmproxy/tools/console/palettes.py
+++ b/mitmproxy/tools/console/palettes.py
@@ -1,3 +1,4 @@
+import typing # noqa
# Low-color themes should ONLY use the standard foreground and background
# colours listed here:
#
@@ -32,7 +33,7 @@ class Palette:
# Grid Editor
'focusfield', 'focusfield_error', 'field_error', 'editfield',
]
- high = None
+ high = None # type: typing.Mapping[str, typing.Sequence[str]]
def palette(self, transparent):
l = []
diff --git a/mitmproxy/tools/console/searchable.py b/mitmproxy/tools/console/searchable.py
index 55c5218a..f2bb5612 100644
--- a/mitmproxy/tools/console/searchable.py
+++ b/mitmproxy/tools/console/searchable.py
@@ -16,10 +16,9 @@ class Highlight(urwid.AttrMap):
class Searchable(urwid.ListBox):
- def __init__(self, view, contents):
+ def __init__(self, contents):
self.walker = urwid.SimpleFocusListWalker(contents)
urwid.ListBox.__init__(self, self.walker)
- self.view = view
self.search_offset = 0
self.current_highlight = None
self.search_term = None
@@ -36,10 +35,10 @@ class Searchable(urwid.ListBox):
self.find_next(False)
elif key == "N":
self.find_next(True)
- elif key == "g":
+ elif key == "m_start":
self.set_focus(0)
self.walker._modified()
- elif key == "G":
+ elif key == "m_end":
self.set_focus(len(self.walker) - 1)
self.walker._modified()
else:
diff --git a/mitmproxy/tools/console/select.py b/mitmproxy/tools/console/select.py
index a990dff8..f7e5d950 100644
--- a/mitmproxy/tools/console/select.py
+++ b/mitmproxy/tools/console/select.py
@@ -113,7 +113,6 @@ class Select(urwid.ListBox):
if key == "enter" or key == " ":
self.get_focus()[0].option.activate()
return None
- key = common.shortcuts(key)
if key in self.keymap:
self.keymap[key].activate()
self.set_focus(self.options.index(self.keymap[key]))
diff --git a/mitmproxy/tools/console/signals.py b/mitmproxy/tools/console/signals.py
index cb71c5c1..5cbbd875 100644
--- a/mitmproxy/tools/console/signals.py
+++ b/mitmproxy/tools/console/signals.py
@@ -24,12 +24,18 @@ status_prompt_path = blinker.Signal()
# Prompt for a single keystroke
status_prompt_onekey = blinker.Signal()
+# Prompt for a command
+status_prompt_command = blinker.Signal()
+
# Call a callback in N seconds
call_in = blinker.Signal()
# Focus the body, footer or header of the main window
focus = blinker.Signal()
+# Set the mini help text in the footer of the main window
+footer_help = blinker.Signal()
+
# Fired when settings change
update_settings = blinker.Signal()
@@ -42,4 +48,3 @@ 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()
diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py
index 3e524972..7e471b90 100644
--- a/mitmproxy/tools/console/statusbar.py
+++ b/mitmproxy/tools/console/statusbar.py
@@ -5,7 +5,8 @@ import urwid
from mitmproxy.tools.console import common
from mitmproxy.tools.console import pathedit
from mitmproxy.tools.console import signals
-from mitmproxy.utils import human
+from mitmproxy.tools.console import commandeditor
+import mitmproxy.tools.console.master # noqa
class PromptPath:
@@ -32,13 +33,15 @@ class PromptStub:
class ActionBar(urwid.WidgetWrap):
- def __init__(self):
+ def __init__(self, master):
+ self.master = master
urwid.WidgetWrap.__init__(self, None)
self.clear()
signals.status_message.connect(self.sig_message)
signals.status_prompt.connect(self.sig_prompt)
signals.status_prompt_path.connect(self.sig_path_prompt)
signals.status_prompt_onekey.connect(self.sig_prompt_onekey)
+ signals.status_prompt_command.connect(self.sig_prompt_command)
self.last_path = ""
@@ -66,6 +69,11 @@ class ActionBar(urwid.WidgetWrap):
self._w = urwid.Edit(self.prep_prompt(prompt), text or "")
self.prompting = PromptStub(callback, args)
+ def sig_prompt_command(self, sender, partial=""):
+ signals.focus.send(self, section="footer")
+ self._w = commandeditor.CommandEdit(partial)
+ self.prompting = commandeditor.CommandExecutor(self.master)
+
def sig_path_prompt(self, sender, prompt, callback, args=()):
signals.focus.send(self, section="footer")
self._w = pathedit.PathEdit(
@@ -135,23 +143,32 @@ class ActionBar(urwid.WidgetWrap):
class StatusBar(urwid.WidgetWrap):
+ keyctx = ""
- def __init__(self, master: "mitmproxy.console.master.ConsoleMaster", helptext):
+ def __init__(
+ self, master: "mitmproxy.tools.console.master.ConsoleMaster", helptext
+ ) -> None:
self.master = master
self.helptext = helptext
self.ib = urwid.WidgetWrap(urwid.Text(""))
- super().__init__(urwid.Pile([self.ib, self.master.ab]))
+ self.ab = ActionBar(self.master)
+ super().__init__(urwid.Pile([self.ib, self.ab]))
signals.update_settings.connect(self.sig_update)
signals.flowlist_change.connect(self.sig_update)
+ signals.footer_help.connect(self.sig_footer_help)
master.options.changed.connect(self.sig_update)
master.view.focus.sig_change.connect(self.sig_update)
self.redraw()
+ def sig_footer_help(self, sender, helptext):
+ self.helptext = helptext
+ self.redraw()
+
def sig_update(self, sender, updated=None):
self.redraw()
def keypress(self, *args, **kwargs):
- return self.master.ab.keypress(*args, **kwargs)
+ return self.ab.keypress(*args, **kwargs)
def get_status(self):
r = []
@@ -187,10 +204,10 @@ class StatusBar(urwid.WidgetWrap):
r.append("[")
r.append(("heading_key", "i"))
r.append(":%s]" % self.master.options.intercept)
- if self.master.options.filter:
+ if self.master.options.view_filter:
r.append("[")
r.append(("heading_key", "f"))
- r.append(":%s]" % self.master.options.filter)
+ r.append(":%s]" % self.master.options.view_filter)
if self.master.options.stickycookie:
r.append("[")
r.append(("heading_key", "t"))
@@ -203,7 +220,7 @@ class StatusBar(urwid.WidgetWrap):
r.append("[")
r.append(("heading_key", "M"))
r.append(":%s]" % self.master.options.default_contentview)
- if self.master.options.console_order:
+ if self.master.options.has_changed("console_order"):
r.append("[")
r.append(("heading_key", "o"))
r.append(":%s]" % self.master.options.console_order)
@@ -224,11 +241,7 @@ class StatusBar(urwid.WidgetWrap):
if self.master.options.console_focus_follow:
opts.append("following")
if self.master.options.stream_large_bodies:
- opts.append(
- "stream:%s" % human.pretty_size(
- self.master.options.stream_large_bodies
- )
- )
+ opts.append(self.master.options.stream_large_bodies)
if opts:
r.append("[%s]" % (":".join(opts)))
@@ -240,8 +253,8 @@ class StatusBar(urwid.WidgetWrap):
r.append(("heading_key", "s"))
r.append("cripts:%s]" % len(self.master.options.scripts))
- if self.master.options.streamfile:
- r.append("[W:%s]" % self.master.options.streamfile)
+ if self.master.options.save_stream_file:
+ r.append("[W:%s]" % self.master.options.save_stream_file)
return r
@@ -285,10 +298,5 @@ class StatusBar(urwid.WidgetWrap):
]), "heading")
self.ib._w = status
- def update(self, text):
- self.helptext = text
- self.redraw()
- self.master.loop.draw_screen()
-
def selectable(self):
return True
diff --git a/mitmproxy/tools/console/tabs.py b/mitmproxy/tools/console/tabs.py
index a2d5e719..93d6909e 100644
--- a/mitmproxy/tools/console/tabs.py
+++ b/mitmproxy/tools/console/tabs.py
@@ -27,6 +27,7 @@ class Tabs(urwid.WidgetWrap):
self.tab_offset = tab_offset
self.tabs = tabs
self.show()
+ self._w = urwid.Pile([])
def change_tab(self, offset):
self.tab_offset = offset
@@ -34,13 +35,16 @@ class Tabs(urwid.WidgetWrap):
def keypress(self, size, key):
n = len(self.tabs)
- if key in ["tab", "l"]:
+ if key == "right":
self.change_tab((self.tab_offset + 1) % n)
- elif key == "h":
+ elif key == "left":
self.change_tab((self.tab_offset - 1) % n)
return self._w.keypress(size, key)
def show(self):
+ if not self.tabs:
+ return
+
headers = []
for i in range(len(self.tabs)):
txt = self.tabs[i][0]()
diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py
index 1c962e2f..ea5b7f3b 100644
--- a/mitmproxy/tools/console/window.py
+++ b/mitmproxy/tools/console/window.py
@@ -1,24 +1,206 @@
import urwid
-
from mitmproxy.tools.console import signals
+from mitmproxy.tools.console import statusbar
+from mitmproxy.tools.console import flowlist
+from mitmproxy.tools.console import flowview
+from mitmproxy.tools.console import commands
+from mitmproxy.tools.console import options
+from mitmproxy.tools.console import overlay
+from mitmproxy.tools.console import help
+from mitmproxy.tools.console import grideditor
+from mitmproxy.tools.console import eventlog
-class Window(urwid.Frame):
+class WindowStack:
+ def __init__(self, master, base):
+ self.master = master
+ self.windows = dict(
+ flowlist = flowlist.FlowListBox(master),
+ flowview = flowview.FlowView(master),
+ commands = commands.Commands(master),
+ options = options.Options(master),
+ help = help.HelpView(None),
+ eventlog = eventlog.EventLog(master),
- def __init__(self, master, body, header, footer, helpctx):
- urwid.Frame.__init__(
- self,
- urwid.AttrWrap(body, "background"),
- header = urwid.AttrWrap(header, "background") if header else None,
- footer = urwid.AttrWrap(footer, "background") if footer else None
+ edit_focus_query = grideditor.QueryEditor(master),
+ edit_focus_cookies = grideditor.CookieEditor(master),
+ edit_focus_setcookies = grideditor.SetCookieEditor(master),
+ edit_focus_form = grideditor.RequestFormEditor(master),
+ edit_focus_path = grideditor.PathEditor(master),
+ edit_focus_request_headers = grideditor.RequestHeaderEditor(master),
+ edit_focus_response_headers = grideditor.ResponseHeaderEditor(master),
+ )
+ self.stack = [base]
+ self.overlay = None
+
+ def set_overlay(self, o, **kwargs):
+ self.overlay = overlay.SimpleOverlay(self, o, self.top(), o.width, **kwargs)
+
+ @property
+ def topwin(self):
+ return self.windows[self.stack[-1]]
+
+ def top(self):
+ if self.overlay:
+ return self.overlay
+ return self.topwin
+
+ def push(self, wname):
+ if self.stack[-1] == wname:
+ return
+ self.stack.append(wname)
+
+ def pop(self, *args, **kwargs):
+ """
+ Pop off the stack, return True if we're already at the top.
+ """
+ if self.overlay:
+ self.overlay = None
+ elif len(self.stack) > 1:
+ self.call("view_popping")
+ self.stack.pop()
+ else:
+ return True
+
+ def call(self, name, *args, **kwargs):
+ f = getattr(self.topwin, name, None)
+ if f:
+ f(*args, **kwargs)
+
+
+class Window(urwid.Frame):
+ def __init__(self, master):
+ self.statusbar = statusbar.StatusBar(master, "")
+ super().__init__(
+ None,
+ header = None,
+ footer = urwid.AttrWrap(self.statusbar, "background")
)
self.master = master
- self.helpctx = helpctx
+ self.master.view.sig_view_refresh.connect(self.view_changed)
+ self.master.view.sig_view_add.connect(self.view_changed)
+ self.master.view.sig_view_remove.connect(self.view_changed)
+ self.master.view.sig_view_update.connect(self.view_changed)
+ self.master.view.focus.sig_change.connect(self.view_changed)
+ self.master.view.focus.sig_change.connect(self.focus_changed)
+
signals.focus.connect(self.sig_focus)
+ signals.flow_change.connect(self.flow_changed)
+ signals.pop_view_state.connect(self.pop)
+ signals.push_view_state.connect(self.push)
+
+ self.master.options.subscribe(self.configure, ["console_layout"])
+ self.pane = 0
+ self.stacks = [
+ WindowStack(master, "flowlist"),
+ WindowStack(master, "eventlog")
+ ]
+
+ def focus_stack(self):
+ return self.stacks[self.pane]
+
+ def configure(self, otions, updated):
+ self.refresh()
+
+ def refresh(self):
+ """
+ Redraw the layout.
+ """
+ c = self.master.options.console_layout
+
+ w = None
+ if c == "single":
+ w = self.stacks[0].top()
+ elif c == "vertical":
+ w = urwid.Pile(
+ [i.top() for i in self.stacks]
+ )
+ else:
+ w = urwid.Columns(
+ [i.top() for i in self.stacks], dividechars=1
+ )
+ self.body = urwid.AttrWrap(w, "background")
+ if c == "single":
+ self.pane = 0
+
+ def flow_changed(self, sender, flow):
+ if self.master.view.focus.flow:
+ if flow.id == self.master.view.focus.flow.id:
+ self.focus_changed()
+
+ def focus_changed(self, *args, **kwargs):
+ """
+ Triggered when the focus changes - either when it's modified, or
+ when it changes to a different flow altogether.
+ """
+ for i in self.stacks:
+ i.call("focus_changed")
+
+ def view_changed(self, *args, **kwargs):
+ """
+ Triggered when the view list has changed.
+ """
+ for i in self.stacks:
+ i.call("view_changed")
+
+ def set_overlay(self, o, **kwargs):
+ """
+ Set an overlay on the currently focused stack.
+ """
+ self.focus_stack().set_overlay(o, **kwargs)
+ self.refresh()
+
+ def push(self, wname):
+ """
+ Push a window onto the currently focused stack.
+ """
+ self.focus_stack().push(wname)
+ self.refresh()
+ self.view_changed()
+ self.focus_changed()
+
+ def pop(self, *args, **kwargs):
+ """
+ Pop a window from the currently focused stack. If there is only one
+ window on the stack, this prompts for exit.
+ """
+ if self.focus_stack().pop():
+ self.master.prompt_for_exit()
+ else:
+ self.refresh()
+ self.view_changed()
+ self.focus_changed()
+
+ def current(self, keyctx):
+ """
+
+ Returns the top window of the current stack, IF the current focus
+ has a matching key context.
+ """
+ t = self.focus_stack().topwin
+ if t.keyctx == keyctx:
+ return t
+
+ def any(self, keyctx):
+ """
+ Returns the top window of either stack if they match the context.
+ """
+ for t in [x.topwin for x in self.stacks]:
+ if t.keyctx == keyctx:
+ return t
def sig_focus(self, sender, section):
self.focus_position = section
+ def switch(self):
+ """
+ Switch between the two panes.
+ """
+ if self.master.options.console_layout == "single":
+ self.pane = 0
+ else:
+ self.pane = (self.pane + 1) % len(self.stacks)
+
def mouse_event(self, *args, **kwargs):
# args: (size, event, button, col, row)
k = super().mouse_event(*args, **kwargs)
@@ -36,76 +218,11 @@ class Window(urwid.Frame):
return False
return True
- def handle_replay(self, k):
- if k == "c":
- creplay = self.master.addons.get("clientplayback")
- if self.master.options.client_replay and creplay.count():
- def stop_client_playback_prompt(a):
- if a != "n":
- self.master.options.client_replay = None
- signals.status_prompt_onekey.send(
- self,
- prompt = "Stop current client replay?",
- keys = (
- ("yes", "y"),
- ("no", "n"),
- ),
- callback = stop_client_playback_prompt
- )
- else:
- signals.status_prompt_path.send(
- self,
- prompt = "Client replay path",
- callback = lambda x: self.master.options.setter("client_replay")([x])
- )
- elif k == "s":
- a = self.master.addons.get("serverplayback")
- if a.count():
- def stop_server_playback(response):
- if response == "y":
- self.master.options.server_replay = []
- signals.status_prompt_onekey.send(
- self,
- prompt = "Stop current server replay?",
- keys = (
- ("yes", "y"),
- ("no", "n"),
- ),
- callback = stop_server_playback
- )
- else:
- signals.status_prompt_path.send(
- self,
- prompt = "Server playback path",
- callback = lambda x: self.master.options.setter("server_replay")([x])
- )
-
def keypress(self, size, k):
- k = super().keypress(size, k)
- if k == "?":
- self.master.view_help(self.helpctx)
- elif k == "i":
- signals.status_prompt.send(
- self,
- prompt = "Intercept filter",
- text = self.master.options.intercept,
- callback = self.master.options.setter("intercept")
- )
- elif k == "O":
- self.master.view_options()
- elif k == "Q":
- raise urwid.ExitMainLoop
- elif k == "q":
- signals.pop_view_state.send(self)
- elif k == "R":
- signals.status_prompt_onekey.send(
- self,
- prompt = "Replay",
- keys = (
- ("client", "c"),
- ("server", "s"),
- ),
- callback = self.handle_replay,
- )
+ if self.focus_part == "footer":
+ return super().keypress(size, k)
else:
- return k
+ fs = self.focus_stack().top()
+ k = fs.keypress(size, k)
+ if k:
+ return self.master.keymap.handle(fs.keyctx, k)
diff --git a/mitmproxy/tools/dump.py b/mitmproxy/tools/dump.py
index e6f0c3df..4d0ccf4b 100644
--- a/mitmproxy/tools/dump.py
+++ b/mitmproxy/tools/dump.py
@@ -1,7 +1,7 @@
from mitmproxy import addons
from mitmproxy import options
from mitmproxy import master
-from mitmproxy.addons import dumper, termlog, termstatus, readstdin, keepserving
+from mitmproxy.addons import dumper, termlog, termstatus, keepserving, readfile
class ErrorCheck:
@@ -16,11 +16,11 @@ class ErrorCheck:
class DumpMaster(master.Master):
def __init__(
- self,
- options: options.Options,
- server,
- with_termlog=True,
- with_dumper=True,
+ self,
+ options: options.Options,
+ server,
+ with_termlog=True,
+ with_dumper=True,
) -> None:
master.Master.__init__(self, options, server)
self.errorcheck = ErrorCheck()
@@ -30,7 +30,7 @@ class DumpMaster(master.Master):
if with_dumper:
self.addons.add(dumper.Dumper())
self.addons.add(
- readstdin.ReadStdin(),
keepserving.KeepServing(),
+ readfile.ReadFileStdin(),
self.errorcheck
)
diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py
index b321e8f8..d8fac077 100644
--- a/mitmproxy/tools/main.py
+++ b/mitmproxy/tools/main.py
@@ -39,7 +39,7 @@ def process_options(parser, opts, args):
if args.version:
print(debug.dump_system_info())
sys.exit(0)
- if args.quiet or args.options:
+ if args.quiet or args.options or args.commands:
args.verbosity = 0
args.flow_detail = 0
@@ -60,7 +60,11 @@ def process_options(parser, opts, args):
return server.DummyServer(pconf)
-def run(MasterKlass, args): # pragma: no cover
+def run(MasterKlass, args, extra=None): # pragma: no cover
+ """
+ extra: Extra argument processing callable which returns a dict of
+ options.
+ """
version_check.check_pyopenssl_version()
debug.register_info_dumpers()
@@ -72,14 +76,20 @@ def run(MasterKlass, args): # pragma: no cover
unknown = optmanager.load_paths(opts, args.conf)
server = process_options(parser, opts, args)
master = MasterKlass(opts, server)
- master.addons.configure_all(opts, opts.keys())
+ master.addons.trigger("configure", opts.keys())
+ master.addons.trigger("tick")
remaining = opts.update_known(**unknown)
if remaining and opts.verbosity > 1:
print("Ignored options: %s" % remaining)
if args.options:
print(optmanager.dump_defaults(opts))
sys.exit(0)
+ if args.commands:
+ master.commands.dump()
+ sys.exit(0)
opts.set(*args.setoptions)
+ if extra:
+ opts.update(**extra(args))
def cleankill(*args, **kwargs):
master.shutdown()
@@ -89,7 +99,7 @@ def run(MasterKlass, args): # pragma: no cover
except exceptions.OptionsError as e:
print("%s: %s" % (sys.argv[0], e), file=sys.stderr)
sys.exit(1)
- except (KeyboardInterrupt, RuntimeError):
+ except (KeyboardInterrupt, RuntimeError) as e:
pass
return master
@@ -107,7 +117,17 @@ def mitmproxy(args=None): # pragma: no cover
def mitmdump(args=None): # pragma: no cover
from mitmproxy.tools import dump
- m = run(dump.DumpMaster, args)
+
+ def extra(args):
+ if args.filter_args:
+ v = " ".join(args.filter_args)
+ return dict(
+ view_filter = v,
+ save_stream_filter = v,
+ )
+ return {}
+
+ m = run(dump.DumpMaster, args, extra)
if m and m.errorcheck.has_errored:
sys.exit(1)
diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py
index 002513b9..c55c0cb5 100644
--- a/mitmproxy/tools/web/app.py
+++ b/mitmproxy/tools/web/app.py
@@ -17,6 +17,7 @@ from mitmproxy import http
from mitmproxy import io
from mitmproxy import log
from mitmproxy import version
+import mitmproxy.tools.web.master # noqa
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
@@ -121,7 +122,7 @@ class RequestHandler(tornado.web.RequestHandler):
self.add_header(
"Content-Security-Policy",
"default-src 'self'; "
- "connect-src 'self' ws://* ; "
+ "connect-src 'self' ws:; "
"style-src 'self' 'unsafe-inline'"
)
@@ -245,7 +246,7 @@ class ResumeFlows(RequestHandler):
def post(self):
for f in self.view:
f.resume()
- self.view.update(f)
+ self.view.update([f])
class KillFlows(RequestHandler):
@@ -253,27 +254,27 @@ class KillFlows(RequestHandler):
for f in self.view:
if f.killable:
f.kill()
- self.view.update(f)
+ self.view.update([f])
class ResumeFlow(RequestHandler):
def post(self, flow_id):
self.flow.resume()
- self.view.update(self.flow)
+ self.view.update([self.flow])
class KillFlow(RequestHandler):
def post(self, flow_id):
if self.flow.killable:
self.flow.kill()
- self.view.update(self.flow)
+ self.view.update([self.flow])
class FlowHandler(RequestHandler):
def delete(self, flow_id):
if self.flow.killable:
self.flow.kill()
- self.view.remove(self.flow)
+ self.view.remove([self.flow])
def put(self, flow_id):
flow = self.flow
@@ -316,13 +317,13 @@ class FlowHandler(RequestHandler):
except APIError:
flow.revert()
raise
- self.view.update(flow)
+ self.view.update([flow])
class DuplicateFlow(RequestHandler):
def post(self, flow_id):
f = self.flow.copy()
- self.view.add(f)
+ self.view.add([f])
self.write(f.id)
@@ -330,14 +331,14 @@ class RevertFlow(RequestHandler):
def post(self, flow_id):
if self.flow.modified():
self.flow.revert()
- self.view.update(self.flow)
+ self.view.update([self.flow])
class ReplayFlow(RequestHandler):
def post(self, flow_id):
self.flow.backup()
self.flow.response = None
- self.view.update(self.flow)
+ self.view.update([self.flow])
try:
self.master.replay_request(self.flow)
@@ -350,7 +351,7 @@ class FlowContent(RequestHandler):
self.flow.backup()
message = getattr(self.flow, message)
message.content = self.filecontents
- self.view.update(self.flow)
+ self.view.update([self.flow])
def get(self, flow_id, message):
message = getattr(self.flow, message)
@@ -421,6 +422,7 @@ class Settings(RequestHandler):
contentViews=[v.name.replace(' ', '_') for v in contentviews.views],
listen_host=self.master.options.listen_host,
listen_port=self.master.options.listen_port,
+ server=self.master.options.server,
))
def put(self):
diff --git a/mitmproxy/tools/web/master.py b/mitmproxy/tools/web/master.py
index e28bd002..c09fe0a2 100644
--- a/mitmproxy/tools/web/master.py
+++ b/mitmproxy/tools/web/master.py
@@ -7,8 +7,10 @@ from mitmproxy import log
from mitmproxy import master
from mitmproxy.addons import eventstore
from mitmproxy.addons import intercept
+from mitmproxy.addons import readfile
from mitmproxy.addons import termlog
from mitmproxy.addons import view
+from mitmproxy.addons import termstatus
from mitmproxy.options import Options # noqa
from mitmproxy.tools.web import app
@@ -31,11 +33,12 @@ class WebMaster(master.Master):
self.addons.add(*addons.default_addons())
self.addons.add(
intercept.Intercept(),
+ readfile.ReadFile(),
self.view,
self.events,
)
if with_termlog:
- self.addons.add(termlog.TermLog())
+ self.addons.add(termlog.TermLog(), termstatus.TermStatus())
self.app = app.Application(
self, self.options.web_debug
)
@@ -99,11 +102,6 @@ class WebMaster(master.Master):
iol.add_callback(self.start)
tornado.ioloop.PeriodicCallback(lambda: self.tick(timeout=0), 5).start()
- self.add_log(
- "Proxy server listening at http://{}:{}/".format(self.server.address[0], self.server.address[1]),
- "info"
- )
-
web_url = "http://{}:{}/".format(self.options.web_iface, self.options.web_port)
self.add_log(
"Web server listening at {}".format(web_url),
diff --git a/mitmproxy/tools/web/static/app.css b/mitmproxy/tools/web/static/app.css
index 09043a0b..43e5cd8d 100644
--- a/mitmproxy/tools/web/static/app.css
+++ b/mitmproxy/tools/web/static/app.css
@@ -133,7 +133,7 @@ header {
padding-top: 6px;
background-color: white;
}
-header menu {
+header > div {
display: block;
margin: 0;
padding: 0;
@@ -226,14 +226,73 @@ header menu {
}
.filter-input .popover {
top: 27px;
+ left: 43px;
display: block;
max-width: none;
opacity: 0.9;
}
+@media (max-width: 767px) {
+ .filter-input .popover {
+ top: 16px;
+ left: 29px;
+ right: 2px;
+ }
+}
.filter-input .popover .popover-content {
max-height: 500px;
overflow-y: auto;
}
+.filter-input .popover .popover-content tr {
+ cursor: pointer;
+}
+.filter-input .popover .popover-content tr:hover {
+ background-color: rgba(193, 215, 235, 0.5) !important;
+}
+.connection-indicator {
+ display: inline;
+ padding: .2em .6em .3em;
+ font-size: 75%;
+ font-weight: bold;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: .25em;
+ float: right;
+ margin: 5px;
+ opacity: 1;
+ transition: all 1s linear;
+}
+a.connection-indicator:hover,
+a.connection-indicator:focus {
+ color: #fff;
+ text-decoration: none;
+ cursor: pointer;
+}
+.connection-indicator:empty {
+ display: none;
+}
+.btn .connection-indicator {
+ position: relative;
+ top: -1px;
+}
+.connection-indicator.init,
+.connection-indicator.fetching {
+ background-color: #5bc0de;
+}
+.connection-indicator.established {
+ background-color: #5cb85c;
+ opacity: 0;
+}
+.connection-indicator.error {
+ background-color: #d9534f;
+ transition: all 0.2s linear;
+}
+.connection-indicator.offline {
+ background-color: #f0ad4e;
+ opacity: 1;
+}
.flow-table {
width: 100%;
overflow-y: scroll;
@@ -358,8 +417,7 @@ header menu {
background-color: #F2F2F2;
}
.flow-detail section {
- display: flex;
- flex-direction: column;
+ overflow-y: scroll;
}
.flow-detail section > article {
overflow: auto;
diff --git a/mitmproxy/tools/web/static/app.js b/mitmproxy/tools/web/static/app.js
index 22692876..8ee4d97d 100644
--- a/mitmproxy/tools/web/static/app.js
+++ b/mitmproxy/tools/web/static/app.js
@@ -245,8 +245,8 @@ document.addEventListener('DOMContentLoaded', function () {
}).call(this,require('_process'))
-},{"./backends/websocket":3,"./components/ProxyApp":37,"./ducks/eventLog":48,"./ducks/index":50,"./urlState":59,"_process":1,"react":"react","react-dom":"react-dom","react-redux":"react-redux","redux":"redux","redux-logger":"redux-logger","redux-thunk":"redux-thunk"}],3:[function(require,module,exports){
-'use strict';
+},{"./backends/websocket":3,"./components/ProxyApp":38,"./ducks/eventLog":50,"./ducks/index":52,"./urlState":61,"_process":1,"react":"react","react-dom":"react-dom","react-redux":"react-redux","redux":"redux","redux-logger":"redux-logger","redux-thunk":"redux-thunk"}],3:[function(require,module,exports){
+"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
@@ -261,7 +261,13 @@ var _createClass = function () { function defineProperties(target, props) { for
*/
-var _utils = require('../utils');
+var _utils = require("../utils");
+
+var _connection = require("../ducks/connection");
+
+var connectionActions = _interopRequireWildcard(_connection);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
@@ -277,7 +283,7 @@ var WebsocketBackend = function () {
}
_createClass(WebsocketBackend, [{
- key: 'connect',
+ key: "connect",
value: function connect() {
var _this = this;
@@ -285,8 +291,8 @@ var WebsocketBackend = function () {
this.socket.addEventListener('open', function () {
return _this.onOpen();
});
- this.socket.addEventListener('close', function () {
- return _this.onClose();
+ this.socket.addEventListener('close', function (event) {
+ return _this.onClose(event);
});
this.socket.addEventListener('message', function (msg) {
return _this.onMessage(JSON.parse(msg.data));
@@ -296,20 +302,21 @@ var WebsocketBackend = function () {
});
}
}, {
- key: 'onOpen',
+ key: "onOpen",
value: function onOpen() {
this.fetchData("settings");
this.fetchData("flows");
this.fetchData("events");
+ this.store.dispatch(connectionActions.startFetching());
}
}, {
- key: 'fetchData',
+ key: "fetchData",
value: function fetchData(resource) {
var _this2 = this;
var queue = [];
this.activeFetches[resource] = queue;
- (0, _utils.fetchApi)('/' + resource).then(function (res) {
+ (0, _utils.fetchApi)("/" + resource).then(function (res) {
return res.json();
}).then(function (json) {
// Make sure that we are not superseded yet by the server sending a RESET.
@@ -317,7 +324,7 @@ var WebsocketBackend = function () {
});
}
}, {
- key: 'onMessage',
+ key: "onMessage",
value: function onMessage(msg) {
if (msg.cmd === CMD_RESET) {
@@ -326,34 +333,39 @@ var WebsocketBackend = function () {
if (msg.resource in this.activeFetches) {
this.activeFetches[msg.resource].push(msg);
} else {
- var type = (msg.resource + '_' + msg.cmd).toUpperCase();
+ var type = (msg.resource + "_" + msg.cmd).toUpperCase();
this.store.dispatch(_extends({ type: type }, msg));
}
}
}, {
- key: 'receive',
+ key: "receive",
value: function receive(resource, data) {
var _this3 = this;
- var type = (resource + '_RECEIVE').toUpperCase();
+ var type = (resource + "_RECEIVE").toUpperCase();
this.store.dispatch({ type: type, cmd: "receive", resource: resource, data: data });
var queue = this.activeFetches[resource];
delete this.activeFetches[resource];
queue.forEach(function (msg) {
return _this3.onMessage(msg);
});
+
+ if (Object.keys(this.activeFetches).length === 0) {
+ // We have fetched the last resource
+ this.store.dispatch(connectionActions.connectionEstablished());
+ }
}
}, {
- key: 'onClose',
- value: function onClose() {
- // FIXME
- console.error("onClose", arguments);
+ key: "onClose",
+ value: function onClose(closeEvent) {
+ this.store.dispatch(connectionActions.connectionError("Connection closed at " + new Date().toUTCString() + " with error code " + closeEvent.code + "."));
+ console.error("websocket connection closed", closeEvent);
}
}, {
- key: 'onError',
+ key: "onError",
value: function onError() {
// FIXME
- console.error("onError", arguments);
+ console.error("websocket connection errored", arguments);
}
}]);
@@ -362,7 +374,7 @@ var WebsocketBackend = function () {
exports.default = WebsocketBackend;
-},{"../utils":60}],4:[function(require,module,exports){
+},{"../ducks/connection":49,"../utils":62}],4:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -375,6 +387,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _ContentViews = require('./ContentView/ContentViews');
@@ -399,8 +415,8 @@ ContentView.propTypes = {
// It may seem a bit weird at the first glance:
// Every view takes the flow and the message as props, e.g.
// <Auto flow={flow} message={flow.request}/>
- flow: _react2.default.PropTypes.object.isRequired,
- message: _react2.default.PropTypes.object.isRequired
+ flow: _propTypes2.default.object.isRequired,
+ message: _propTypes2.default.object.isRequired
};
ContentView.isContentTooLarge = function (msg) {
@@ -448,7 +464,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
updateEdit: _flow.updateEdit
})(ContentView);
-},{"../ducks/ui/flow":52,"./ContentView/ContentViews":8,"./ContentView/MetaViews":10,"./ContentView/ShowFullContentButton":11,"react":"react","react-redux":"react-redux"}],5:[function(require,module,exports){
+},{"../ducks/ui/flow":54,"./ContentView/ContentViews":8,"./ContentView/MetaViews":10,"./ContentView/ShowFullContentButton":11,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],5:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -460,6 +476,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactCodemirror = require('react-codemirror');
var _reactCodemirror2 = _interopRequireDefault(_reactCodemirror);
@@ -467,8 +487,8 @@ var _reactCodemirror2 = _interopRequireDefault(_reactCodemirror);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
CodeEditor.propTypes = {
- content: _react.PropTypes.string.isRequired,
- onChange: _react.PropTypes.func.isRequired
+ content: _propTypes2.default.string.isRequired,
+ onChange: _propTypes2.default.func.isRequired
};
function CodeEditor(_ref) {
@@ -488,7 +508,7 @@ function CodeEditor(_ref) {
);
}
-},{"react":"react","react-codemirror":"react-codemirror"}],6:[function(require,module,exports){
+},{"prop-types":"prop-types","react":"react","react-codemirror":"react-codemirror"}],6:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -503,6 +523,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _utils = require('../../flow/utils.js');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -611,13 +635,13 @@ exports.default = function (View) {
return _class;
}(_react2.default.Component), _class.displayName = View.displayName || View.name, _class.matches = View.matches, _class.propTypes = _extends({}, View.propTypes, {
- content: _react.PropTypes.string, // mark as non-required
- flow: _react.PropTypes.object.isRequired,
- message: _react.PropTypes.object.isRequired
+ content: _propTypes2.default.string, // mark as non-required
+ flow: _propTypes2.default.object.isRequired,
+ message: _propTypes2.default.object.isRequired
}), _temp;
};
-},{"../../flow/utils.js":58,"react":"react"}],7:[function(require,module,exports){
+},{"../../flow/utils.js":60,"prop-types":"prop-types","react":"react"}],7:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -628,6 +652,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _ViewSelector = require('./ViewSelector');
@@ -645,8 +673,8 @@ var _DownloadContentButton2 = _interopRequireDefault(_DownloadContentButton);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
ContentViewOptions.propTypes = {
- flow: _react2.default.PropTypes.object.isRequired,
- message: _react2.default.PropTypes.object.isRequired
+ flow: _propTypes2.default.object.isRequired,
+ message: _propTypes2.default.object.isRequired
};
function ContentViewOptions(_ref) {
@@ -689,7 +717,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
};
})(ContentViewOptions);
-},{"./DownloadContentButton":9,"./UploadContentButton":12,"./ViewSelector":13,"react":"react","react-redux":"react-redux"}],8:[function(require,module,exports){
+},{"./DownloadContentButton":9,"./UploadContentButton":12,"./ViewSelector":13,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],8:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -705,6 +733,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _flow = require('../../ducks/ui/flow');
@@ -732,8 +764,8 @@ ViewImage.matches = function (msg) {
return isImage.test(_utils.MessageUtils.getContentType(msg));
};
ViewImage.propTypes = {
- flow: _react.PropTypes.object.isRequired,
- message: _react.PropTypes.object.isRequired
+ flow: _propTypes2.default.object.isRequired,
+ message: _propTypes2.default.object.isRequired
};
function ViewImage(_ref) {
var flow = _ref.flow,
@@ -747,7 +779,7 @@ function ViewImage(_ref) {
}
Edit.propTypes = {
- content: _react2.default.PropTypes.string.isRequired
+ content: _propTypes2.default.string.isRequired
};
function Edit(_ref2) {
@@ -834,10 +866,10 @@ var ViewServer = function (_Component) {
}(_react.Component);
ViewServer.propTypes = {
- showFullContent: _react.PropTypes.bool.isRequired,
- maxLines: _react.PropTypes.number.isRequired,
- setContentViewDescription: _react.PropTypes.func.isRequired,
- setContent: _react.PropTypes.func.isRequired
+ showFullContent: _propTypes2.default.bool.isRequired,
+ maxLines: _propTypes2.default.number.isRequired,
+ setContentViewDescription: _propTypes2.default.func.isRequired,
+ setContent: _propTypes2.default.func.isRequired
};
@@ -855,7 +887,7 @@ exports.Edit = Edit;
exports.ViewServer = ViewServer;
exports.ViewImage = ViewImage;
-},{"../../ducks/ui/flow":52,"../../flow/utils":58,"./CodeEditor":5,"./ContentLoader":6,"react":"react","react-redux":"react-redux"}],9:[function(require,module,exports){
+},{"../../ducks/ui/flow":54,"../../flow/utils":60,"./CodeEditor":5,"./ContentLoader":6,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],9:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -865,11 +897,15 @@ exports.default = DownloadContentButton;
var _utils = require("../../flow/utils");
-var _react = require("react");
+var _propTypes = require("prop-types");
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
DownloadContentButton.propTypes = {
- flow: _react.PropTypes.object.isRequired,
- message: _react.PropTypes.object.isRequired
+ flow: _propTypes2.default.object.isRequired,
+ message: _propTypes2.default.object.isRequired
};
function DownloadContentButton(_ref) {
@@ -886,7 +922,7 @@ function DownloadContentButton(_ref) {
);
}
-},{"../../flow/utils":58,"react":"react"}],10:[function(require,module,exports){
+},{"../../flow/utils":60,"prop-types":"prop-types"}],10:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -967,7 +1003,7 @@ function ContentTooLarge(_ref3) {
);
}
-},{"../../utils.js":60,"./DownloadContentButton":9,"./UploadContentButton":12,"react":"react"}],11:[function(require,module,exports){
+},{"../../utils.js":62,"./DownloadContentButton":9,"./UploadContentButton":12,"react":"react"}],11:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -978,6 +1014,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _reactDom = require('react-dom');
@@ -991,8 +1031,8 @@ var _flow = require('../../ducks/ui/flow');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
ShowFullContentButton.propTypes = {
- setShowFullContent: _react.PropTypes.func.isRequired,
- showFullContent: _react.PropTypes.bool.isRequired
+ setShowFullContent: _propTypes2.default.func.isRequired,
+ showFullContent: _propTypes2.default.bool.isRequired
};
function ShowFullContentButton(_ref) {
@@ -1035,7 +1075,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
setShowFullContent: _flow.setShowFullContent
})(ShowFullContentButton);
-},{"../../ducks/ui/flow":52,"../common/Button":40,"react":"react","react-dom":"react-dom","react-redux":"react-redux"}],12:[function(require,module,exports){
+},{"../../ducks/ui/flow":54,"../common/Button":41,"prop-types":"prop-types","react":"react","react-dom":"react-dom","react-redux":"react-redux"}],12:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -1043,7 +1083,9 @@ Object.defineProperty(exports, "__esModule", {
});
exports.default = UploadContentButton;
-var _react = require('react');
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
var _FileChooser = require('../common/FileChooser');
@@ -1052,7 +1094,7 @@ var _FileChooser2 = _interopRequireDefault(_FileChooser);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
UploadContentButton.propTypes = {
- uploadContent: _react.PropTypes.func.isRequired
+ uploadContent: _propTypes2.default.func.isRequired
};
function UploadContentButton(_ref) {
@@ -1066,7 +1108,7 @@ function UploadContentButton(_ref) {
className: 'btn btn-default btn-xs' });
}
-},{"../common/FileChooser":43,"react":"react"}],13:[function(require,module,exports){
+},{"../common/FileChooser":44,"prop-types":"prop-types"}],13:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -1077,6 +1119,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _flow = require('../../ducks/ui/flow');
@@ -1088,9 +1134,9 @@ var _Dropdown2 = _interopRequireDefault(_Dropdown);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
ViewSelector.propTypes = {
- contentViews: _react.PropTypes.array.isRequired,
- activeView: _react.PropTypes.string.isRequired,
- setContentView: _react.PropTypes.func.isRequired
+ contentViews: _propTypes2.default.array.isRequired,
+ activeView: _propTypes2.default.string.isRequired,
+ setContentView: _propTypes2.default.func.isRequired
};
function ViewSelector(_ref) {
@@ -1138,7 +1184,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
setContentView: _flow.setContentView
})(ViewSelector);
-},{"../../ducks/ui/flow":52,"../common/Dropdown":42,"react":"react","react-redux":"react-redux"}],14:[function(require,module,exports){
+},{"../../ducks/ui/flow":54,"../common/Dropdown":43,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],14:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -1151,6 +1197,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _eventLog = require('../ducks/eventLog');
@@ -1229,7 +1279,7 @@ var EventLog = function (_Component) {
_react2.default.createElement(
'div',
{ className: 'pull-right' },
- ['debug', 'info', 'web'].map(function (type) {
+ ['debug', 'info', 'web', 'warn', 'error'].map(function (type) {
return _react2.default.createElement(_ToggleButton2.default, { key: type, text: type, checked: filters[type], onToggle: function onToggle() {
return toggleFilter(type);
} });
@@ -1246,11 +1296,11 @@ var EventLog = function (_Component) {
}(_react.Component);
EventLog.propTypes = {
- filters: _react.PropTypes.object.isRequired,
- events: _react.PropTypes.array.isRequired,
- toggleFilter: _react.PropTypes.func.isRequired,
- close: _react.PropTypes.func.isRequired,
- defaultHeight: _react.PropTypes.number
+ filters: _propTypes2.default.object.isRequired,
+ events: _propTypes2.default.array.isRequired,
+ toggleFilter: _propTypes2.default.func.isRequired,
+ close: _propTypes2.default.func.isRequired,
+ defaultHeight: _propTypes2.default.number
};
EventLog.defaultProps = {
defaultHeight: 200
@@ -1265,7 +1315,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
toggleFilter: _eventLog.toggleFilter
})(EventLog);
-},{"../ducks/eventLog":48,"./EventLog/EventList":15,"./common/ToggleButton":45,"react":"react","react-redux":"react-redux"}],15:[function(require,module,exports){
+},{"../ducks/eventLog":50,"./EventLog/EventList":15,"./common/ToggleButton":46,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],15:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -1278,6 +1328,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
@@ -1395,8 +1449,8 @@ var EventLogList = function (_Component) {
}(_react.Component);
EventLogList.propTypes = {
- events: _react.PropTypes.array.isRequired,
- rowHeight: _react.PropTypes.number
+ events: _propTypes2.default.array.isRequired,
+ rowHeight: _propTypes2.default.number
};
EventLogList.defaultProps = {
rowHeight: 18
@@ -1406,13 +1460,18 @@ EventLogList.defaultProps = {
function LogIcon(_ref) {
var event = _ref.event;
- var icon = { web: 'html5', debug: 'bug' }[event.level] || 'info';
+ var icon = {
+ web: 'html5',
+ debug: 'bug',
+ warn: 'exclamation-triangle',
+ error: 'ban'
+ }[event.level] || 'info';
return _react2.default.createElement('i', { className: 'fa fa-fw fa-' + icon });
}
exports.default = (0, _AutoScroll2.default)(EventLogList);
-},{"../helpers/AutoScroll":46,"../helpers/VirtualScroll":47,"react":"react","react-dom":"react-dom","shallowequal":"shallowequal"}],16:[function(require,module,exports){
+},{"../helpers/AutoScroll":47,"../helpers/VirtualScroll":48,"prop-types":"prop-types","react":"react","react-dom":"react-dom","shallowequal":"shallowequal"}],16:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -1425,6 +1484,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
@@ -1592,18 +1655,18 @@ var FlowTable = function (_React$Component) {
}(_react2.default.Component);
FlowTable.propTypes = {
- onSelect: _react.PropTypes.func.isRequired,
- flows: _react.PropTypes.array.isRequired,
- rowHeight: _react.PropTypes.number,
- highlight: _react.PropTypes.string,
- selected: _react.PropTypes.object
+ onSelect: _propTypes2.default.func.isRequired,
+ flows: _propTypes2.default.array.isRequired,
+ rowHeight: _propTypes2.default.number,
+ highlight: _propTypes2.default.string,
+ selected: _propTypes2.default.object
};
FlowTable.defaultProps = {
rowHeight: 32
};
exports.default = (0, _AutoScroll2.default)(FlowTable);
-},{"../filt/filt":57,"./FlowTable/FlowRow":18,"./FlowTable/FlowTableHead":19,"./helpers/AutoScroll":46,"./helpers/VirtualScroll":47,"react":"react","react-dom":"react-dom","shallowequal":"shallowequal"}],17:[function(require,module,exports){
+},{"../filt/filt":59,"./FlowTable/FlowRow":18,"./FlowTable/FlowTableHead":19,"./helpers/AutoScroll":47,"./helpers/VirtualScroll":48,"prop-types":"prop-types","react":"react","react-dom":"react-dom","shallowequal":"shallowequal"}],17:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -1770,7 +1833,7 @@ TimeColumn.headerName = 'Time';
exports.default = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, SizeColumn, TimeColumn];
-},{"../../flow/utils.js":58,"../../utils.js":60,"classnames":"classnames","react":"react"}],18:[function(require,module,exports){
+},{"../../flow/utils.js":60,"../../utils.js":62,"classnames":"classnames","react":"react"}],18:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -1781,6 +1844,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
@@ -1794,10 +1861,10 @@ var _utils = require('../../utils');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
FlowRow.propTypes = {
- onSelect: _react.PropTypes.func.isRequired,
- flow: _react.PropTypes.object.isRequired,
- highlighted: _react.PropTypes.bool,
- selected: _react.PropTypes.bool
+ onSelect: _propTypes2.default.func.isRequired,
+ flow: _propTypes2.default.object.isRequired,
+ highlighted: _propTypes2.default.bool,
+ selected: _propTypes2.default.bool
};
function FlowRow(_ref) {
@@ -1827,7 +1894,7 @@ function FlowRow(_ref) {
exports.default = (0, _utils.pure)(FlowRow);
-},{"../../utils":60,"./FlowColumns":17,"classnames":"classnames","react":"react"}],19:[function(require,module,exports){
+},{"../../utils":62,"./FlowColumns":17,"classnames":"classnames","prop-types":"prop-types","react":"react"}],19:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -1838,6 +1905,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _classnames = require('classnames');
@@ -1853,9 +1924,9 @@ var _flows = require('../../ducks/flows');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
FlowTableHead.propTypes = {
- setSort: _react.PropTypes.func.isRequired,
- sortDesc: _react2.default.PropTypes.bool.isRequired,
- sortColumn: _react2.default.PropTypes.string
+ setSort: _propTypes2.default.func.isRequired,
+ sortDesc: _propTypes2.default.bool.isRequired,
+ sortColumn: _propTypes2.default.string
};
function FlowTableHead(_ref) {
@@ -1891,7 +1962,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
setSort: _flows.setSort
})(FlowTableHead);
-},{"../../ducks/flows":49,"./FlowColumns":17,"classnames":"classnames","react":"react","react-redux":"react-redux"}],20:[function(require,module,exports){
+},{"../../ducks/flows":51,"./FlowColumns":17,"classnames":"classnames","prop-types":"prop-types","react":"react","react-redux":"react-redux"}],20:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -2030,7 +2101,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
selectTab: _flow.selectTab
})(FlowView);
-},{"../ducks/ui/flow":52,"./FlowView/Details":21,"./FlowView/Messages":23,"./FlowView/Nav":24,"./Prompt":36,"lodash":"lodash","react":"react","react-redux":"react-redux"}],21:[function(require,module,exports){
+},{"../ducks/ui/flow":54,"./FlowView/Details":21,"./FlowView/Messages":23,"./FlowView/Nav":24,"./Prompt":37,"lodash":"lodash","react":"react","react-redux":"react-redux"}],21:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -2253,7 +2324,7 @@ function Details(_ref5) {
);
}
-},{"../../utils.js":60,"lodash":"lodash","react":"react"}],22:[function(require,module,exports){
+},{"../../utils.js":62,"lodash":"lodash","react":"react"}],22:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -2268,6 +2339,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
@@ -2488,12 +2563,12 @@ var Headers = function (_Component2) {
}(_react.Component);
Headers.propTypes = {
- onChange: _react.PropTypes.func.isRequired,
- message: _react.PropTypes.object.isRequired
+ onChange: _propTypes2.default.func.isRequired,
+ message: _propTypes2.default.object.isRequired
};
exports.default = Headers;
-},{"../../utils":60,"../ValueEditor/ValueEditor":39,"react":"react","react-dom":"react-dom"}],23:[function(require,module,exports){
+},{"../../utils":62,"../ValueEditor/ValueEditor":40,"prop-types":"prop-types","react":"react","react-dom":"react-dom"}],23:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -2511,6 +2586,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _utils = require('../../flow/utils.js');
@@ -2829,7 +2908,7 @@ var Response = exports.Response = function (_Component2) {
exports.Response = Response = Message(Response);
ErrorView.propTypes = {
- flow: _react.PropTypes.object.isRequired
+ flow: _propTypes2.default.object.isRequired
};
function ErrorView(_ref3) {
@@ -2855,7 +2934,7 @@ function ErrorView(_ref3) {
);
}
-},{"../../ducks/flows":49,"../../ducks/ui/flow":52,"../../flow/utils.js":58,"../../utils.js":60,"../ContentView":4,"../ContentView/ContentViewOptions":7,"../ValueEditor/ValidateEditor":38,"../ValueEditor/ValueEditor":39,"./Headers":22,"./ToggleEdit":25,"react":"react","react-redux":"react-redux"}],24:[function(require,module,exports){
+},{"../../ducks/flows":51,"../../ducks/ui/flow":54,"../../flow/utils.js":60,"../../utils.js":62,"../ContentView":4,"../ContentView/ContentViewOptions":7,"../ValueEditor/ValidateEditor":39,"../ValueEditor/ValueEditor":40,"./Headers":22,"./ToggleEdit":25,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],24:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -2867,6 +2946,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _classnames = require('classnames');
@@ -2876,9 +2959,9 @@ var _classnames2 = _interopRequireDefault(_classnames);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
NavAction.propTypes = {
- icon: _react.PropTypes.string.isRequired,
- title: _react.PropTypes.string.isRequired,
- onClick: _react.PropTypes.func.isRequired
+ icon: _propTypes2.default.string.isRequired,
+ title: _propTypes2.default.string.isRequired,
+ onClick: _propTypes2.default.func.isRequired
};
function NavAction(_ref) {
@@ -2900,9 +2983,9 @@ function NavAction(_ref) {
}
Nav.propTypes = {
- active: _react.PropTypes.string.isRequired,
- tabs: _react.PropTypes.array.isRequired,
- onSelectTab: _react.PropTypes.func.isRequired
+ active: _propTypes2.default.string.isRequired,
+ tabs: _propTypes2.default.array.isRequired,
+ onSelectTab: _propTypes2.default.func.isRequired
};
function Nav(_ref2) {
@@ -2929,7 +3012,7 @@ function Nav(_ref2) {
);
}
-},{"classnames":"classnames","react":"react","react-redux":"react-redux"}],25:[function(require,module,exports){
+},{"classnames":"classnames","prop-types":"prop-types","react":"react","react-redux":"react-redux"}],25:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -2940,6 +3023,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _flow = require('../../ducks/ui/flow');
@@ -2947,10 +3034,10 @@ var _flow = require('../../ducks/ui/flow');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
ToggleEdit.propTypes = {
- isEdit: _react.PropTypes.bool.isRequired,
- flow: _react.PropTypes.object.isRequired,
- startEdit: _react.PropTypes.func.isRequired,
- stopEdit: _react.PropTypes.func.isRequired
+ isEdit: _propTypes2.default.bool.isRequired,
+ flow: _propTypes2.default.object.isRequired,
+ startEdit: _propTypes2.default.func.isRequired,
+ stopEdit: _propTypes2.default.func.isRequired
};
function ToggleEdit(_ref) {
@@ -2990,7 +3077,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
stopEdit: _flow.stopEdit
})(ToggleEdit);
-},{"../../ducks/ui/flow":52,"react":"react","react-redux":"react-redux"}],26:[function(require,module,exports){
+},{"../../ducks/ui/flow":54,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],26:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -3001,6 +3088,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _utils = require('../utils.js');
@@ -3008,7 +3099,7 @@ var _utils = require('../utils.js');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
Footer.propTypes = {
- settings: _react2.default.PropTypes.object.isRequired
+ settings: _propTypes2.default.object.isRequired
};
function Footer(_ref) {
@@ -3027,7 +3118,8 @@ function Footer(_ref) {
stream_large_bodies = settings.stream_large_bodies,
listen_host = settings.listen_host,
listen_port = settings.listen_port,
- version = settings.version;
+ version = settings.version,
+ server = settings.server;
return _react2.default.createElement(
'footer',
@@ -3100,7 +3192,7 @@ function Footer(_ref) {
_react2.default.createElement(
'div',
{ className: 'pull-right' },
- _react2.default.createElement(
+ server && _react2.default.createElement(
'span',
{ className: 'label label-primary', title: 'HTTP Proxy Server Address' },
listen_host || "*",
@@ -3123,7 +3215,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
};
})(Footer);
-},{"../utils.js":60,"react":"react","react-redux":"react-redux"}],27:[function(require,module,exports){
+},{"../utils.js":62,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],27:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -3136,6 +3228,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _classnames = require('classnames');
@@ -3160,6 +3256,10 @@ var _FlowMenu2 = _interopRequireDefault(_FlowMenu);
var _header = require('../ducks/ui/header');
+var _ConnectionIndicator = require('./Header/ConnectionIndicator');
+
+var _ConnectionIndicator2 = _interopRequireDefault(_ConnectionIndicator);
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
@@ -3222,10 +3322,11 @@ var Header = function (_Component) {
} },
Entry.title
);
- })
+ }),
+ _react2.default.createElement(_ConnectionIndicator2.default, null)
),
_react2.default.createElement(
- 'menu',
+ 'div',
null,
_react2.default.createElement(Active, null)
)
@@ -3246,7 +3347,76 @@ exports.default = (0, _reactRedux.connect)(function (state) {
setActiveMenu: _header.setActiveMenu
})(Header);
-},{"../ducks/ui/header":53,"./Header/FileMenu":28,"./Header/FlowMenu":31,"./Header/MainMenu":32,"./Header/OptionMenu":34,"classnames":"classnames","react":"react","react-redux":"react-redux"}],28:[function(require,module,exports){
+},{"../ducks/ui/header":55,"./Header/ConnectionIndicator":28,"./Header/FileMenu":29,"./Header/FlowMenu":32,"./Header/MainMenu":33,"./Header/OptionMenu":35,"classnames":"classnames","prop-types":"prop-types","react":"react","react-redux":"react-redux"}],28:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _react = require("react");
+
+var _react2 = _interopRequireDefault(_react);
+
+var _propTypes = require("prop-types");
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
+var _reactRedux = require("react-redux");
+
+var _connection = require("../../ducks/connection");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+ConnectionIndicator.propTypes = {
+ state: _propTypes2.default.symbol.isRequired,
+ message: _propTypes2.default.string
+
+};
+function ConnectionIndicator(_ref) {
+ var state = _ref.state,
+ message = _ref.message;
+
+ switch (state) {
+ case _connection.ConnectionState.INIT:
+ return _react2.default.createElement(
+ "span",
+ { className: "connection-indicator init" },
+ "connecting\u2026"
+ );
+ case _connection.ConnectionState.FETCHING:
+ return _react2.default.createElement(
+ "span",
+ { className: "connection-indicator fetching" },
+ "fetching data\u2026"
+ );
+ case _connection.ConnectionState.ESTABLISHED:
+ return _react2.default.createElement(
+ "span",
+ { className: "connection-indicator established" },
+ "connected"
+ );
+ case _connection.ConnectionState.ERROR:
+ return _react2.default.createElement(
+ "span",
+ { className: "connection-indicator error",
+ title: message },
+ "connection lost"
+ );
+ case _connection.ConnectionState.OFFLINE:
+ return _react2.default.createElement(
+ "span",
+ { className: "connection-indicator offline" },
+ "offline"
+ );
+ }
+}
+
+exports.default = (0, _reactRedux.connect)(function (state) {
+ return state.connection;
+})(ConnectionIndicator);
+
+},{"../../ducks/connection":49,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],29:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -3257,6 +3427,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _FileChooser = require('../common/FileChooser');
@@ -3276,9 +3450,9 @@ function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
FileMenu.propTypes = {
- clearFlows: _react.PropTypes.func.isRequired,
- loadFlows: _react.PropTypes.func.isRequired,
- saveFlows: _react.PropTypes.func.isRequired
+ clearFlows: _propTypes2.default.func.isRequired,
+ loadFlows: _propTypes2.default.func.isRequired,
+ saveFlows: _propTypes2.default.func.isRequired
};
FileMenu.onNewClick = function (e, clearFlows) {
@@ -3333,7 +3507,7 @@ exports.default = (0, _reactRedux.connect)(null, {
saveFlows: flowsActions.download
})(FileMenu);
-},{"../../ducks/flows":49,"../common/Dropdown":42,"../common/FileChooser":43,"react":"react","react-redux":"react-redux"}],29:[function(require,module,exports){
+},{"../../ducks/flows":51,"../common/Dropdown":43,"../common/FileChooser":44,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],30:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -3393,6 +3567,8 @@ var FilterDocs = function (_Component) {
}, {
key: 'render',
value: function render() {
+ var _this3 = this;
+
var doc = this.state.doc;
return !doc ? _react2.default.createElement('i', { className: 'fa fa-spinner fa-spin' }) : _react2.default.createElement(
@@ -3404,7 +3580,9 @@ var FilterDocs = function (_Component) {
doc.commands.map(function (cmd) {
return _react2.default.createElement(
'tr',
- { key: cmd[1] },
+ { key: cmd[1], onClick: function onClick(e) {
+ return _this3.props.selectHandler(cmd[0].split(" ")[0] + " ");
+ } },
_react2.default.createElement(
'td',
null,
@@ -3444,7 +3622,7 @@ FilterDocs.xhr = null;
FilterDocs.doc = null;
exports.default = FilterDocs;
-},{"../../utils":60,"react":"react"}],30:[function(require,module,exports){
+},{"../../utils":62,"react":"react"}],31:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -3457,6 +3635,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
@@ -3502,6 +3684,7 @@ var FilterInput = function (_Component) {
_this.onKeyDown = _this.onKeyDown.bind(_this);
_this.onMouseEnter = _this.onMouseEnter.bind(_this);
_this.onMouseLeave = _this.onMouseLeave.bind(_this);
+ _this.selectFilter = _this.selectFilter.bind(_this);
return _this;
}
@@ -3527,7 +3710,7 @@ var FilterInput = function (_Component) {
key: 'getDesc',
value: function getDesc() {
if (!this.state.value) {
- return _react2.default.createElement(_FilterDocs2.default, null);
+ return _react2.default.createElement(_FilterDocs2.default, { selectHandler: this.selectFilter });
}
try {
return _filt2.default.parse(this.state.value).desc;
@@ -3577,6 +3760,12 @@ var FilterInput = function (_Component) {
e.stopPropagation();
}
}, {
+ key: 'selectFilter',
+ value: function selectFilter(cmd) {
+ this.setState({ value: cmd });
+ _reactDom2.default.findDOMNode(this.refs.input).focus();
+ }
+ }, {
key: 'blur',
value: function blur() {
_reactDom2.default.findDOMNode(this.refs.input).blur();
@@ -3638,7 +3827,7 @@ var FilterInput = function (_Component) {
exports.default = FilterInput;
-},{"../../filt/filt":57,"../../utils.js":60,"./FilterDocs":29,"classnames":"classnames","react":"react","react-dom":"react-dom"}],31:[function(require,module,exports){
+},{"../../filt/filt":59,"../../utils.js":62,"./FilterDocs":30,"classnames":"classnames","prop-types":"prop-types","react":"react","react-dom":"react-dom"}],32:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -3649,6 +3838,10 @@ var _react = require("react");
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require("prop-types");
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require("react-redux");
var _Button = require("../common/Button");
@@ -3668,13 +3861,13 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
FlowMenu.title = 'Flow';
FlowMenu.propTypes = {
- flow: _react.PropTypes.object,
- resumeFlow: _react.PropTypes.func.isRequired,
- killFlow: _react.PropTypes.func.isRequired,
- replayFlow: _react.PropTypes.func.isRequired,
- duplicateFlow: _react.PropTypes.func.isRequired,
- removeFlow: _react.PropTypes.func.isRequired,
- revertFlow: _react.PropTypes.func.isRequired
+ flow: _propTypes2.default.object,
+ resumeFlow: _propTypes2.default.func.isRequired,
+ killFlow: _propTypes2.default.func.isRequired,
+ replayFlow: _propTypes2.default.func.isRequired,
+ duplicateFlow: _propTypes2.default.func.isRequired,
+ removeFlow: _propTypes2.default.func.isRequired,
+ revertFlow: _propTypes2.default.func.isRequired
};
function FlowMenu(_ref) {
@@ -3801,7 +3994,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
revertFlow: flowsActions.revert
})(FlowMenu);
-},{"../../ducks/flows":49,"../../flow/utils.js":58,"../common/Button":40,"react":"react","react-redux":"react-redux"}],32:[function(require,module,exports){
+},{"../../ducks/flows":51,"../../flow/utils.js":60,"../common/Button":41,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],33:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -3813,6 +4006,10 @@ var _react = require("react");
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require("prop-types");
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require("react-redux");
var _FilterInput = require("./FilterInput");
@@ -3866,7 +4063,7 @@ var HighlightInput = (0, _reactRedux.connect)(function (state) {
};
}, { onChange: _flows.setHighlight })(_FilterInput2.default);
-},{"../../ducks/flows":49,"../../ducks/settings":51,"./FilterInput":30,"react":"react","react-redux":"react-redux"}],33:[function(require,module,exports){
+},{"../../ducks/flows":51,"../../ducks/settings":53,"./FilterInput":31,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],34:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -3876,7 +4073,9 @@ exports.MenuToggle = MenuToggle;
exports.SettingsToggle = SettingsToggle;
exports.EventlogToggle = EventlogToggle;
-var _react = require("react");
+var _propTypes = require("prop-types");
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
var _reactRedux = require("react-redux");
@@ -3884,12 +4083,14 @@ var _settings = require("../../ducks/settings");
var _eventLog = require("../../ducks/eventLog");
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
MenuToggle.propTypes = {
- value: _react.PropTypes.bool.isRequired,
- onChange: _react.PropTypes.func.isRequired,
- children: _react.PropTypes.node.isRequired
+ value: _propTypes2.default.bool.isRequired,
+ onChange: _propTypes2.default.func.isRequired,
+ children: _propTypes2.default.node.isRequired
};
function MenuToggle(_ref) {
@@ -3912,8 +4113,8 @@ function MenuToggle(_ref) {
}
SettingsToggle.propTypes = {
- setting: _react.PropTypes.string.isRequired,
- children: _react.PropTypes.node.isRequired
+ setting: _propTypes2.default.string.isRequired,
+ children: _propTypes2.default.node.isRequired
};
function SettingsToggle(_ref2) {
@@ -3962,7 +4163,7 @@ exports.EventlogToggle = EventlogToggle = (0, _reactRedux.connect)(function (sta
toggleVisibility: _eventLog.toggleVisibility
})(EventlogToggle);
-},{"../../ducks/eventLog":48,"../../ducks/settings":51,"react":"react","react-redux":"react-redux"}],34:[function(require,module,exports){
+},{"../../ducks/eventLog":50,"../../ducks/settings":53,"prop-types":"prop-types","react-redux":"react-redux"}],35:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -3974,6 +4175,10 @@ var _react = require("react");
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require("prop-types");
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require("react-redux");
var _MenuToggle = require("./MenuToggle");
@@ -4068,7 +4273,7 @@ function OptionMenu() {
);
}
-},{"../common/DocsLink":41,"./MenuToggle":33,"react":"react","react-redux":"react-redux"}],35:[function(require,module,exports){
+},{"../common/DocsLink":42,"./MenuToggle":34,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],36:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -4081,6 +4286,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _Splitter = require('./common/Splitter');
@@ -4155,8 +4364,8 @@ var MainView = function (_Component) {
}(_react.Component);
MainView.propTypes = {
- highlight: _react.PropTypes.string,
- sort: _react.PropTypes.object
+ highlight: _propTypes2.default.string,
+ sort: _propTypes2.default.object
};
exports.default = (0, _reactRedux.connect)(function (state) {
return {
@@ -4171,7 +4380,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
updateFlow: flowsActions.update
})(MainView);
-},{"../ducks/flows":49,"./FlowTable":16,"./FlowView":20,"./common/Splitter":44,"react":"react","react-redux":"react-redux"}],36:[function(require,module,exports){
+},{"../ducks/flows":51,"./FlowTable":16,"./FlowView":20,"./common/Splitter":45,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],37:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -4183,6 +4392,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
@@ -4196,9 +4409,9 @@ var _utils = require('../utils.js');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
Prompt.propTypes = {
- options: _react.PropTypes.array.isRequired,
- done: _react.PropTypes.func.isRequired,
- prompt: _react.PropTypes.string
+ options: _propTypes2.default.array.isRequired,
+ done: _propTypes2.default.func.isRequired,
+ prompt: _propTypes2.default.string
};
function Prompt(_ref) {
@@ -4272,7 +4485,7 @@ function Prompt(_ref) {
);
}
-},{"../utils.js":60,"lodash":"lodash","react":"react","react-dom":"react-dom"}],37:[function(require,module,exports){
+},{"../utils.js":62,"lodash":"lodash","prop-types":"prop-types","react":"react","react-dom":"react-dom"}],38:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -4285,6 +4498,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _reactRedux = require('react-redux');
var _keyboard = require('../ducks/ui/keyboard');
@@ -4363,7 +4580,7 @@ exports.default = (0, _reactRedux.connect)(function (state) {
onKeyDown: _keyboard.onKeyDown
})(ProxyAppMain);
-},{"../ducks/ui/keyboard":55,"./EventLog":14,"./Footer":26,"./Header":27,"./MainView":35,"react":"react","react-redux":"react-redux"}],38:[function(require,module,exports){
+},{"../ducks/ui/keyboard":57,"./EventLog":14,"./Footer":26,"./Header":27,"./MainView":36,"prop-types":"prop-types","react":"react","react-redux":"react-redux"}],39:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -4376,6 +4593,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _ValueEditor = require('./ValueEditor');
var _ValueEditor2 = _interopRequireDefault(_ValueEditor);
@@ -4451,15 +4672,15 @@ var ValidateEditor = function (_Component) {
}(_react.Component);
ValidateEditor.propTypes = {
- content: _react.PropTypes.string.isRequired,
- readonly: _react.PropTypes.bool,
- onDone: _react.PropTypes.func.isRequired,
- className: _react.PropTypes.string,
- isValid: _react.PropTypes.func.isRequired
+ content: _propTypes2.default.string.isRequired,
+ readonly: _propTypes2.default.bool,
+ onDone: _propTypes2.default.func.isRequired,
+ className: _propTypes2.default.string,
+ isValid: _propTypes2.default.func.isRequired
};
exports.default = ValidateEditor;
-},{"./ValueEditor":39,"classnames":"classnames","react":"react"}],39:[function(require,module,exports){
+},{"./ValueEditor":40,"classnames":"classnames","prop-types":"prop-types","react":"react"}],40:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -4472,6 +4693,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _lodash = require('lodash');
var _lodash2 = _interopRequireDefault(_lodash);
@@ -4659,12 +4884,12 @@ var ValueEditor = function (_Component) {
}(_react.Component);
ValueEditor.propTypes = {
- content: _react.PropTypes.string.isRequired,
- readonly: _react.PropTypes.bool,
- onDone: _react.PropTypes.func.isRequired,
- className: _react.PropTypes.string,
- onInput: _react.PropTypes.func,
- onKeyDown: _react.PropTypes.func
+ content: _propTypes2.default.string.isRequired,
+ readonly: _propTypes2.default.bool,
+ onDone: _propTypes2.default.func.isRequired,
+ className: _propTypes2.default.string,
+ onInput: _propTypes2.default.func,
+ onKeyDown: _propTypes2.default.func
};
ValueEditor.defaultProps = {
onInput: function onInput() {},
@@ -4672,7 +4897,7 @@ ValueEditor.defaultProps = {
};
exports.default = ValueEditor;
-},{"../../utils":60,"classnames":"classnames","lodash":"lodash","react":"react"}],40:[function(require,module,exports){
+},{"../../utils":62,"classnames":"classnames","lodash":"lodash","prop-types":"prop-types","react":"react"}],41:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -4684,6 +4909,10 @@ var _react = require("react");
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require("prop-types");
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _classnames = require("classnames");
var _classnames2 = _interopRequireDefault(_classnames);
@@ -4691,10 +4920,10 @@ var _classnames2 = _interopRequireDefault(_classnames);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
Button.propTypes = {
- onClick: _react.PropTypes.func.isRequired,
- children: _react.PropTypes.node.isRequired,
- icon: _react.PropTypes.string,
- title: _react.PropTypes.string
+ onClick: _propTypes2.default.func.isRequired,
+ children: _propTypes2.default.node.isRequired,
+ icon: _propTypes2.default.string,
+ title: _propTypes2.default.string
};
function Button(_ref) {
@@ -4716,7 +4945,7 @@ function Button(_ref) {
);
}
-},{"classnames":"classnames","react":"react"}],41:[function(require,module,exports){
+},{"classnames":"classnames","prop-types":"prop-types","react":"react"}],42:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -4724,10 +4953,14 @@ Object.defineProperty(exports, "__esModule", {
});
exports.default = DocsLink;
-var _react = require("react");
+var _propTypes = require("prop-types");
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
DocsLink.propTypes = {
- resource: _react.PropTypes.string.isRequired
+ resource: _propTypes2.default.string.isRequired
};
function DocsLink(_ref) {
@@ -4742,7 +4975,7 @@ function DocsLink(_ref) {
);
}
-},{"react":"react"}],42:[function(require,module,exports){
+},{"prop-types":"prop-types"}],43:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -4756,6 +4989,10 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
@@ -4842,16 +5079,16 @@ var Dropdown = function (_Component) {
}(_react.Component);
Dropdown.propTypes = {
- dropup: _react.PropTypes.bool,
- className: _react.PropTypes.string,
- btnClass: _react.PropTypes.string.isRequired
+ dropup: _propTypes2.default.bool,
+ className: _propTypes2.default.string,
+ btnClass: _propTypes2.default.string.isRequired
};
Dropdown.defaultProps = {
dropup: false
};
exports.default = Dropdown;
-},{"classnames":"classnames","react":"react"}],43:[function(require,module,exports){
+},{"classnames":"classnames","prop-types":"prop-types","react":"react"}],44:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -4863,14 +5100,18 @@ var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
FileChooser.propTypes = {
- icon: _react.PropTypes.string,
- text: _react.PropTypes.string,
- className: _react.PropTypes.string,
- title: _react.PropTypes.string,
- onOpenFile: _react.PropTypes.func.isRequired
+ icon: _propTypes2.default.string,
+ text: _propTypes2.default.string,
+ className: _propTypes2.default.string,
+ title: _propTypes2.default.string,
+ onOpenFile: _propTypes2.default.func.isRequired
};
function FileChooser(_ref) {
@@ -4903,7 +5144,7 @@ function FileChooser(_ref) {
);
}
-},{"react":"react"}],44:[function(require,module,exports){
+},{"prop-types":"prop-types","react":"react"}],45:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -5048,24 +5289,28 @@ var Splitter = function (_Component) {
Splitter.defaultProps = { axis: 'x' };
exports.default = Splitter;
-},{"classnames":"classnames","react":"react","react-dom":"react-dom"}],45:[function(require,module,exports){
-"use strict";
+},{"classnames":"classnames","react":"react","react-dom":"react-dom"}],46:[function(require,module,exports){
+'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = ToggleButton;
-var _react = require("react");
+var _react = require('react');
var _react2 = _interopRequireDefault(_react);
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
ToggleButton.propTypes = {
- checked: _react.PropTypes.bool.isRequired,
- onToggle: _react.PropTypes.func.isRequired,
- text: _react.PropTypes.string.isRequired
+ checked: _propTypes2.default.bool.isRequired,
+ onToggle: _propTypes2.default.func.isRequired,
+ text: _propTypes2.default.string.isRequired
};
function ToggleButton(_ref) {
@@ -5074,15 +5319,15 @@ function ToggleButton(_ref) {
text = _ref.text;
return _react2.default.createElement(
- "div",
+ 'div',
{ className: "btn btn-toggle " + (checked ? "btn-primary" : "btn-default"), onClick: onToggle },
- _react2.default.createElement("i", { className: "fa fa-fw " + (checked ? "fa-check-square-o" : "fa-square-o") }),
- "\xA0",
+ _react2.default.createElement('i', { className: "fa fa-fw " + (checked ? "fa-check-square-o" : "fa-square-o") }),
+ '\xA0',
text
);
}
-},{"react":"react"}],46:[function(require,module,exports){
+},{"prop-types":"prop-types","react":"react"}],47:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -5148,7 +5393,7 @@ exports.default = function (Component) {
}(Component), _class.displayName = Component.name, _temp), Component);
};
-},{"react":"react","react-dom":"react-dom"}],47:[function(require,module,exports){
+},{"react":"react","react-dom":"react-dom"}],48:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -5227,7 +5472,65 @@ function calcVScroll(opts) {
return { start: start, end: end, paddingTop: paddingTop, paddingBottom: paddingBottom };
}
-},{}],48:[function(require,module,exports){
+},{}],49:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = reducer;
+exports.startFetching = startFetching;
+exports.connectionEstablished = connectionEstablished;
+exports.connectionError = connectionError;
+exports.setOffline = setOffline;
+var ConnectionState = exports.ConnectionState = {
+ INIT: Symbol("init"),
+ FETCHING: Symbol("fetching"), // WebSocket is established, but still startFetching resources.
+ ESTABLISHED: Symbol("established"),
+ ERROR: Symbol("error"),
+ OFFLINE: Symbol("offline") };
+
+var defaultState = {
+ state: ConnectionState.INIT,
+ message: null
+};
+
+function reducer() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultState;
+ var action = arguments[1];
+
+ switch (action.type) {
+
+ case ConnectionState.ESTABLISHED:
+ case ConnectionState.FETCHING:
+ case ConnectionState.ERROR:
+ case ConnectionState.OFFLINE:
+ return {
+ state: action.type,
+ message: action.message
+ };
+
+ default:
+ return state;
+ }
+}
+
+function startFetching() {
+ return { type: ConnectionState.FETCHING };
+}
+
+function connectionEstablished() {
+ return { type: ConnectionState.ESTABLISHED };
+}
+
+function connectionError(message) {
+ return { type: ConnectionState.ERROR, message: message };
+}
+function setOffline() {
+ return { type: ConnectionState.OFFLINE };
+}
+
+},{}],50:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -5257,7 +5560,7 @@ var TOGGLE_FILTER = exports.TOGGLE_FILTER = 'EVENTS_TOGGLE_FILTER';
var defaultState = _extends({
visible: false,
- filters: { debug: false, info: true, web: true }
+ filters: { debug: false, info: true, web: true, warn: true, error: true }
}, (0, storeActions.default)(undefined, {}));
function reduce() {
@@ -5313,7 +5616,7 @@ function add(message) {
};
}
-},{"./utils/store":56}],49:[function(require,module,exports){
+},{"./utils/store":58}],51:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -5388,8 +5691,6 @@ function reduce() {
case UPDATE:
case REMOVE:
case RECEIVE:
- // FIXME: Update state.selected on REMOVE:
- // The selected flow may have been removed, we need to select the next one in the view.
var storeAction = storeActions[action.cmd](action.data, makeFilter(state.filter), makeSort(state.sort));
var selected = state.selected;
@@ -5514,22 +5815,20 @@ function setSort(column, desc) {
return { type: SET_SORT, sort: { column: column, desc: desc } };
}
-function selectRelative(shift) {
- return function (dispatch, getState) {
- var currentSelectionIndex = getState().flows.viewIndex[getState().flows.selected[0]];
- var minIndex = 0;
- var maxIndex = getState().flows.view.length - 1;
- var newIndex = void 0;
- if (currentSelectionIndex === undefined) {
- newIndex = shift < 0 ? minIndex : maxIndex;
- } else {
- newIndex = currentSelectionIndex + shift;
- newIndex = window.Math.max(newIndex, minIndex);
- newIndex = window.Math.min(newIndex, maxIndex);
- }
- var flow = getState().flows.view[newIndex];
- dispatch(select(flow ? flow.id : undefined));
- };
+function selectRelative(flows, shift) {
+ var currentSelectionIndex = flows.viewIndex[flows.selected[0]];
+ var minIndex = 0;
+ var maxIndex = flows.view.length - 1;
+ var newIndex = void 0;
+ if (currentSelectionIndex === undefined) {
+ newIndex = shift < 0 ? minIndex : maxIndex;
+ } else {
+ newIndex = currentSelectionIndex + shift;
+ newIndex = window.Math.max(newIndex, minIndex);
+ newIndex = window.Math.min(newIndex, maxIndex);
+ }
+ var flow = flows.view[newIndex];
+ return select(flow ? flow.id : undefined);
}
function resume(flow) {
@@ -5591,7 +5890,7 @@ function uploadContent(flow, file, type) {
file = new window.Blob([file], { type: 'plain/text' });
body.append('file', file);
return function (dispatch) {
- return (0, _utils.fetchApi)("/flows/" + flow.id + "/" + type + "/content", { method: 'post', body: body });
+ return (0, _utils.fetchApi)("/flows/" + flow.id + "/" + type + "/content", { method: 'POST', body: body });
};
}
@@ -5610,7 +5909,7 @@ function upload(file) {
var body = new FormData();
body.append('file', file);
return function (dispatch) {
- return (0, _utils.fetchApi)('/flows/dump', { method: 'post', body: body });
+ return (0, _utils.fetchApi)('/flows/dump', { method: 'POST', body: body });
};
}
@@ -5621,41 +5920,46 @@ function select(id) {
};
}
-},{"../filt/filt":57,"../flow/utils":58,"../utils":60,"./utils/store":56}],50:[function(require,module,exports){
-'use strict';
+},{"../filt/filt":59,"../flow/utils":60,"../utils":62,"./utils/store":58}],52:[function(require,module,exports){
+"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
-var _redux = require('redux');
+var _redux = require("redux");
-var _eventLog = require('./eventLog');
+var _eventLog = require("./eventLog");
var _eventLog2 = _interopRequireDefault(_eventLog);
-var _flows = require('./flows');
+var _flows = require("./flows");
var _flows2 = _interopRequireDefault(_flows);
-var _settings = require('./settings');
+var _settings = require("./settings");
var _settings2 = _interopRequireDefault(_settings);
-var _index = require('./ui/index');
+var _index = require("./ui/index");
var _index2 = _interopRequireDefault(_index);
+var _connection = require("./connection");
+
+var _connection2 = _interopRequireDefault(_connection);
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
exports.default = (0, _redux.combineReducers)({
eventLog: _eventLog2.default,
flows: _flows2.default,
settings: _settings2.default,
+ connection: _connection2.default,
ui: _index2.default
});
-},{"./eventLog":48,"./flows":49,"./settings":51,"./ui/index":54,"redux":"redux"}],51:[function(require,module,exports){
+},{"./connection":49,"./eventLog":50,"./flows":51,"./settings":53,"./ui/index":56,"redux":"redux"}],53:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -5699,7 +6003,7 @@ function update(settings) {
return { type: REQUEST_UPDATE };
}
-},{"../utils":60}],52:[function(require,module,exports){
+},{"../utils":62}],54:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -5759,7 +6063,7 @@ function reducer() {
var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultState;
var action = arguments[1];
- var wasInEditMode = !!state.modifiedFlow;
+ var wasInEditMode = state.modifiedFlow;
var content = action.content || state.content;
var isFullContentShown = content && content.length <= state.maxContentLines;
@@ -5815,13 +6119,13 @@ function reducer() {
return _extends({}, state, {
tab: action.tab ? action.tab : 'request',
displayLarge: false,
- showFullContent: state.contentView == 'Edit'
+ showFullContent: state.contentView === 'Edit'
});
case SET_CONTENT_VIEW:
return _extends({}, state, {
contentView: action.contentView,
- showFullContent: action.contentView == 'Edit'
+ showFullContent: action.contentView === 'Edit'
});
case SET_CONTENT:
@@ -5871,11 +6175,12 @@ function setContent(content) {
return { type: SET_CONTENT, content: content };
}
-function stopEdit(flow, modifiedFlow) {
- return flowsActions.update(flow, (0, _utils.getDiff)(flow, modifiedFlow));
+function stopEdit(data, modifiedFlow) {
+ var diff = (0, _utils.getDiff)(data, modifiedFlow);
+ return { type: flowsActions.UPDATE, data: data, diff: diff };
}
-},{"../../utils":60,"../flows":49,"lodash":"lodash"}],53:[function(require,module,exports){
+},{"../../utils":62,"../flows":51,"lodash":"lodash"}],55:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -5924,7 +6229,7 @@ function reducer() {
// Deselect
if (action.flowIds.length === 0 && state.isFlowSelected) {
var activeMenu = state.activeMenu;
- if (activeMenu == 'Flow') {
+ if (activeMenu === 'Flow') {
activeMenu = 'Start';
}
return _extends({}, state, {
@@ -5942,7 +6247,7 @@ function setActiveMenu(activeMenu) {
return { type: SET_ACTIVE_MENU, activeMenu: activeMenu };
}
-},{"../flows":49}],54:[function(require,module,exports){
+},{"../flows":51}],56:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -5967,7 +6272,7 @@ exports.default = (0, _redux.combineReducers)({
header: _header2.default
});
-},{"./flow":52,"./header":53,"redux":"redux"}],55:[function(require,module,exports){
+},{"./flow":54,"./header":55,"redux":"redux"}],57:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -5990,39 +6295,40 @@ function onKeyDown(e) {
if (e.ctrlKey) {
return function () {};
}
- var key = e.keyCode;
- var shiftKey = e.shiftKey;
+ var key = e.keyCode,
+ shiftKey = e.shiftKey;
e.preventDefault();
return function (dispatch, getState) {
- var flow = getState().flows.byId[getState().flows.selected[0]];
+ var flows = getState().flows,
+ flow = flows.byId[getState().flows.selected[0]];
switch (key) {
case _utils.Key.K:
case _utils.Key.UP:
- dispatch(flowsActions.selectRelative(-1));
+ dispatch(flowsActions.selectRelative(flows, -1));
break;
case _utils.Key.J:
case _utils.Key.DOWN:
- dispatch(flowsActions.selectRelative(+1));
+ dispatch(flowsActions.selectRelative(flows, +1));
break;
case _utils.Key.SPACE:
case _utils.Key.PAGE_DOWN:
- dispatch(flowsActions.selectRelative(+10));
+ dispatch(flowsActions.selectRelative(flows, +10));
break;
case _utils.Key.PAGE_UP:
- dispatch(flowsActions.selectRelative(-10));
+ dispatch(flowsActions.selectRelative(flows, -10));
break;
case _utils.Key.END:
- dispatch(flowsActions.selectRelative(+1e10));
+ dispatch(flowsActions.selectRelative(flows, +1e10));
break;
case _utils.Key.HOME:
- dispatch(flowsActions.selectRelative(-1e10));
+ dispatch(flowsActions.selectRelative(flows, -1e10));
break;
case _utils.Key.ESC:
@@ -6117,7 +6423,7 @@ function onKeyDown(e) {
};
}
-},{"../../utils":60,"../flows":49,"./flow":52}],56:[function(require,module,exports){
+},{"../../utils":62,"../flows":51,"./flow":54}],58:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -6387,7 +6693,7 @@ function defaultSort(a, b) {
return 0;
}
-},{}],57:[function(require,module,exports){
+},{}],59:[function(require,module,exports){
"use strict";
module.exports = function () {
@@ -6580,47 +6886,52 @@ module.exports = function () {
peg$c94 = function peg$c94(s) {
return url(s);
},
- peg$c95 = { type: "other", description: "integer" },
- peg$c96 = /^['"]/,
- peg$c97 = { type: "class", value: "['\"]", description: "['\"]" },
- peg$c98 = /^[0-9]/,
- peg$c99 = { type: "class", value: "[0-9]", description: "[0-9]" },
- peg$c100 = function peg$c100(digits) {
+ peg$c95 = "~websocket",
+ peg$c96 = { type: "literal", value: "~websocket", description: "\"~websocket\"" },
+ peg$c97 = function peg$c97() {
+ return websocketFilter;
+ },
+ peg$c98 = { type: "other", description: "integer" },
+ peg$c99 = /^['"]/,
+ peg$c100 = { type: "class", value: "['\"]", description: "['\"]" },
+ peg$c101 = /^[0-9]/,
+ peg$c102 = { type: "class", value: "[0-9]", description: "[0-9]" },
+ peg$c103 = function peg$c103(digits) {
return parseInt(digits.join(""), 10);
},
- peg$c101 = { type: "other", description: "string" },
- peg$c102 = "\"",
- peg$c103 = { type: "literal", value: "\"", description: "\"\\\"\"" },
- peg$c104 = function peg$c104(chars) {
+ peg$c104 = { type: "other", description: "string" },
+ peg$c105 = "\"",
+ peg$c106 = { type: "literal", value: "\"", description: "\"\\\"\"" },
+ peg$c107 = function peg$c107(chars) {
return chars.join("");
},
- peg$c105 = "'",
- peg$c106 = { type: "literal", value: "'", description: "\"'\"" },
- peg$c107 = /^["\\]/,
- peg$c108 = { type: "class", value: "[\"\\\\]", description: "[\"\\\\]" },
- peg$c109 = { type: "any", description: "any character" },
- peg$c110 = function peg$c110(char) {
+ peg$c108 = "'",
+ peg$c109 = { type: "literal", value: "'", description: "\"'\"" },
+ peg$c110 = /^["\\]/,
+ peg$c111 = { type: "class", value: "[\"\\\\]", description: "[\"\\\\]" },
+ peg$c112 = { type: "any", description: "any character" },
+ peg$c113 = function peg$c113(char) {
return char;
},
- peg$c111 = "\\",
- peg$c112 = { type: "literal", value: "\\", description: "\"\\\\\"" },
- peg$c113 = /^['\\]/,
- peg$c114 = { type: "class", value: "['\\\\]", description: "['\\\\]" },
- peg$c115 = /^['"\\]/,
- peg$c116 = { type: "class", value: "['\"\\\\]", description: "['\"\\\\]" },
- peg$c117 = "n",
- peg$c118 = { type: "literal", value: "n", description: "\"n\"" },
- peg$c119 = function peg$c119() {
+ peg$c114 = "\\",
+ peg$c115 = { type: "literal", value: "\\", description: "\"\\\\\"" },
+ peg$c116 = /^['\\]/,
+ peg$c117 = { type: "class", value: "['\\\\]", description: "['\\\\]" },
+ peg$c118 = /^['"\\]/,
+ peg$c119 = { type: "class", value: "['\"\\\\]", description: "['\"\\\\]" },
+ peg$c120 = "n",
+ peg$c121 = { type: "literal", value: "n", description: "\"n\"" },
+ peg$c122 = function peg$c122() {
return "\n";
},
- peg$c120 = "r",
- peg$c121 = { type: "literal", value: "r", description: "\"r\"" },
- peg$c122 = function peg$c122() {
+ peg$c123 = "r",
+ peg$c124 = { type: "literal", value: "r", description: "\"r\"" },
+ peg$c125 = function peg$c125() {
return "\r";
},
- peg$c123 = "t",
- peg$c124 = { type: "literal", value: "t", description: "\"t\"" },
- peg$c125 = function peg$c125() {
+ peg$c126 = "t",
+ peg$c127 = { type: "literal", value: "t", description: "\"t\"" },
+ peg$c128 = function peg$c128() {
return "\t";
},
peg$currPos = 0,
@@ -7885,12 +8196,29 @@ module.exports = function () {
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
- s1 = peg$parseStringLiteral();
+ if (input.substr(peg$currPos, 10) === peg$c95) {
+ s1 = peg$c95;
+ peg$currPos += 10;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) {
+ peg$fail(peg$c96);
+ }
+ }
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c94(s1);
+ s1 = peg$c97();
}
s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ s1 = peg$parseStringLiteral();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$c94(s1);
+ }
+ s0 = s1;
+ }
}
}
}
@@ -7924,13 +8252,13 @@ module.exports = function () {
peg$silentFails++;
s0 = peg$currPos;
- if (peg$c96.test(input.charAt(peg$currPos))) {
+ if (peg$c99.test(input.charAt(peg$currPos))) {
s1 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c97);
+ peg$fail(peg$c100);
}
}
if (s1 === peg$FAILED) {
@@ -7938,25 +8266,25 @@ module.exports = function () {
}
if (s1 !== peg$FAILED) {
s2 = [];
- if (peg$c98.test(input.charAt(peg$currPos))) {
+ if (peg$c101.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c99);
+ peg$fail(peg$c102);
}
}
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
- if (peg$c98.test(input.charAt(peg$currPos))) {
+ if (peg$c101.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c99);
+ peg$fail(peg$c102);
}
}
}
@@ -7964,13 +8292,13 @@ module.exports = function () {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
- if (peg$c96.test(input.charAt(peg$currPos))) {
+ if (peg$c99.test(input.charAt(peg$currPos))) {
s3 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c97);
+ peg$fail(peg$c100);
}
}
if (s3 === peg$FAILED) {
@@ -7978,7 +8306,7 @@ module.exports = function () {
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c100(s2);
+ s1 = peg$c103(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -7996,7 +8324,7 @@ module.exports = function () {
if (s0 === peg$FAILED) {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c95);
+ peg$fail(peg$c98);
}
}
@@ -8009,12 +8337,12 @@ module.exports = function () {
peg$silentFails++;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 34) {
- s1 = peg$c102;
+ s1 = peg$c105;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c103);
+ peg$fail(peg$c106);
}
}
if (s1 !== peg$FAILED) {
@@ -8026,17 +8354,17 @@ module.exports = function () {
}
if (s2 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 34) {
- s3 = peg$c102;
+ s3 = peg$c105;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c103);
+ peg$fail(peg$c106);
}
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c104(s2);
+ s1 = peg$c107(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -8053,12 +8381,12 @@ module.exports = function () {
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 39) {
- s1 = peg$c105;
+ s1 = peg$c108;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c106);
+ peg$fail(peg$c109);
}
}
if (s1 !== peg$FAILED) {
@@ -8070,17 +8398,17 @@ module.exports = function () {
}
if (s2 !== peg$FAILED) {
if (input.charCodeAt(peg$currPos) === 39) {
- s3 = peg$c105;
+ s3 = peg$c108;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c106);
+ peg$fail(peg$c109);
}
}
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c104(s2);
+ s1 = peg$c107(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -8119,7 +8447,7 @@ module.exports = function () {
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c104(s2);
+ s1 = peg$c107(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -8135,7 +8463,7 @@ module.exports = function () {
if (s0 === peg$FAILED) {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c101);
+ peg$fail(peg$c104);
}
}
@@ -8148,13 +8476,13 @@ module.exports = function () {
s0 = peg$currPos;
s1 = peg$currPos;
peg$silentFails++;
- if (peg$c107.test(input.charAt(peg$currPos))) {
+ if (peg$c110.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c108);
+ peg$fail(peg$c111);
}
}
peg$silentFails--;
@@ -8171,12 +8499,12 @@ module.exports = function () {
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c109);
+ peg$fail(peg$c112);
}
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c110(s2);
+ s1 = peg$c113(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -8189,19 +8517,19 @@ module.exports = function () {
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 92) {
- s1 = peg$c111;
+ s1 = peg$c114;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c112);
+ peg$fail(peg$c115);
}
}
if (s1 !== peg$FAILED) {
s2 = peg$parseEscapeSequence();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c110(s2);
+ s1 = peg$c113(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -8222,13 +8550,13 @@ module.exports = function () {
s0 = peg$currPos;
s1 = peg$currPos;
peg$silentFails++;
- if (peg$c113.test(input.charAt(peg$currPos))) {
+ if (peg$c116.test(input.charAt(peg$currPos))) {
s2 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c114);
+ peg$fail(peg$c117);
}
}
peg$silentFails--;
@@ -8245,12 +8573,12 @@ module.exports = function () {
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c109);
+ peg$fail(peg$c112);
}
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c110(s2);
+ s1 = peg$c113(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -8263,19 +8591,19 @@ module.exports = function () {
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 92) {
- s1 = peg$c111;
+ s1 = peg$c114;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c112);
+ peg$fail(peg$c115);
}
}
if (s1 !== peg$FAILED) {
s2 = peg$parseEscapeSequence();
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c110(s2);
+ s1 = peg$c113(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -8311,12 +8639,12 @@ module.exports = function () {
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c109);
+ peg$fail(peg$c112);
}
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c110(s2);
+ s1 = peg$c113(s2);
s0 = s1;
} else {
peg$currPos = s0;
@@ -8333,61 +8661,61 @@ module.exports = function () {
function peg$parseEscapeSequence() {
var s0, s1;
- if (peg$c115.test(input.charAt(peg$currPos))) {
+ if (peg$c118.test(input.charAt(peg$currPos))) {
s0 = input.charAt(peg$currPos);
peg$currPos++;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c116);
+ peg$fail(peg$c119);
}
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 110) {
- s1 = peg$c117;
+ s1 = peg$c120;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c118);
+ peg$fail(peg$c121);
}
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c119();
+ s1 = peg$c122();
}
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 114) {
- s1 = peg$c120;
+ s1 = peg$c123;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c121);
+ peg$fail(peg$c124);
}
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c122();
+ s1 = peg$c125();
}
s0 = s1;
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 116) {
- s1 = peg$c123;
+ s1 = peg$c126;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) {
- peg$fail(peg$c124);
+ peg$fail(peg$c127);
}
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
- s1 = peg$c125();
+ s1 = peg$c128();
}
s0 = s1;
}
@@ -8485,7 +8813,7 @@ module.exports = function () {
function domain(regex) {
regex = new RegExp(regex, "i");
function domainFilter(flow) {
- return flow.request && regex.test(flow.request.host);
+ return flow.request && (regex.test(flow.request.host) || regex.test(flow.request.pretty_host));
}
domainFilter.desc = "domain matches " + regex;
return domainFilter;
@@ -8594,6 +8922,10 @@ module.exports = function () {
urlFilter.desc = "url matches " + regex;
return urlFilter;
}
+ function websocketFilter(flow) {
+ return flow.type === "websocket";
+ }
+ websocketFilter.desc = "is a Websocket Flow";
peg$result = peg$startRuleFunction();
@@ -8614,7 +8946,7 @@ module.exports = function () {
};
}();
-},{"../flow/utils.js":58}],58:[function(require,module,exports){
+},{"../flow/utils.js":60}],60:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -8681,16 +9013,12 @@ var MessageUtils = exports.MessageUtils = {
};
var RequestUtils = exports.RequestUtils = _lodash2.default.extend(MessageUtils, {
- pretty_host: function pretty_host(request) {
- //FIXME: Add hostheader
- return request.host;
- },
pretty_url: function pretty_url(request) {
var port = "";
if (defaultPorts[request.scheme] !== request.port) {
port = ":" + request.port;
}
- return request.scheme + "://" + this.pretty_host(request) + port + request.path;
+ return request.scheme + "://" + request.pretty_host + port + request.path;
}
});
@@ -8733,7 +9061,7 @@ var isValidHttpVersion = exports.isValidHttpVersion = function isValidHttpVersio
return isValidHttpVersion_regex.test(httpVersion);
};
-},{"lodash":"lodash"}],59:[function(require,module,exports){
+},{"lodash":"lodash"}],61:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -8842,7 +9170,7 @@ function initialize(store) {
});
}
-},{"./ducks/eventLog":48,"./ducks/flows":49,"./ducks/ui/flow":52}],60:[function(require,module,exports){
+},{"./ducks/eventLog":50,"./ducks/flows":51,"./ducks/ui/flow":54}],62:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
diff --git a/mitmproxy/tools/web/static/vendor.js b/mitmproxy/tools/web/static/vendor.js
index e83b8ba4..3b5ed1ac 100644
--- a/mitmproxy/tools/web/static/vendor.js
+++ b/mitmproxy/tools/web/static/vendor.js
@@ -11809,244 +11809,6 @@ function isNative(value) {
module.exports = isArray;
},{}],32:[function(require,module,exports){
-/**
- * lodash 3.1.2 (Custom Build) <https://lodash.com/>
- * Build: `lodash modern modularize exports="npm" -o ./`
- * Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/>
- * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
- * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
- * Available under MIT license <https://lodash.com/license>
- */
-var getNative = require('lodash._getnative'),
- isArguments = require('lodash.isarguments'),
- isArray = require('lodash.isarray');
-
-/** Used to detect unsigned integer values. */
-var reIsUint = /^\d+$/;
-
-/** Used for native method references. */
-var objectProto = Object.prototype;
-
-/** Used to check objects for own properties. */
-var hasOwnProperty = objectProto.hasOwnProperty;
-
-/* Native method references for those with the same name as other `lodash` methods. */
-var nativeKeys = getNative(Object, 'keys');
-
-/**
- * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer)
- * of an array-like value.
- */
-var MAX_SAFE_INTEGER = 9007199254740991;
-
-/**
- * The base implementation of `_.property` without support for deep paths.
- *
- * @private
- * @param {string} key The key of the property to get.
- * @returns {Function} Returns the new function.
- */
-function baseProperty(key) {
- return function(object) {
- return object == null ? undefined : object[key];
- };
-}
-
-/**
- * Gets the "length" property value of `object`.
- *
- * **Note:** This function is used to avoid a [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792)
- * that affects Safari on at least iOS 8.1-8.3 ARM64.
- *
- * @private
- * @param {Object} object The object to query.
- * @returns {*} Returns the "length" value.
- */
-var getLength = baseProperty('length');
-
-/**
- * Checks if `value` is array-like.
- *
- * @private
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
- */
-function isArrayLike(value) {
- return value != null && isLength(getLength(value));
-}
-
-/**
- * Checks if `value` is a valid array-like index.
- *
- * @private
- * @param {*} value The value to check.
- * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
- * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
- */
-function isIndex(value, length) {
- value = (typeof value == 'number' || reIsUint.test(value)) ? +value : -1;
- length = length == null ? MAX_SAFE_INTEGER : length;
- return value > -1 && value % 1 == 0 && value < length;
-}
-
-/**
- * Checks if `value` is a valid array-like length.
- *
- * **Note:** This function is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).
- *
- * @private
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
- */
-function isLength(value) {
- return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
-}
-
-/**
- * A fallback implementation of `Object.keys` which creates an array of the
- * own enumerable property names of `object`.
- *
- * @private
- * @param {Object} object The object to query.
- * @returns {Array} Returns the array of property names.
- */
-function shimKeys(object) {
- var props = keysIn(object),
- propsLength = props.length,
- length = propsLength && object.length;
-
- var allowIndexes = !!length && isLength(length) &&
- (isArray(object) || isArguments(object));
-
- var index = -1,
- result = [];
-
- while (++index < propsLength) {
- var key = props[index];
- if ((allowIndexes && isIndex(key, length)) || hasOwnProperty.call(object, key)) {
- result.push(key);
- }
- }
- return result;
-}
-
-/**
- * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`.
- * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
- *
- * @static
- * @memberOf _
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is an object, else `false`.
- * @example
- *
- * _.isObject({});
- * // => true
- *
- * _.isObject([1, 2, 3]);
- * // => true
- *
- * _.isObject(1);
- * // => false
- */
-function isObject(value) {
- // Avoid a V8 JIT bug in Chrome 19-20.
- // See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
- var type = typeof value;
- return !!value && (type == 'object' || type == 'function');
-}
-
-/**
- * Creates an array of the own enumerable property names of `object`.
- *
- * **Note:** Non-object values are coerced to objects. See the
- * [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys)
- * for more details.
- *
- * @static
- * @memberOf _
- * @category Object
- * @param {Object} object The object to query.
- * @returns {Array} Returns the array of property names.
- * @example
- *
- * function Foo() {
- * this.a = 1;
- * this.b = 2;
- * }
- *
- * Foo.prototype.c = 3;
- *
- * _.keys(new Foo);
- * // => ['a', 'b'] (iteration order is not guaranteed)
- *
- * _.keys('hi');
- * // => ['0', '1']
- */
-var keys = !nativeKeys ? shimKeys : function(object) {
- var Ctor = object == null ? undefined : object.constructor;
- if ((typeof Ctor == 'function' && Ctor.prototype === object) ||
- (typeof object != 'function' && isArrayLike(object))) {
- return shimKeys(object);
- }
- return isObject(object) ? nativeKeys(object) : [];
-};
-
-/**
- * Creates an array of the own and inherited enumerable property names of `object`.
- *
- * **Note:** Non-object values are coerced to objects.
- *
- * @static
- * @memberOf _
- * @category Object
- * @param {Object} object The object to query.
- * @returns {Array} Returns the array of property names.
- * @example
- *
- * function Foo() {
- * this.a = 1;
- * this.b = 2;
- * }
- *
- * Foo.prototype.c = 3;
- *
- * _.keysIn(new Foo);
- * // => ['a', 'b', 'c'] (iteration order is not guaranteed)
- */
-function keysIn(object) {
- if (object == null) {
- return [];
- }
- if (!isObject(object)) {
- object = Object(object);
- }
- var length = object.length;
- length = (length && isLength(length) &&
- (isArray(object) || isArguments(object)) && length) || 0;
-
- var Ctor = object.constructor,
- index = -1,
- isProto = typeof Ctor == 'function' && Ctor.prototype === object,
- result = Array(length),
- skipIndexes = length > 0;
-
- while (++index < length) {
- result[index] = (index + '');
- }
- for (var key in object) {
- if (!(skipIndexes && isIndex(key, length)) &&
- !(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
- result.push(key);
- }
- }
- return result;
-}
-
-module.exports = keys;
-
-},{"lodash._getnative":28,"lodash.isarguments":30,"lodash.isarray":31}],33:[function(require,module,exports){
var root = require('./_root');
/** Built-in value references. */
@@ -12054,7 +11816,7 @@ var Symbol = root.Symbol;
module.exports = Symbol;
-},{"./_root":40}],34:[function(require,module,exports){
+},{"./_root":39}],33:[function(require,module,exports){
var Symbol = require('./_Symbol'),
getRawTag = require('./_getRawTag'),
objectToString = require('./_objectToString');
@@ -12084,7 +11846,7 @@ function baseGetTag(value) {
module.exports = baseGetTag;
-},{"./_Symbol":33,"./_getRawTag":37,"./_objectToString":38}],35:[function(require,module,exports){
+},{"./_Symbol":32,"./_getRawTag":36,"./_objectToString":37}],34:[function(require,module,exports){
(function (global){
/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
@@ -12093,7 +11855,7 @@ module.exports = freeGlobal;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],36:[function(require,module,exports){
+},{}],35:[function(require,module,exports){
var overArg = require('./_overArg');
/** Built-in value references. */
@@ -12101,7 +11863,7 @@ var getPrototype = overArg(Object.getPrototypeOf, Object);
module.exports = getPrototype;
-},{"./_overArg":39}],37:[function(require,module,exports){
+},{"./_overArg":38}],36:[function(require,module,exports){
var Symbol = require('./_Symbol');
/** Used for built-in method references. */
@@ -12149,7 +11911,7 @@ function getRawTag(value) {
module.exports = getRawTag;
-},{"./_Symbol":33}],38:[function(require,module,exports){
+},{"./_Symbol":32}],37:[function(require,module,exports){
/** Used for built-in method references. */
var objectProto = Object.prototype;
@@ -12173,7 +11935,7 @@ function objectToString(value) {
module.exports = objectToString;
-},{}],39:[function(require,module,exports){
+},{}],38:[function(require,module,exports){
/**
* Creates a unary function that invokes `func` with its argument transformed.
*
@@ -12190,7 +11952,7 @@ function overArg(func, transform) {
module.exports = overArg;
-},{}],40:[function(require,module,exports){
+},{}],39:[function(require,module,exports){
var freeGlobal = require('./_freeGlobal');
/** Detect free variable `self`. */
@@ -12201,7 +11963,7 @@ var root = freeGlobal || freeSelf || Function('return this')();
module.exports = root;
-},{"./_freeGlobal":35}],41:[function(require,module,exports){
+},{"./_freeGlobal":34}],40:[function(require,module,exports){
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
@@ -12232,7 +11994,7 @@ function isObjectLike(value) {
module.exports = isObjectLike;
-},{}],42:[function(require,module,exports){
+},{}],41:[function(require,module,exports){
var baseGetTag = require('./_baseGetTag'),
getPrototype = require('./_getPrototype'),
isObjectLike = require('./isObjectLike');
@@ -12296,7 +12058,99 @@ function isPlainObject(value) {
module.exports = isPlainObject;
-},{"./_baseGetTag":34,"./_getPrototype":36,"./isObjectLike":41}],43:[function(require,module,exports){
+},{"./_baseGetTag":33,"./_getPrototype":35,"./isObjectLike":40}],42:[function(require,module,exports){
+/*
+object-assign
+(c) Sindre Sorhus
+@license MIT
+*/
+
+'use strict';
+/* eslint-disable no-unused-vars */
+var getOwnPropertySymbols = Object.getOwnPropertySymbols;
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+var propIsEnumerable = Object.prototype.propertyIsEnumerable;
+
+function toObject(val) {
+ if (val === null || val === undefined) {
+ throw new TypeError('Object.assign cannot be called with null or undefined');
+ }
+
+ return Object(val);
+}
+
+function shouldUseNative() {
+ try {
+ if (!Object.assign) {
+ return false;
+ }
+
+ // Detect buggy property enumeration order in older V8 versions.
+
+ // https://bugs.chromium.org/p/v8/issues/detail?id=4118
+ var test1 = new String('abc'); // eslint-disable-line no-new-wrappers
+ test1[5] = 'de';
+ if (Object.getOwnPropertyNames(test1)[0] === '5') {
+ return false;
+ }
+
+ // https://bugs.chromium.org/p/v8/issues/detail?id=3056
+ var test2 = {};
+ for (var i = 0; i < 10; i++) {
+ test2['_' + String.fromCharCode(i)] = i;
+ }
+ var order2 = Object.getOwnPropertyNames(test2).map(function (n) {
+ return test2[n];
+ });
+ if (order2.join('') !== '0123456789') {
+ return false;
+ }
+
+ // https://bugs.chromium.org/p/v8/issues/detail?id=3056
+ var test3 = {};
+ 'abcdefghijklmnopqrst'.split('').forEach(function (letter) {
+ test3[letter] = letter;
+ });
+ if (Object.keys(Object.assign({}, test3)).join('') !==
+ 'abcdefghijklmnopqrst') {
+ return false;
+ }
+
+ return true;
+ } catch (err) {
+ // We don't expect any of the above to throw, but better to be safe.
+ return false;
+ }
+}
+
+module.exports = shouldUseNative() ? Object.assign : function (target, source) {
+ var from;
+ var to = toObject(target);
+ var symbols;
+
+ for (var s = 1; s < arguments.length; s++) {
+ from = Object(arguments[s]);
+
+ for (var key in from) {
+ if (hasOwnProperty.call(from, key)) {
+ to[key] = from[key];
+ }
+ }
+
+ if (getOwnPropertySymbols) {
+ symbols = getOwnPropertySymbols(from);
+ for (var i = 0; i < symbols.length; i++) {
+ if (propIsEnumerable.call(from, symbols[i])) {
+ to[symbols[i]] = from[symbols[i]];
+ }
+ }
+ }
+ }
+
+ return to;
+};
+
+},{}],43:[function(require,module,exports){
// shim for using process in browser
var process = module.exports = {};
@@ -12479,6 +12333,627 @@ process.chdir = function (dir) {
process.umask = function() { return 0; };
},{}],44:[function(require,module,exports){
+(function (process){
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+'use strict';
+
+if (process.env.NODE_ENV !== 'production') {
+ var invariant = require('fbjs/lib/invariant');
+ var warning = require('fbjs/lib/warning');
+ var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret');
+ var loggedTypeFailures = {};
+}
+
+/**
+ * Assert that the values match with the type specs.
+ * Error messages are memorized and will only be shown once.
+ *
+ * @param {object} typeSpecs Map of name to a ReactPropType
+ * @param {object} values Runtime values that need to be type-checked
+ * @param {string} location e.g. "prop", "context", "child context"
+ * @param {string} componentName Name of the component for error messages.
+ * @param {?Function} getStack Returns the component stack.
+ * @private
+ */
+function checkPropTypes(typeSpecs, values, location, componentName, getStack) {
+ if (process.env.NODE_ENV !== 'production') {
+ for (var typeSpecName in typeSpecs) {
+ if (typeSpecs.hasOwnProperty(typeSpecName)) {
+ var error;
+ // Prop type validation may throw. In case they do, we don't want to
+ // fail the render phase where it didn't fail before. So we log it.
+ // After these have been cleaned up, we'll let them throw.
+ try {
+ // This is intentionally an invariant that gets caught. It's the same
+ // behavior as without this statement except with a better message.
+ invariant(typeof typeSpecs[typeSpecName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', componentName || 'React class', location, typeSpecName);
+ error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret);
+ } catch (ex) {
+ error = ex;
+ }
+ warning(!error || error instanceof Error, '%s: type specification of %s `%s` is invalid; the type checker ' + 'function must return `null` or an `Error` but returned a %s. ' + 'You may have forgotten to pass an argument to the type checker ' + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 'shape all require an argument).', componentName || 'React class', location, typeSpecName, typeof error);
+ if (error instanceof Error && !(error.message in loggedTypeFailures)) {
+ // Only monitor this failure once because there tends to be a lot of the
+ // same error.
+ loggedTypeFailures[error.message] = true;
+
+ var stack = getStack ? getStack() : '';
+
+ warning(false, 'Failed %s type: %s%s', location, error.message, stack != null ? stack : '');
+ }
+ }
+ }
+ }
+}
+
+module.exports = checkPropTypes;
+
+}).call(this,require('_process'))
+
+},{"./lib/ReactPropTypesSecret":47,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],45:[function(require,module,exports){
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+'use strict';
+
+var emptyFunction = require('fbjs/lib/emptyFunction');
+var invariant = require('fbjs/lib/invariant');
+
+module.exports = function() {
+ // Important!
+ // Keep this list in sync with production version in `./factoryWithTypeCheckers.js`.
+ function shim() {
+ invariant(
+ false,
+ 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' +
+ 'Use PropTypes.checkPropTypes() to call them. ' +
+ 'Read more at http://fb.me/use-check-prop-types'
+ );
+ };
+ shim.isRequired = shim;
+ function getShim() {
+ return shim;
+ };
+ var ReactPropTypes = {
+ array: shim,
+ bool: shim,
+ func: shim,
+ number: shim,
+ object: shim,
+ string: shim,
+ symbol: shim,
+
+ any: shim,
+ arrayOf: getShim,
+ element: shim,
+ instanceOf: getShim,
+ node: shim,
+ objectOf: getShim,
+ oneOf: getShim,
+ oneOfType: getShim,
+ shape: getShim
+ };
+
+ ReactPropTypes.checkPropTypes = emptyFunction;
+ ReactPropTypes.PropTypes = ReactPropTypes;
+
+ return ReactPropTypes;
+};
+
+},{"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18}],46:[function(require,module,exports){
+(function (process){
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+'use strict';
+
+var emptyFunction = require('fbjs/lib/emptyFunction');
+var invariant = require('fbjs/lib/invariant');
+var warning = require('fbjs/lib/warning');
+
+var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret');
+var checkPropTypes = require('./checkPropTypes');
+
+module.exports = function(isValidElement, throwOnDirectAccess) {
+ /* global Symbol */
+ var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
+ var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
+
+ /**
+ * Returns the iterator method function contained on the iterable object.
+ *
+ * Be sure to invoke the function with the iterable as context:
+ *
+ * var iteratorFn = getIteratorFn(myIterable);
+ * if (iteratorFn) {
+ * var iterator = iteratorFn.call(myIterable);
+ * ...
+ * }
+ *
+ * @param {?object} maybeIterable
+ * @return {?function}
+ */
+ function getIteratorFn(maybeIterable) {
+ var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]);
+ if (typeof iteratorFn === 'function') {
+ return iteratorFn;
+ }
+ }
+
+ /**
+ * Collection of methods that allow declaration and validation of props that are
+ * supplied to React components. Example usage:
+ *
+ * var Props = require('ReactPropTypes');
+ * var MyArticle = React.createClass({
+ * propTypes: {
+ * // An optional string prop named "description".
+ * description: Props.string,
+ *
+ * // A required enum prop named "category".
+ * category: Props.oneOf(['News','Photos']).isRequired,
+ *
+ * // A prop named "dialog" that requires an instance of Dialog.
+ * dialog: Props.instanceOf(Dialog).isRequired
+ * },
+ * render: function() { ... }
+ * });
+ *
+ * A more formal specification of how these methods are used:
+ *
+ * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...)
+ * decl := ReactPropTypes.{type}(.isRequired)?
+ *
+ * Each and every declaration produces a function with the same signature. This
+ * allows the creation of custom validation functions. For example:
+ *
+ * var MyLink = React.createClass({
+ * propTypes: {
+ * // An optional string or URI prop named "href".
+ * href: function(props, propName, componentName) {
+ * var propValue = props[propName];
+ * if (propValue != null && typeof propValue !== 'string' &&
+ * !(propValue instanceof URI)) {
+ * return new Error(
+ * 'Expected a string or an URI for ' + propName + ' in ' +
+ * componentName
+ * );
+ * }
+ * }
+ * },
+ * render: function() {...}
+ * });
+ *
+ * @internal
+ */
+
+ var ANONYMOUS = '<<anonymous>>';
+
+ // Important!
+ // Keep this list in sync with production version in `./factoryWithThrowingShims.js`.
+ var ReactPropTypes = {
+ array: createPrimitiveTypeChecker('array'),
+ bool: createPrimitiveTypeChecker('boolean'),
+ func: createPrimitiveTypeChecker('function'),
+ number: createPrimitiveTypeChecker('number'),
+ object: createPrimitiveTypeChecker('object'),
+ string: createPrimitiveTypeChecker('string'),
+ symbol: createPrimitiveTypeChecker('symbol'),
+
+ any: createAnyTypeChecker(),
+ arrayOf: createArrayOfTypeChecker,
+ element: createElementTypeChecker(),
+ instanceOf: createInstanceTypeChecker,
+ node: createNodeChecker(),
+ objectOf: createObjectOfTypeChecker,
+ oneOf: createEnumTypeChecker,
+ oneOfType: createUnionTypeChecker,
+ shape: createShapeTypeChecker
+ };
+
+ /**
+ * inlined Object.is polyfill to avoid requiring consumers ship their own
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
+ */
+ /*eslint-disable no-self-compare*/
+ function is(x, y) {
+ // SameValue algorithm
+ if (x === y) {
+ // Steps 1-5, 7-10
+ // Steps 6.b-6.e: +0 != -0
+ return x !== 0 || 1 / x === 1 / y;
+ } else {
+ // Step 6.a: NaN == NaN
+ return x !== x && y !== y;
+ }
+ }
+ /*eslint-enable no-self-compare*/
+
+ /**
+ * We use an Error-like object for backward compatibility as people may call
+ * PropTypes directly and inspect their output. However, we don't use real
+ * Errors anymore. We don't inspect their stack anyway, and creating them
+ * is prohibitively expensive if they are created too often, such as what
+ * happens in oneOfType() for any type before the one that matched.
+ */
+ function PropTypeError(message) {
+ this.message = message;
+ this.stack = '';
+ }
+ // Make `instanceof Error` still work for returned errors.
+ PropTypeError.prototype = Error.prototype;
+
+ function createChainableTypeChecker(validate) {
+ if (process.env.NODE_ENV !== 'production') {
+ var manualPropTypeCallCache = {};
+ var manualPropTypeWarningCount = 0;
+ }
+ function checkType(isRequired, props, propName, componentName, location, propFullName, secret) {
+ componentName = componentName || ANONYMOUS;
+ propFullName = propFullName || propName;
+
+ if (secret !== ReactPropTypesSecret) {
+ if (throwOnDirectAccess) {
+ // New behavior only for users of `prop-types` package
+ invariant(
+ false,
+ 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' +
+ 'Use `PropTypes.checkPropTypes()` to call them. ' +
+ 'Read more at http://fb.me/use-check-prop-types'
+ );
+ } else if (process.env.NODE_ENV !== 'production' && typeof console !== 'undefined') {
+ // Old behavior for people using React.PropTypes
+ var cacheKey = componentName + ':' + propName;
+ if (
+ !manualPropTypeCallCache[cacheKey] &&
+ // Avoid spamming the console because they are often not actionable except for lib authors
+ manualPropTypeWarningCount < 3
+ ) {
+ warning(
+ false,
+ 'You are manually calling a React.PropTypes validation ' +
+ 'function for the `%s` prop on `%s`. This is deprecated ' +
+ 'and will throw in the standalone `prop-types` package. ' +
+ 'You may be seeing this warning due to a third-party PropTypes ' +
+ 'library. See https://fb.me/react-warning-dont-call-proptypes ' + 'for details.',
+ propFullName,
+ componentName
+ );
+ manualPropTypeCallCache[cacheKey] = true;
+ manualPropTypeWarningCount++;
+ }
+ }
+ }
+ if (props[propName] == null) {
+ if (isRequired) {
+ if (props[propName] === null) {
+ return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required ' + ('in `' + componentName + '`, but its value is `null`.'));
+ }
+ return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required in ' + ('`' + componentName + '`, but its value is `undefined`.'));
+ }
+ return null;
+ } else {
+ return validate(props, propName, componentName, location, propFullName);
+ }
+ }
+
+ var chainedCheckType = checkType.bind(null, false);
+ chainedCheckType.isRequired = checkType.bind(null, true);
+
+ return chainedCheckType;
+ }
+
+ function createPrimitiveTypeChecker(expectedType) {
+ function validate(props, propName, componentName, location, propFullName, secret) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== expectedType) {
+ // `propValue` being instance of, say, date/regexp, pass the 'object'
+ // check, but we can offer a more precise error message here rather than
+ // 'of type `object`'.
+ var preciseType = getPreciseType(propValue);
+
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createAnyTypeChecker() {
+ return createChainableTypeChecker(emptyFunction.thatReturnsNull);
+ }
+
+ function createArrayOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (typeof typeChecker !== 'function') {
+ return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside arrayOf.');
+ }
+ var propValue = props[propName];
+ if (!Array.isArray(propValue)) {
+ var propType = getPropType(propValue);
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.'));
+ }
+ for (var i = 0; i < propValue.length; i++) {
+ var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']', ReactPropTypesSecret);
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createElementTypeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ if (!isValidElement(propValue)) {
+ var propType = getPropType(propValue);
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a single ReactElement.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createInstanceTypeChecker(expectedClass) {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!(props[propName] instanceof expectedClass)) {
+ var expectedClassName = expectedClass.name || ANONYMOUS;
+ var actualClassName = getClassName(props[propName]);
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createEnumTypeChecker(expectedValues) {
+ if (!Array.isArray(expectedValues)) {
+ process.env.NODE_ENV !== 'production' ? warning(false, 'Invalid argument supplied to oneOf, expected an instance of array.') : void 0;
+ return emptyFunction.thatReturnsNull;
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ for (var i = 0; i < expectedValues.length; i++) {
+ if (is(propValue, expectedValues[i])) {
+ return null;
+ }
+ }
+
+ var valuesString = JSON.stringify(expectedValues);
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of value `' + propValue + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.'));
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createObjectOfTypeChecker(typeChecker) {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (typeof typeChecker !== 'function') {
+ return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside objectOf.');
+ }
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.'));
+ }
+ for (var key in propValue) {
+ if (propValue.hasOwnProperty(key)) {
+ var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret);
+ if (error instanceof Error) {
+ return error;
+ }
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createUnionTypeChecker(arrayOfTypeCheckers) {
+ if (!Array.isArray(arrayOfTypeCheckers)) {
+ process.env.NODE_ENV !== 'production' ? warning(false, 'Invalid argument supplied to oneOfType, expected an instance of array.') : void 0;
+ return emptyFunction.thatReturnsNull;
+ }
+
+ function validate(props, propName, componentName, location, propFullName) {
+ for (var i = 0; i < arrayOfTypeCheckers.length; i++) {
+ var checker = arrayOfTypeCheckers[i];
+ if (checker(props, propName, componentName, location, propFullName, ReactPropTypesSecret) == null) {
+ return null;
+ }
+ }
+
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.'));
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createNodeChecker() {
+ function validate(props, propName, componentName, location, propFullName) {
+ if (!isNode(props[propName])) {
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.'));
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function createShapeTypeChecker(shapeTypes) {
+ function validate(props, propName, componentName, location, propFullName) {
+ var propValue = props[propName];
+ var propType = getPropType(propValue);
+ if (propType !== 'object') {
+ return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.'));
+ }
+ for (var key in shapeTypes) {
+ var checker = shapeTypes[key];
+ if (!checker) {
+ continue;
+ }
+ var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret);
+ if (error) {
+ return error;
+ }
+ }
+ return null;
+ }
+ return createChainableTypeChecker(validate);
+ }
+
+ function isNode(propValue) {
+ switch (typeof propValue) {
+ case 'number':
+ case 'string':
+ case 'undefined':
+ return true;
+ case 'boolean':
+ return !propValue;
+ case 'object':
+ if (Array.isArray(propValue)) {
+ return propValue.every(isNode);
+ }
+ if (propValue === null || isValidElement(propValue)) {
+ return true;
+ }
+
+ var iteratorFn = getIteratorFn(propValue);
+ if (iteratorFn) {
+ var iterator = iteratorFn.call(propValue);
+ var step;
+ if (iteratorFn !== propValue.entries) {
+ while (!(step = iterator.next()).done) {
+ if (!isNode(step.value)) {
+ return false;
+ }
+ }
+ } else {
+ // Iterator will provide entry [k,v] tuples rather than values.
+ while (!(step = iterator.next()).done) {
+ var entry = step.value;
+ if (entry) {
+ if (!isNode(entry[1])) {
+ return false;
+ }
+ }
+ }
+ }
+ } else {
+ return false;
+ }
+
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ function isSymbol(propType, propValue) {
+ // Native Symbol.
+ if (propType === 'symbol') {
+ return true;
+ }
+
+ // 19.4.3.5 Symbol.prototype[@@toStringTag] === 'Symbol'
+ if (propValue['@@toStringTag'] === 'Symbol') {
+ return true;
+ }
+
+ // Fallback for non-spec compliant Symbols which are polyfilled.
+ if (typeof Symbol === 'function' && propValue instanceof Symbol) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Equivalent of `typeof` but with special handling for array and regexp.
+ function getPropType(propValue) {
+ var propType = typeof propValue;
+ if (Array.isArray(propValue)) {
+ return 'array';
+ }
+ if (propValue instanceof RegExp) {
+ // Old webkits (at least until Android 4.0) return 'function' rather than
+ // 'object' for typeof a RegExp. We'll normalize this here so that /bla/
+ // passes PropTypes.object.
+ return 'object';
+ }
+ if (isSymbol(propType, propValue)) {
+ return 'symbol';
+ }
+ return propType;
+ }
+
+ // This handles more types than `getPropType`. Only used for error messages.
+ // See `createPrimitiveTypeChecker`.
+ function getPreciseType(propValue) {
+ var propType = getPropType(propValue);
+ if (propType === 'object') {
+ if (propValue instanceof Date) {
+ return 'date';
+ } else if (propValue instanceof RegExp) {
+ return 'regexp';
+ }
+ }
+ return propType;
+ }
+
+ // Returns class name of the object, if any.
+ function getClassName(propValue) {
+ if (!propValue.constructor || !propValue.constructor.name) {
+ return ANONYMOUS;
+ }
+ return propValue.constructor.name;
+ }
+
+ ReactPropTypes.checkPropTypes = checkPropTypes;
+ ReactPropTypes.PropTypes = ReactPropTypes;
+
+ return ReactPropTypes;
+};
+
+}).call(this,require('_process'))
+
+},{"./checkPropTypes":44,"./lib/ReactPropTypesSecret":47,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],47:[function(require,module,exports){
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+'use strict';
+
+var ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';
+
+module.exports = ReactPropTypesSecret;
+
+},{}],48:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -12552,7 +13027,7 @@ var ARIADOMPropertyConfig = {
};
module.exports = ARIADOMPropertyConfig;
-},{}],45:[function(require,module,exports){
+},{}],49:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -12576,7 +13051,7 @@ var AutoFocusUtils = {
};
module.exports = AutoFocusUtils;
-},{"./ReactDOMComponentTree":76,"fbjs/lib/focusNode":12}],46:[function(require,module,exports){
+},{"./ReactDOMComponentTree":80,"fbjs/lib/focusNode":12}],50:[function(require,module,exports){
/**
* Copyright 2013-present Facebook, Inc.
* All rights reserved.
@@ -12961,7 +13436,7 @@ var BeforeInputEventPlugin = {
};
module.exports = BeforeInputEventPlugin;
-},{"./EventPropagators":62,"./FallbackCompositionState":63,"./SyntheticCompositionEvent":127,"./SyntheticInputEvent":131,"fbjs/lib/ExecutionEnvironment":4}],47:[function(require,module,exports){
+},{"./EventPropagators":66,"./FallbackCompositionState":67,"./SyntheticCompositionEvent":131,"./SyntheticInputEvent":135,"fbjs/lib/ExecutionEnvironment":4}],51:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -13109,7 +13584,7 @@ var CSSProperty = {
};
module.exports = CSSProperty;
-},{}],48:[function(require,module,exports){
+},{}],52:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -13321,7 +13796,7 @@ var CSSPropertyOperations = {
module.exports = CSSPropertyOperations;
}).call(this,require('_process'))
-},{"./CSSProperty":47,"./ReactInstrumentation":105,"./dangerousStyleValue":144,"_process":43,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/camelizeStyleName":6,"fbjs/lib/hyphenateStyleName":17,"fbjs/lib/memoizeStringOnly":21,"fbjs/lib/warning":25}],49:[function(require,module,exports){
+},{"./CSSProperty":51,"./ReactInstrumentation":109,"./dangerousStyleValue":148,"_process":43,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/camelizeStyleName":6,"fbjs/lib/hyphenateStyleName":17,"fbjs/lib/memoizeStringOnly":21,"fbjs/lib/warning":25}],53:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -13443,7 +13918,7 @@ var CallbackQueue = function () {
module.exports = PooledClass.addPoolingTo(CallbackQueue);
}).call(this,require('_process'))
-},{"./PooledClass":67,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],50:[function(require,module,exports){
+},{"./PooledClass":71,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],54:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -13764,7 +14239,7 @@ var ChangeEventPlugin = {
};
module.exports = ChangeEventPlugin;
-},{"./EventPluginHub":59,"./EventPropagators":62,"./ReactDOMComponentTree":76,"./ReactUpdates":120,"./SyntheticEvent":129,"./getEventTarget":152,"./isEventSupported":160,"./isTextInputElement":161,"fbjs/lib/ExecutionEnvironment":4}],51:[function(require,module,exports){
+},{"./EventPluginHub":63,"./EventPropagators":66,"./ReactDOMComponentTree":80,"./ReactUpdates":124,"./SyntheticEvent":133,"./getEventTarget":156,"./isEventSupported":164,"./isTextInputElement":165,"fbjs/lib/ExecutionEnvironment":4}],55:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -13992,7 +14467,7 @@ var DOMChildrenOperations = {
module.exports = DOMChildrenOperations;
}).call(this,require('_process'))
-},{"./DOMLazyTree":52,"./Danger":56,"./ReactDOMComponentTree":76,"./ReactInstrumentation":105,"./createMicrosoftUnsafeLocalFunction":143,"./setInnerHTML":165,"./setTextContent":166,"_process":43}],52:[function(require,module,exports){
+},{"./DOMLazyTree":56,"./Danger":60,"./ReactDOMComponentTree":80,"./ReactInstrumentation":109,"./createMicrosoftUnsafeLocalFunction":147,"./setInnerHTML":169,"./setTextContent":170,"_process":43}],56:[function(require,module,exports){
/**
* Copyright 2015-present, Facebook, Inc.
* All rights reserved.
@@ -14110,7 +14585,7 @@ DOMLazyTree.queueHTML = queueHTML;
DOMLazyTree.queueText = queueText;
module.exports = DOMLazyTree;
-},{"./DOMNamespaces":53,"./createMicrosoftUnsafeLocalFunction":143,"./setInnerHTML":165,"./setTextContent":166}],53:[function(require,module,exports){
+},{"./DOMNamespaces":57,"./createMicrosoftUnsafeLocalFunction":147,"./setInnerHTML":169,"./setTextContent":170}],57:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -14130,7 +14605,7 @@ var DOMNamespaces = {
};
module.exports = DOMNamespaces;
-},{}],54:[function(require,module,exports){
+},{}],58:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -14343,7 +14818,7 @@ var DOMProperty = {
module.exports = DOMProperty;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],55:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],59:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -14583,7 +15058,7 @@ var DOMPropertyOperations = {
module.exports = DOMPropertyOperations;
}).call(this,require('_process'))
-},{"./DOMProperty":54,"./ReactDOMComponentTree":76,"./ReactInstrumentation":105,"./quoteAttributeValueForBrowser":162,"_process":43,"fbjs/lib/warning":25}],56:[function(require,module,exports){
+},{"./DOMProperty":58,"./ReactDOMComponentTree":80,"./ReactInstrumentation":109,"./quoteAttributeValueForBrowser":166,"_process":43,"fbjs/lib/warning":25}],60:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -14634,7 +15109,7 @@ var Danger = {
module.exports = Danger;
}).call(this,require('_process'))
-},{"./DOMLazyTree":52,"./reactProdInvariant":163,"_process":43,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/createNodesFromMarkup":9,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18}],57:[function(require,module,exports){
+},{"./DOMLazyTree":56,"./reactProdInvariant":167,"_process":43,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/createNodesFromMarkup":9,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18}],61:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -14660,7 +15135,7 @@ module.exports = Danger;
var DefaultEventPluginOrder = ['ResponderEventPlugin', 'SimpleEventPlugin', 'TapEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'BeforeInputEventPlugin'];
module.exports = DefaultEventPluginOrder;
-},{}],58:[function(require,module,exports){
+},{}],62:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -14760,7 +15235,7 @@ var EnterLeaveEventPlugin = {
};
module.exports = EnterLeaveEventPlugin;
-},{"./EventPropagators":62,"./ReactDOMComponentTree":76,"./SyntheticMouseEvent":133}],59:[function(require,module,exports){
+},{"./EventPropagators":66,"./ReactDOMComponentTree":80,"./SyntheticMouseEvent":137}],63:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -15041,7 +15516,7 @@ var EventPluginHub = {
module.exports = EventPluginHub;
}).call(this,require('_process'))
-},{"./EventPluginRegistry":60,"./EventPluginUtils":61,"./ReactErrorUtils":96,"./accumulateInto":140,"./forEachAccumulated":148,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],60:[function(require,module,exports){
+},{"./EventPluginRegistry":64,"./EventPluginUtils":65,"./ReactErrorUtils":100,"./accumulateInto":144,"./forEachAccumulated":152,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],64:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -15299,7 +15774,7 @@ var EventPluginRegistry = {
module.exports = EventPluginRegistry;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],61:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],65:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -15528,7 +16003,7 @@ var EventPluginUtils = {
module.exports = EventPluginUtils;
}).call(this,require('_process'))
-},{"./ReactErrorUtils":96,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],62:[function(require,module,exports){
+},{"./ReactErrorUtils":100,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],66:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -15665,7 +16140,7 @@ var EventPropagators = {
module.exports = EventPropagators;
}).call(this,require('_process'))
-},{"./EventPluginHub":59,"./EventPluginUtils":61,"./accumulateInto":140,"./forEachAccumulated":148,"_process":43,"fbjs/lib/warning":25}],63:[function(require,module,exports){
+},{"./EventPluginHub":63,"./EventPluginUtils":65,"./accumulateInto":144,"./forEachAccumulated":152,"_process":43,"fbjs/lib/warning":25}],67:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -15760,7 +16235,7 @@ _assign(FallbackCompositionState.prototype, {
PooledClass.addPoolingTo(FallbackCompositionState);
module.exports = FallbackCompositionState;
-},{"./PooledClass":67,"./getTextContentAccessor":157,"object-assign":170}],64:[function(require,module,exports){
+},{"./PooledClass":71,"./getTextContentAccessor":161,"object-assign":42}],68:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -15972,7 +16447,7 @@ var HTMLDOMPropertyConfig = {
};
module.exports = HTMLDOMPropertyConfig;
-},{"./DOMProperty":54}],65:[function(require,module,exports){
+},{"./DOMProperty":58}],69:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -16031,7 +16506,7 @@ var KeyEscapeUtils = {
};
module.exports = KeyEscapeUtils;
-},{}],66:[function(require,module,exports){
+},{}],70:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -16169,7 +16644,7 @@ var LinkedValueUtils = {
module.exports = LinkedValueUtils;
}).call(this,require('_process'))
-},{"./ReactPropTypesSecret":113,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/React":187}],67:[function(require,module,exports){
+},{"./ReactPropTypesSecret":117,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/React":190}],71:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -16284,7 +16759,7 @@ var PooledClass = {
module.exports = PooledClass;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],68:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],72:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -16612,7 +17087,7 @@ var ReactBrowserEventEmitter = _assign({}, ReactEventEmitterMixin, {
});
module.exports = ReactBrowserEventEmitter;
-},{"./EventPluginRegistry":60,"./ReactEventEmitterMixin":97,"./ViewportMetrics":139,"./getVendorPrefixedEventName":158,"./isEventSupported":160,"object-assign":170}],69:[function(require,module,exports){
+},{"./EventPluginRegistry":64,"./ReactEventEmitterMixin":101,"./ViewportMetrics":143,"./getVendorPrefixedEventName":162,"./isEventSupported":164,"object-assign":42}],73:[function(require,module,exports){
(function (process){
/**
* Copyright 2014-present, Facebook, Inc.
@@ -16769,7 +17244,7 @@ var ReactChildReconciler = {
module.exports = ReactChildReconciler;
}).call(this,require('_process'))
-},{"./KeyEscapeUtils":65,"./ReactReconciler":115,"./instantiateReactComponent":159,"./shouldUpdateReactComponent":167,"./traverseAllChildren":168,"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":191}],70:[function(require,module,exports){
+},{"./KeyEscapeUtils":69,"./ReactReconciler":119,"./instantiateReactComponent":163,"./shouldUpdateReactComponent":171,"./traverseAllChildren":172,"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":194}],74:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -16799,7 +17274,7 @@ var ReactComponentBrowserEnvironment = {
};
module.exports = ReactComponentBrowserEnvironment;
-},{"./DOMChildrenOperations":51,"./ReactDOMIDOperations":80}],71:[function(require,module,exports){
+},{"./DOMChildrenOperations":55,"./ReactDOMIDOperations":84}],75:[function(require,module,exports){
(function (process){
/**
* Copyright 2014-present, Facebook, Inc.
@@ -16848,7 +17323,7 @@ var ReactComponentEnvironment = {
module.exports = ReactComponentEnvironment;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],72:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],76:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -17753,7 +18228,7 @@ var ReactCompositeComponent = {
module.exports = ReactCompositeComponent;
}).call(this,require('_process'))
-},{"./ReactComponentEnvironment":71,"./ReactErrorUtils":96,"./ReactInstanceMap":104,"./ReactInstrumentation":105,"./ReactNodeTypes":110,"./ReactReconciler":115,"./checkReactTypeSpec":142,"./reactProdInvariant":163,"./shouldUpdateReactComponent":167,"_process":43,"fbjs/lib/emptyObject":11,"fbjs/lib/invariant":18,"fbjs/lib/shallowEqual":24,"fbjs/lib/warning":25,"object-assign":170,"react/lib/React":187,"react/lib/ReactCurrentOwner":192}],73:[function(require,module,exports){
+},{"./ReactComponentEnvironment":75,"./ReactErrorUtils":100,"./ReactInstanceMap":108,"./ReactInstrumentation":109,"./ReactNodeTypes":114,"./ReactReconciler":119,"./checkReactTypeSpec":146,"./reactProdInvariant":167,"./shouldUpdateReactComponent":171,"_process":43,"fbjs/lib/emptyObject":11,"fbjs/lib/invariant":18,"fbjs/lib/shallowEqual":24,"fbjs/lib/warning":25,"object-assign":42,"react/lib/React":190,"react/lib/ReactCurrentOwner":195}],77:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -17867,7 +18342,7 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = ReactDOM;
}).call(this,require('_process'))
-},{"./ReactDOMComponentTree":76,"./ReactDOMInvalidARIAHook":82,"./ReactDOMNullInputValuePropHook":83,"./ReactDOMUnknownPropertyHook":90,"./ReactDefaultInjection":93,"./ReactInstrumentation":105,"./ReactMount":108,"./ReactReconciler":115,"./ReactUpdates":120,"./ReactVersion":121,"./findDOMNode":146,"./getHostComponentFromComposite":153,"./renderSubtreeIntoContainer":164,"_process":43,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/warning":25}],74:[function(require,module,exports){
+},{"./ReactDOMComponentTree":80,"./ReactDOMInvalidARIAHook":86,"./ReactDOMNullInputValuePropHook":87,"./ReactDOMUnknownPropertyHook":94,"./ReactDefaultInjection":97,"./ReactInstrumentation":109,"./ReactMount":112,"./ReactReconciler":119,"./ReactUpdates":124,"./ReactVersion":125,"./findDOMNode":150,"./getHostComponentFromComposite":157,"./renderSubtreeIntoContainer":168,"_process":43,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/warning":25}],78:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -18871,7 +19346,7 @@ _assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mi
module.exports = ReactDOMComponent;
}).call(this,require('_process'))
-},{"./AutoFocusUtils":45,"./CSSPropertyOperations":48,"./DOMLazyTree":52,"./DOMNamespaces":53,"./DOMProperty":54,"./DOMPropertyOperations":55,"./EventPluginHub":59,"./EventPluginRegistry":60,"./ReactBrowserEventEmitter":68,"./ReactDOMComponentFlags":75,"./ReactDOMComponentTree":76,"./ReactDOMInput":81,"./ReactDOMOption":84,"./ReactDOMSelect":85,"./ReactDOMTextarea":88,"./ReactInstrumentation":105,"./ReactMultiChild":109,"./ReactServerRenderingTransaction":117,"./escapeTextContentForBrowser":145,"./isEventSupported":160,"./reactProdInvariant":163,"./validateDOMNesting":169,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18,"fbjs/lib/shallowEqual":24,"fbjs/lib/warning":25,"object-assign":170}],75:[function(require,module,exports){
+},{"./AutoFocusUtils":49,"./CSSPropertyOperations":52,"./DOMLazyTree":56,"./DOMNamespaces":57,"./DOMProperty":58,"./DOMPropertyOperations":59,"./EventPluginHub":63,"./EventPluginRegistry":64,"./ReactBrowserEventEmitter":72,"./ReactDOMComponentFlags":79,"./ReactDOMComponentTree":80,"./ReactDOMInput":85,"./ReactDOMOption":88,"./ReactDOMSelect":89,"./ReactDOMTextarea":92,"./ReactInstrumentation":109,"./ReactMultiChild":113,"./ReactServerRenderingTransaction":121,"./escapeTextContentForBrowser":149,"./isEventSupported":164,"./reactProdInvariant":167,"./validateDOMNesting":173,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18,"fbjs/lib/shallowEqual":24,"fbjs/lib/warning":25,"object-assign":42}],79:[function(require,module,exports){
/**
* Copyright 2015-present, Facebook, Inc.
* All rights reserved.
@@ -18889,7 +19364,7 @@ var ReactDOMComponentFlags = {
};
module.exports = ReactDOMComponentFlags;
-},{}],76:[function(require,module,exports){
+},{}],80:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -19087,7 +19562,7 @@ var ReactDOMComponentTree = {
module.exports = ReactDOMComponentTree;
}).call(this,require('_process'))
-},{"./DOMProperty":54,"./ReactDOMComponentFlags":75,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],77:[function(require,module,exports){
+},{"./DOMProperty":58,"./ReactDOMComponentFlags":79,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],81:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -19123,7 +19598,7 @@ function ReactDOMContainerInfo(topLevelWrapper, node) {
module.exports = ReactDOMContainerInfo;
}).call(this,require('_process'))
-},{"./validateDOMNesting":169,"_process":43}],78:[function(require,module,exports){
+},{"./validateDOMNesting":173,"_process":43}],82:[function(require,module,exports){
/**
* Copyright 2014-present, Facebook, Inc.
* All rights reserved.
@@ -19183,7 +19658,7 @@ _assign(ReactDOMEmptyComponent.prototype, {
});
module.exports = ReactDOMEmptyComponent;
-},{"./DOMLazyTree":52,"./ReactDOMComponentTree":76,"object-assign":170}],79:[function(require,module,exports){
+},{"./DOMLazyTree":56,"./ReactDOMComponentTree":80,"object-assign":42}],83:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -19202,7 +19677,7 @@ var ReactDOMFeatureFlags = {
};
module.exports = ReactDOMFeatureFlags;
-},{}],80:[function(require,module,exports){
+},{}],84:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -19236,7 +19711,7 @@ var ReactDOMIDOperations = {
};
module.exports = ReactDOMIDOperations;
-},{"./DOMChildrenOperations":51,"./ReactDOMComponentTree":76}],81:[function(require,module,exports){
+},{"./DOMChildrenOperations":55,"./ReactDOMComponentTree":80}],85:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -19517,7 +19992,7 @@ function _handleChange(event) {
module.exports = ReactDOMInput;
}).call(this,require('_process'))
-},{"./DOMPropertyOperations":55,"./LinkedValueUtils":66,"./ReactDOMComponentTree":76,"./ReactUpdates":120,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"object-assign":170}],82:[function(require,module,exports){
+},{"./DOMPropertyOperations":59,"./LinkedValueUtils":70,"./ReactDOMComponentTree":80,"./ReactUpdates":124,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"object-assign":42}],86:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -19613,7 +20088,7 @@ var ReactDOMInvalidARIAHook = {
module.exports = ReactDOMInvalidARIAHook;
}).call(this,require('_process'))
-},{"./DOMProperty":54,"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":191}],83:[function(require,module,exports){
+},{"./DOMProperty":58,"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":194}],87:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -19659,7 +20134,7 @@ var ReactDOMNullInputValuePropHook = {
module.exports = ReactDOMNullInputValuePropHook;
}).call(this,require('_process'))
-},{"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":191}],84:[function(require,module,exports){
+},{"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":194}],88:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -19785,7 +20260,7 @@ var ReactDOMOption = {
module.exports = ReactDOMOption;
}).call(this,require('_process'))
-},{"./ReactDOMComponentTree":76,"./ReactDOMSelect":85,"_process":43,"fbjs/lib/warning":25,"object-assign":170,"react/lib/React":187}],85:[function(require,module,exports){
+},{"./ReactDOMComponentTree":80,"./ReactDOMSelect":89,"_process":43,"fbjs/lib/warning":25,"object-assign":42,"react/lib/React":190}],89:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -19988,7 +20463,7 @@ function _handleChange(event) {
module.exports = ReactDOMSelect;
}).call(this,require('_process'))
-},{"./LinkedValueUtils":66,"./ReactDOMComponentTree":76,"./ReactUpdates":120,"_process":43,"fbjs/lib/warning":25,"object-assign":170}],86:[function(require,module,exports){
+},{"./LinkedValueUtils":70,"./ReactDOMComponentTree":80,"./ReactUpdates":124,"_process":43,"fbjs/lib/warning":25,"object-assign":42}],90:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -20200,7 +20675,7 @@ var ReactDOMSelection = {
};
module.exports = ReactDOMSelection;
-},{"./getNodeForCharacterOffset":156,"./getTextContentAccessor":157,"fbjs/lib/ExecutionEnvironment":4}],87:[function(require,module,exports){
+},{"./getNodeForCharacterOffset":160,"./getTextContentAccessor":161,"fbjs/lib/ExecutionEnvironment":4}],91:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -20367,7 +20842,7 @@ _assign(ReactDOMTextComponent.prototype, {
module.exports = ReactDOMTextComponent;
}).call(this,require('_process'))
-},{"./DOMChildrenOperations":51,"./DOMLazyTree":52,"./ReactDOMComponentTree":76,"./escapeTextContentForBrowser":145,"./reactProdInvariant":163,"./validateDOMNesting":169,"_process":43,"fbjs/lib/invariant":18,"object-assign":170}],88:[function(require,module,exports){
+},{"./DOMChildrenOperations":55,"./DOMLazyTree":56,"./ReactDOMComponentTree":80,"./escapeTextContentForBrowser":149,"./reactProdInvariant":167,"./validateDOMNesting":173,"_process":43,"fbjs/lib/invariant":18,"object-assign":42}],92:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -20530,7 +21005,7 @@ function _handleChange(event) {
module.exports = ReactDOMTextarea;
}).call(this,require('_process'))
-},{"./LinkedValueUtils":66,"./ReactDOMComponentTree":76,"./ReactUpdates":120,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"object-assign":170}],89:[function(require,module,exports){
+},{"./LinkedValueUtils":70,"./ReactDOMComponentTree":80,"./ReactUpdates":124,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"object-assign":42}],93:[function(require,module,exports){
(function (process){
/**
* Copyright 2015-present, Facebook, Inc.
@@ -20669,7 +21144,7 @@ module.exports = {
};
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],90:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],94:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -20784,7 +21259,7 @@ var ReactDOMUnknownPropertyHook = {
module.exports = ReactDOMUnknownPropertyHook;
}).call(this,require('_process'))
-},{"./DOMProperty":54,"./EventPluginRegistry":60,"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":191}],91:[function(require,module,exports){
+},{"./DOMProperty":58,"./EventPluginRegistry":64,"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":194}],95:[function(require,module,exports){
(function (process){
/**
* Copyright 2016-present, Facebook, Inc.
@@ -21148,7 +21623,7 @@ if (/[?&]react_perf\b/.test(url)) {
module.exports = ReactDebugTool;
}).call(this,require('_process'))
-},{"./ReactHostOperationHistoryHook":101,"./ReactInvalidSetStateWarningHook":106,"_process":43,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/performanceNow":23,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":191}],92:[function(require,module,exports){
+},{"./ReactHostOperationHistoryHook":105,"./ReactInvalidSetStateWarningHook":110,"_process":43,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/performanceNow":23,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":194}],96:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -21216,7 +21691,7 @@ var ReactDefaultBatchingStrategy = {
};
module.exports = ReactDefaultBatchingStrategy;
-},{"./ReactUpdates":120,"./Transaction":138,"fbjs/lib/emptyFunction":10,"object-assign":170}],93:[function(require,module,exports){
+},{"./ReactUpdates":124,"./Transaction":142,"fbjs/lib/emptyFunction":10,"object-assign":42}],97:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -21302,7 +21777,7 @@ function inject() {
module.exports = {
inject: inject
};
-},{"./ARIADOMPropertyConfig":44,"./BeforeInputEventPlugin":46,"./ChangeEventPlugin":50,"./DefaultEventPluginOrder":57,"./EnterLeaveEventPlugin":58,"./HTMLDOMPropertyConfig":64,"./ReactComponentBrowserEnvironment":70,"./ReactDOMComponent":74,"./ReactDOMComponentTree":76,"./ReactDOMEmptyComponent":78,"./ReactDOMTextComponent":87,"./ReactDOMTreeTraversal":89,"./ReactDefaultBatchingStrategy":92,"./ReactEventListener":98,"./ReactInjection":102,"./ReactReconcileTransaction":114,"./SVGDOMPropertyConfig":122,"./SelectEventPlugin":123,"./SimpleEventPlugin":124}],94:[function(require,module,exports){
+},{"./ARIADOMPropertyConfig":48,"./BeforeInputEventPlugin":50,"./ChangeEventPlugin":54,"./DefaultEventPluginOrder":61,"./EnterLeaveEventPlugin":62,"./HTMLDOMPropertyConfig":68,"./ReactComponentBrowserEnvironment":74,"./ReactDOMComponent":78,"./ReactDOMComponentTree":80,"./ReactDOMEmptyComponent":82,"./ReactDOMTextComponent":91,"./ReactDOMTreeTraversal":93,"./ReactDefaultBatchingStrategy":96,"./ReactEventListener":102,"./ReactInjection":106,"./ReactReconcileTransaction":118,"./SVGDOMPropertyConfig":126,"./SelectEventPlugin":127,"./SimpleEventPlugin":128}],98:[function(require,module,exports){
/**
* Copyright 2014-present, Facebook, Inc.
* All rights reserved.
@@ -21322,7 +21797,7 @@ module.exports = {
var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element') || 0xeac7;
module.exports = REACT_ELEMENT_TYPE;
-},{}],95:[function(require,module,exports){
+},{}],99:[function(require,module,exports){
/**
* Copyright 2014-present, Facebook, Inc.
* All rights reserved.
@@ -21352,7 +21827,7 @@ var ReactEmptyComponent = {
ReactEmptyComponent.injection = ReactEmptyComponentInjection;
module.exports = ReactEmptyComponent;
-},{}],96:[function(require,module,exports){
+},{}],100:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -21432,7 +21907,7 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = ReactErrorUtils;
}).call(this,require('_process'))
-},{"_process":43}],97:[function(require,module,exports){
+},{"_process":43}],101:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -21465,7 +21940,7 @@ var ReactEventEmitterMixin = {
};
module.exports = ReactEventEmitterMixin;
-},{"./EventPluginHub":59}],98:[function(require,module,exports){
+},{"./EventPluginHub":63}],102:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -21620,7 +22095,7 @@ var ReactEventListener = {
};
module.exports = ReactEventListener;
-},{"./PooledClass":67,"./ReactDOMComponentTree":76,"./ReactUpdates":120,"./getEventTarget":152,"fbjs/lib/EventListener":3,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/getUnboundedScrollPosition":15,"object-assign":170}],99:[function(require,module,exports){
+},{"./PooledClass":71,"./ReactDOMComponentTree":80,"./ReactUpdates":124,"./getEventTarget":156,"fbjs/lib/EventListener":3,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/getUnboundedScrollPosition":15,"object-assign":42}],103:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -21642,7 +22117,7 @@ var ReactFeatureFlags = {
};
module.exports = ReactFeatureFlags;
-},{}],100:[function(require,module,exports){
+},{}],104:[function(require,module,exports){
(function (process){
/**
* Copyright 2014-present, Facebook, Inc.
@@ -21713,7 +22188,7 @@ var ReactHostComponent = {
module.exports = ReactHostComponent;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],101:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],105:[function(require,module,exports){
/**
* Copyright 2016-present, Facebook, Inc.
* All rights reserved.
@@ -21747,7 +22222,7 @@ var ReactHostOperationHistoryHook = {
};
module.exports = ReactHostOperationHistoryHook;
-},{}],102:[function(require,module,exports){
+},{}],106:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -21781,7 +22256,7 @@ var ReactInjection = {
};
module.exports = ReactInjection;
-},{"./DOMProperty":54,"./EventPluginHub":59,"./EventPluginUtils":61,"./ReactBrowserEventEmitter":68,"./ReactComponentEnvironment":71,"./ReactEmptyComponent":95,"./ReactHostComponent":100,"./ReactUpdates":120}],103:[function(require,module,exports){
+},{"./DOMProperty":58,"./EventPluginHub":63,"./EventPluginUtils":65,"./ReactBrowserEventEmitter":72,"./ReactComponentEnvironment":75,"./ReactEmptyComponent":99,"./ReactHostComponent":104,"./ReactUpdates":124}],107:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -21905,7 +22380,7 @@ var ReactInputSelection = {
};
module.exports = ReactInputSelection;
-},{"./ReactDOMSelection":86,"fbjs/lib/containsNode":7,"fbjs/lib/focusNode":12,"fbjs/lib/getActiveElement":13}],104:[function(require,module,exports){
+},{"./ReactDOMSelection":90,"fbjs/lib/containsNode":7,"fbjs/lib/focusNode":12,"fbjs/lib/getActiveElement":13}],108:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -21953,7 +22428,7 @@ var ReactInstanceMap = {
};
module.exports = ReactInstanceMap;
-},{}],105:[function(require,module,exports){
+},{}],109:[function(require,module,exports){
(function (process){
/**
* Copyright 2016-present, Facebook, Inc.
@@ -21980,7 +22455,7 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = { debugTool: debugTool };
}).call(this,require('_process'))
-},{"./ReactDebugTool":91,"_process":43}],106:[function(require,module,exports){
+},{"./ReactDebugTool":95,"_process":43}],110:[function(require,module,exports){
(function (process){
/**
* Copyright 2016-present, Facebook, Inc.
@@ -22020,7 +22495,7 @@ var ReactInvalidSetStateWarningHook = {
module.exports = ReactInvalidSetStateWarningHook;
}).call(this,require('_process'))
-},{"_process":43,"fbjs/lib/warning":25}],107:[function(require,module,exports){
+},{"_process":43,"fbjs/lib/warning":25}],111:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -22070,7 +22545,7 @@ var ReactMarkupChecksum = {
};
module.exports = ReactMarkupChecksum;
-},{"./adler32":141}],108:[function(require,module,exports){
+},{"./adler32":145}],112:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -22611,7 +23086,7 @@ var ReactMount = {
module.exports = ReactMount;
}).call(this,require('_process'))
-},{"./DOMLazyTree":52,"./DOMProperty":54,"./ReactBrowserEventEmitter":68,"./ReactDOMComponentTree":76,"./ReactDOMContainerInfo":77,"./ReactDOMFeatureFlags":79,"./ReactFeatureFlags":99,"./ReactInstanceMap":104,"./ReactInstrumentation":105,"./ReactMarkupChecksum":107,"./ReactReconciler":115,"./ReactUpdateQueue":119,"./ReactUpdates":120,"./instantiateReactComponent":159,"./reactProdInvariant":163,"./setInnerHTML":165,"./shouldUpdateReactComponent":167,"_process":43,"fbjs/lib/emptyObject":11,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/React":187,"react/lib/ReactCurrentOwner":192}],109:[function(require,module,exports){
+},{"./DOMLazyTree":56,"./DOMProperty":58,"./ReactBrowserEventEmitter":72,"./ReactDOMComponentTree":80,"./ReactDOMContainerInfo":81,"./ReactDOMFeatureFlags":83,"./ReactFeatureFlags":103,"./ReactInstanceMap":108,"./ReactInstrumentation":109,"./ReactMarkupChecksum":111,"./ReactReconciler":119,"./ReactUpdateQueue":123,"./ReactUpdates":124,"./instantiateReactComponent":163,"./reactProdInvariant":167,"./setInnerHTML":169,"./shouldUpdateReactComponent":171,"_process":43,"fbjs/lib/emptyObject":11,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/React":190,"react/lib/ReactCurrentOwner":195}],113:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -23064,7 +23539,7 @@ var ReactMultiChild = {
module.exports = ReactMultiChild;
}).call(this,require('_process'))
-},{"./ReactChildReconciler":69,"./ReactComponentEnvironment":71,"./ReactInstanceMap":104,"./ReactInstrumentation":105,"./ReactReconciler":115,"./flattenChildren":147,"./reactProdInvariant":163,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18,"react/lib/ReactCurrentOwner":192}],110:[function(require,module,exports){
+},{"./ReactChildReconciler":73,"./ReactComponentEnvironment":75,"./ReactInstanceMap":108,"./ReactInstrumentation":109,"./ReactReconciler":119,"./flattenChildren":151,"./reactProdInvariant":167,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18,"react/lib/ReactCurrentOwner":195}],114:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -23107,7 +23582,7 @@ var ReactNodeTypes = {
module.exports = ReactNodeTypes;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"react/lib/React":187}],111:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"react/lib/React":190}],115:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -23204,7 +23679,7 @@ var ReactOwner = {
module.exports = ReactOwner;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],112:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],116:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -23232,7 +23707,7 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = ReactPropTypeLocationNames;
}).call(this,require('_process'))
-},{"_process":43}],113:[function(require,module,exports){
+},{"_process":43}],117:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -23249,7 +23724,7 @@ module.exports = ReactPropTypeLocationNames;
var ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';
module.exports = ReactPropTypesSecret;
-},{}],114:[function(require,module,exports){
+},{}],118:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -23430,7 +23905,7 @@ PooledClass.addPoolingTo(ReactReconcileTransaction);
module.exports = ReactReconcileTransaction;
}).call(this,require('_process'))
-},{"./CallbackQueue":49,"./PooledClass":67,"./ReactBrowserEventEmitter":68,"./ReactInputSelection":103,"./ReactInstrumentation":105,"./ReactUpdateQueue":119,"./Transaction":138,"_process":43,"object-assign":170}],115:[function(require,module,exports){
+},{"./CallbackQueue":53,"./PooledClass":71,"./ReactBrowserEventEmitter":72,"./ReactInputSelection":107,"./ReactInstrumentation":109,"./ReactUpdateQueue":123,"./Transaction":142,"_process":43,"object-assign":42}],119:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -23601,7 +24076,7 @@ var ReactReconciler = {
module.exports = ReactReconciler;
}).call(this,require('_process'))
-},{"./ReactInstrumentation":105,"./ReactRef":116,"_process":43,"fbjs/lib/warning":25}],116:[function(require,module,exports){
+},{"./ReactInstrumentation":109,"./ReactRef":120,"_process":43,"fbjs/lib/warning":25}],120:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -23690,7 +24165,7 @@ ReactRef.detachRefs = function (instance, element) {
};
module.exports = ReactRef;
-},{"./ReactOwner":111}],117:[function(require,module,exports){
+},{"./ReactOwner":115}],121:[function(require,module,exports){
(function (process){
/**
* Copyright 2014-present, Facebook, Inc.
@@ -23783,7 +24258,7 @@ PooledClass.addPoolingTo(ReactServerRenderingTransaction);
module.exports = ReactServerRenderingTransaction;
}).call(this,require('_process'))
-},{"./PooledClass":67,"./ReactInstrumentation":105,"./ReactServerUpdateQueue":118,"./Transaction":138,"_process":43,"object-assign":170}],118:[function(require,module,exports){
+},{"./PooledClass":71,"./ReactInstrumentation":109,"./ReactServerUpdateQueue":122,"./Transaction":142,"_process":43,"object-assign":42}],122:[function(require,module,exports){
(function (process){
/**
* Copyright 2015-present, Facebook, Inc.
@@ -23925,7 +24400,7 @@ var ReactServerUpdateQueue = function () {
module.exports = ReactServerUpdateQueue;
}).call(this,require('_process'))
-},{"./ReactUpdateQueue":119,"_process":43,"fbjs/lib/warning":25}],119:[function(require,module,exports){
+},{"./ReactUpdateQueue":123,"_process":43,"fbjs/lib/warning":25}],123:[function(require,module,exports){
(function (process){
/**
* Copyright 2015-present, Facebook, Inc.
@@ -24154,7 +24629,7 @@ var ReactUpdateQueue = {
module.exports = ReactUpdateQueue;
}).call(this,require('_process'))
-},{"./ReactInstanceMap":104,"./ReactInstrumentation":105,"./ReactUpdates":120,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/ReactCurrentOwner":192}],120:[function(require,module,exports){
+},{"./ReactInstanceMap":108,"./ReactInstrumentation":109,"./ReactUpdates":124,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/ReactCurrentOwner":195}],124:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -24408,7 +24883,7 @@ var ReactUpdates = {
module.exports = ReactUpdates;
}).call(this,require('_process'))
-},{"./CallbackQueue":49,"./PooledClass":67,"./ReactFeatureFlags":99,"./ReactReconciler":115,"./Transaction":138,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"object-assign":170}],121:[function(require,module,exports){
+},{"./CallbackQueue":53,"./PooledClass":71,"./ReactFeatureFlags":103,"./ReactReconciler":119,"./Transaction":142,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"object-assign":42}],125:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -24422,7 +24897,7 @@ module.exports = ReactUpdates;
'use strict';
module.exports = '15.4.2';
-},{}],122:[function(require,module,exports){
+},{}],126:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -24724,7 +25199,7 @@ Object.keys(ATTRS).forEach(function (key) {
});
module.exports = SVGDOMPropertyConfig;
-},{}],123:[function(require,module,exports){
+},{}],127:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -24915,7 +25390,7 @@ var SelectEventPlugin = {
};
module.exports = SelectEventPlugin;
-},{"./EventPropagators":62,"./ReactDOMComponentTree":76,"./ReactInputSelection":103,"./SyntheticEvent":129,"./isTextInputElement":161,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/getActiveElement":13,"fbjs/lib/shallowEqual":24}],124:[function(require,module,exports){
+},{"./EventPropagators":66,"./ReactDOMComponentTree":80,"./ReactInputSelection":107,"./SyntheticEvent":133,"./isTextInputElement":165,"fbjs/lib/ExecutionEnvironment":4,"fbjs/lib/getActiveElement":13,"fbjs/lib/shallowEqual":24}],128:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -25146,7 +25621,7 @@ var SimpleEventPlugin = {
module.exports = SimpleEventPlugin;
}).call(this,require('_process'))
-},{"./EventPropagators":62,"./ReactDOMComponentTree":76,"./SyntheticAnimationEvent":125,"./SyntheticClipboardEvent":126,"./SyntheticDragEvent":128,"./SyntheticEvent":129,"./SyntheticFocusEvent":130,"./SyntheticKeyboardEvent":132,"./SyntheticMouseEvent":133,"./SyntheticTouchEvent":134,"./SyntheticTransitionEvent":135,"./SyntheticUIEvent":136,"./SyntheticWheelEvent":137,"./getEventCharCode":149,"./reactProdInvariant":163,"_process":43,"fbjs/lib/EventListener":3,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18}],125:[function(require,module,exports){
+},{"./EventPropagators":66,"./ReactDOMComponentTree":80,"./SyntheticAnimationEvent":129,"./SyntheticClipboardEvent":130,"./SyntheticDragEvent":132,"./SyntheticEvent":133,"./SyntheticFocusEvent":134,"./SyntheticKeyboardEvent":136,"./SyntheticMouseEvent":137,"./SyntheticTouchEvent":138,"./SyntheticTransitionEvent":139,"./SyntheticUIEvent":140,"./SyntheticWheelEvent":141,"./getEventCharCode":153,"./reactProdInvariant":167,"_process":43,"fbjs/lib/EventListener":3,"fbjs/lib/emptyFunction":10,"fbjs/lib/invariant":18}],129:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25185,7 +25660,7 @@ function SyntheticAnimationEvent(dispatchConfig, dispatchMarker, nativeEvent, na
SyntheticEvent.augmentClass(SyntheticAnimationEvent, AnimationEventInterface);
module.exports = SyntheticAnimationEvent;
-},{"./SyntheticEvent":129}],126:[function(require,module,exports){
+},{"./SyntheticEvent":133}],130:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25223,7 +25698,7 @@ function SyntheticClipboardEvent(dispatchConfig, dispatchMarker, nativeEvent, na
SyntheticEvent.augmentClass(SyntheticClipboardEvent, ClipboardEventInterface);
module.exports = SyntheticClipboardEvent;
-},{"./SyntheticEvent":129}],127:[function(require,module,exports){
+},{"./SyntheticEvent":133}],131:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25259,7 +25734,7 @@ function SyntheticCompositionEvent(dispatchConfig, dispatchMarker, nativeEvent,
SyntheticEvent.augmentClass(SyntheticCompositionEvent, CompositionEventInterface);
module.exports = SyntheticCompositionEvent;
-},{"./SyntheticEvent":129}],128:[function(require,module,exports){
+},{"./SyntheticEvent":133}],132:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25295,7 +25770,7 @@ function SyntheticDragEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeE
SyntheticMouseEvent.augmentClass(SyntheticDragEvent, DragEventInterface);
module.exports = SyntheticDragEvent;
-},{"./SyntheticMouseEvent":133}],129:[function(require,module,exports){
+},{"./SyntheticMouseEvent":137}],133:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -25566,7 +26041,7 @@ function getPooledWarningPropertyDefinition(propName, getVal) {
}
}).call(this,require('_process'))
-},{"./PooledClass":67,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/warning":25,"object-assign":170}],130:[function(require,module,exports){
+},{"./PooledClass":71,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/warning":25,"object-assign":42}],134:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25602,7 +26077,7 @@ function SyntheticFocusEvent(dispatchConfig, dispatchMarker, nativeEvent, native
SyntheticUIEvent.augmentClass(SyntheticFocusEvent, FocusEventInterface);
module.exports = SyntheticFocusEvent;
-},{"./SyntheticUIEvent":136}],131:[function(require,module,exports){
+},{"./SyntheticUIEvent":140}],135:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25639,7 +26114,7 @@ function SyntheticInputEvent(dispatchConfig, dispatchMarker, nativeEvent, native
SyntheticEvent.augmentClass(SyntheticInputEvent, InputEventInterface);
module.exports = SyntheticInputEvent;
-},{"./SyntheticEvent":129}],132:[function(require,module,exports){
+},{"./SyntheticEvent":133}],136:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25723,7 +26198,7 @@ function SyntheticKeyboardEvent(dispatchConfig, dispatchMarker, nativeEvent, nat
SyntheticUIEvent.augmentClass(SyntheticKeyboardEvent, KeyboardEventInterface);
module.exports = SyntheticKeyboardEvent;
-},{"./SyntheticUIEvent":136,"./getEventCharCode":149,"./getEventKey":150,"./getEventModifierState":151}],133:[function(require,module,exports){
+},{"./SyntheticUIEvent":140,"./getEventCharCode":153,"./getEventKey":154,"./getEventModifierState":155}],137:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25795,7 +26270,7 @@ function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent, native
SyntheticUIEvent.augmentClass(SyntheticMouseEvent, MouseEventInterface);
module.exports = SyntheticMouseEvent;
-},{"./SyntheticUIEvent":136,"./ViewportMetrics":139,"./getEventModifierState":151}],134:[function(require,module,exports){
+},{"./SyntheticUIEvent":140,"./ViewportMetrics":143,"./getEventModifierState":155}],138:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25840,7 +26315,7 @@ function SyntheticTouchEvent(dispatchConfig, dispatchMarker, nativeEvent, native
SyntheticUIEvent.augmentClass(SyntheticTouchEvent, TouchEventInterface);
module.exports = SyntheticTouchEvent;
-},{"./SyntheticUIEvent":136,"./getEventModifierState":151}],135:[function(require,module,exports){
+},{"./SyntheticUIEvent":140,"./getEventModifierState":155}],139:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25879,7 +26354,7 @@ function SyntheticTransitionEvent(dispatchConfig, dispatchMarker, nativeEvent, n
SyntheticEvent.augmentClass(SyntheticTransitionEvent, TransitionEventInterface);
module.exports = SyntheticTransitionEvent;
-},{"./SyntheticEvent":129}],136:[function(require,module,exports){
+},{"./SyntheticEvent":133}],140:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25938,7 +26413,7 @@ function SyntheticUIEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEve
SyntheticEvent.augmentClass(SyntheticUIEvent, UIEventInterface);
module.exports = SyntheticUIEvent;
-},{"./SyntheticEvent":129,"./getEventTarget":152}],137:[function(require,module,exports){
+},{"./SyntheticEvent":133,"./getEventTarget":156}],141:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -25992,7 +26467,7 @@ function SyntheticWheelEvent(dispatchConfig, dispatchMarker, nativeEvent, native
SyntheticMouseEvent.augmentClass(SyntheticWheelEvent, WheelEventInterface);
module.exports = SyntheticWheelEvent;
-},{"./SyntheticMouseEvent":133}],138:[function(require,module,exports){
+},{"./SyntheticMouseEvent":137}],142:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -26220,7 +26695,7 @@ var TransactionImpl = {
module.exports = TransactionImpl;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],139:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],143:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -26247,7 +26722,7 @@ var ViewportMetrics = {
};
module.exports = ViewportMetrics;
-},{}],140:[function(require,module,exports){
+},{}],144:[function(require,module,exports){
(function (process){
/**
* Copyright 2014-present, Facebook, Inc.
@@ -26308,7 +26783,7 @@ function accumulateInto(current, next) {
module.exports = accumulateInto;
}).call(this,require('_process'))
-},{"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18}],141:[function(require,module,exports){
+},{"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18}],145:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -26352,7 +26827,7 @@ function adler32(data) {
}
module.exports = adler32;
-},{}],142:[function(require,module,exports){
+},{}],146:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -26442,7 +26917,7 @@ function checkReactTypeSpec(typeSpecs, values, location, componentName, element,
module.exports = checkReactTypeSpec;
}).call(this,require('_process'))
-},{"./ReactPropTypeLocationNames":112,"./ReactPropTypesSecret":113,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":191}],143:[function(require,module,exports){
+},{"./ReactPropTypeLocationNames":116,"./ReactPropTypesSecret":117,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":194}],147:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -26474,7 +26949,7 @@ var createMicrosoftUnsafeLocalFunction = function (func) {
};
module.exports = createMicrosoftUnsafeLocalFunction;
-},{}],144:[function(require,module,exports){
+},{}],148:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -26556,7 +27031,7 @@ function dangerousStyleValue(name, value, component) {
module.exports = dangerousStyleValue;
}).call(this,require('_process'))
-},{"./CSSProperty":47,"_process":43,"fbjs/lib/warning":25}],145:[function(require,module,exports){
+},{"./CSSProperty":51,"_process":43,"fbjs/lib/warning":25}],149:[function(require,module,exports){
/**
* Copyright 2016-present, Facebook, Inc.
* All rights reserved.
@@ -26679,7 +27154,7 @@ function escapeTextContentForBrowser(text) {
}
module.exports = escapeTextContentForBrowser;
-},{}],146:[function(require,module,exports){
+},{}],150:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -26742,7 +27217,7 @@ function findDOMNode(componentOrElement) {
module.exports = findDOMNode;
}).call(this,require('_process'))
-},{"./ReactDOMComponentTree":76,"./ReactInstanceMap":104,"./getHostComponentFromComposite":153,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/ReactCurrentOwner":192}],147:[function(require,module,exports){
+},{"./ReactDOMComponentTree":80,"./ReactInstanceMap":108,"./getHostComponentFromComposite":157,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/ReactCurrentOwner":195}],151:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -26821,7 +27296,7 @@ function flattenChildren(children, selfDebugID) {
module.exports = flattenChildren;
}).call(this,require('_process'))
-},{"./KeyEscapeUtils":65,"./traverseAllChildren":168,"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":191}],148:[function(require,module,exports){
+},{"./KeyEscapeUtils":69,"./traverseAllChildren":172,"_process":43,"fbjs/lib/warning":25,"react/lib/ReactComponentTreeHook":194}],152:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -26852,7 +27327,7 @@ function forEachAccumulated(arr, cb, scope) {
}
module.exports = forEachAccumulated;
-},{}],149:[function(require,module,exports){
+},{}],153:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -26902,7 +27377,7 @@ function getEventCharCode(nativeEvent) {
}
module.exports = getEventCharCode;
-},{}],150:[function(require,module,exports){
+},{}],154:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27004,7 +27479,7 @@ function getEventKey(nativeEvent) {
}
module.exports = getEventKey;
-},{"./getEventCharCode":149}],151:[function(require,module,exports){
+},{"./getEventCharCode":153}],155:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27047,7 +27522,7 @@ function getEventModifierState(nativeEvent) {
}
module.exports = getEventModifierState;
-},{}],152:[function(require,module,exports){
+},{}],156:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27082,7 +27557,7 @@ function getEventTarget(nativeEvent) {
}
module.exports = getEventTarget;
-},{}],153:[function(require,module,exports){
+},{}],157:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27112,7 +27587,7 @@ function getHostComponentFromComposite(inst) {
}
module.exports = getHostComponentFromComposite;
-},{"./ReactNodeTypes":110}],154:[function(require,module,exports){
+},{"./ReactNodeTypes":114}],158:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27153,7 +27628,7 @@ function getIteratorFn(maybeIterable) {
}
module.exports = getIteratorFn;
-},{}],155:[function(require,module,exports){
+},{}],159:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27174,7 +27649,7 @@ function getNextDebugID() {
}
module.exports = getNextDebugID;
-},{}],156:[function(require,module,exports){
+},{}],160:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27248,7 +27723,7 @@ function getNodeForCharacterOffset(root, offset) {
}
module.exports = getNodeForCharacterOffset;
-},{}],157:[function(require,module,exports){
+},{}],161:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27281,7 +27756,7 @@ function getTextContentAccessor() {
}
module.exports = getTextContentAccessor;
-},{"fbjs/lib/ExecutionEnvironment":4}],158:[function(require,module,exports){
+},{"fbjs/lib/ExecutionEnvironment":4}],162:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27382,7 +27857,7 @@ function getVendorPrefixedEventName(eventName) {
}
module.exports = getVendorPrefixedEventName;
-},{"fbjs/lib/ExecutionEnvironment":4}],159:[function(require,module,exports){
+},{"fbjs/lib/ExecutionEnvironment":4}],163:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -27513,7 +27988,7 @@ function instantiateReactComponent(node, shouldHaveDebugID) {
module.exports = instantiateReactComponent;
}).call(this,require('_process'))
-},{"./ReactCompositeComponent":72,"./ReactEmptyComponent":95,"./ReactHostComponent":100,"./getNextDebugID":155,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"object-assign":170}],160:[function(require,module,exports){
+},{"./ReactCompositeComponent":76,"./ReactEmptyComponent":99,"./ReactHostComponent":104,"./getNextDebugID":159,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"object-assign":42}],164:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27573,7 +28048,7 @@ function isEventSupported(eventNameSuffix, capture) {
}
module.exports = isEventSupported;
-},{"fbjs/lib/ExecutionEnvironment":4}],161:[function(require,module,exports){
+},{"fbjs/lib/ExecutionEnvironment":4}],165:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27624,7 +28099,7 @@ function isTextInputElement(elem) {
}
module.exports = isTextInputElement;
-},{}],162:[function(require,module,exports){
+},{}],166:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27650,7 +28125,7 @@ function quoteAttributeValueForBrowser(value) {
}
module.exports = quoteAttributeValueForBrowser;
-},{"./escapeTextContentForBrowser":145}],163:[function(require,module,exports){
+},{"./escapeTextContentForBrowser":149}],167:[function(require,module,exports){
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27689,7 +28164,7 @@ function reactProdInvariant(code) {
}
module.exports = reactProdInvariant;
-},{}],164:[function(require,module,exports){
+},{}],168:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27705,7 +28180,7 @@ module.exports = reactProdInvariant;
var ReactMount = require('./ReactMount');
module.exports = ReactMount.renderSubtreeIntoContainer;
-},{"./ReactMount":108}],165:[function(require,module,exports){
+},{"./ReactMount":112}],169:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27803,7 +28278,7 @@ if (ExecutionEnvironment.canUseDOM) {
}
module.exports = setInnerHTML;
-},{"./DOMNamespaces":53,"./createMicrosoftUnsafeLocalFunction":143,"fbjs/lib/ExecutionEnvironment":4}],166:[function(require,module,exports){
+},{"./DOMNamespaces":57,"./createMicrosoftUnsafeLocalFunction":147,"fbjs/lib/ExecutionEnvironment":4}],170:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27855,7 +28330,7 @@ if (ExecutionEnvironment.canUseDOM) {
}
module.exports = setTextContent;
-},{"./escapeTextContentForBrowser":145,"./setInnerHTML":165,"fbjs/lib/ExecutionEnvironment":4}],167:[function(require,module,exports){
+},{"./escapeTextContentForBrowser":149,"./setInnerHTML":169,"fbjs/lib/ExecutionEnvironment":4}],171:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -27897,7 +28372,7 @@ function shouldUpdateReactComponent(prevElement, nextElement) {
}
module.exports = shouldUpdateReactComponent;
-},{}],168:[function(require,module,exports){
+},{}],172:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -28076,7 +28551,7 @@ function traverseAllChildren(children, callback, traverseContext) {
module.exports = traverseAllChildren;
}).call(this,require('_process'))
-},{"./KeyEscapeUtils":65,"./ReactElementSymbol":94,"./getIteratorFn":154,"./reactProdInvariant":163,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/ReactCurrentOwner":192}],169:[function(require,module,exports){
+},{"./KeyEscapeUtils":69,"./ReactElementSymbol":98,"./getIteratorFn":158,"./reactProdInvariant":167,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"react/lib/ReactCurrentOwner":195}],173:[function(require,module,exports){
(function (process){
/**
* Copyright 2015-present, Facebook, Inc.
@@ -28461,99 +28936,7 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = validateDOMNesting;
}).call(this,require('_process'))
-},{"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/warning":25,"object-assign":170}],170:[function(require,module,exports){
-/*
-object-assign
-(c) Sindre Sorhus
-@license MIT
-*/
-
-'use strict';
-/* eslint-disable no-unused-vars */
-var getOwnPropertySymbols = Object.getOwnPropertySymbols;
-var hasOwnProperty = Object.prototype.hasOwnProperty;
-var propIsEnumerable = Object.prototype.propertyIsEnumerable;
-
-function toObject(val) {
- if (val === null || val === undefined) {
- throw new TypeError('Object.assign cannot be called with null or undefined');
- }
-
- return Object(val);
-}
-
-function shouldUseNative() {
- try {
- if (!Object.assign) {
- return false;
- }
-
- // Detect buggy property enumeration order in older V8 versions.
-
- // https://bugs.chromium.org/p/v8/issues/detail?id=4118
- var test1 = new String('abc'); // eslint-disable-line no-new-wrappers
- test1[5] = 'de';
- if (Object.getOwnPropertyNames(test1)[0] === '5') {
- return false;
- }
-
- // https://bugs.chromium.org/p/v8/issues/detail?id=3056
- var test2 = {};
- for (var i = 0; i < 10; i++) {
- test2['_' + String.fromCharCode(i)] = i;
- }
- var order2 = Object.getOwnPropertyNames(test2).map(function (n) {
- return test2[n];
- });
- if (order2.join('') !== '0123456789') {
- return false;
- }
-
- // https://bugs.chromium.org/p/v8/issues/detail?id=3056
- var test3 = {};
- 'abcdefghijklmnopqrst'.split('').forEach(function (letter) {
- test3[letter] = letter;
- });
- if (Object.keys(Object.assign({}, test3)).join('') !==
- 'abcdefghijklmnopqrst') {
- return false;
- }
-
- return true;
- } catch (err) {
- // We don't expect any of the above to throw, but better to be safe.
- return false;
- }
-}
-
-module.exports = shouldUseNative() ? Object.assign : function (target, source) {
- var from;
- var to = toObject(target);
- var symbols;
-
- for (var s = 1; s < arguments.length; s++) {
- from = Object(arguments[s]);
-
- for (var key in from) {
- if (hasOwnProperty.call(from, key)) {
- to[key] = from[key];
- }
- }
-
- if (getOwnPropertySymbols) {
- symbols = getOwnPropertySymbols(from);
- for (var i = 0; i < symbols.length; i++) {
- if (propIsEnumerable.call(from, symbols[i])) {
- to[symbols[i]] = from[symbols[i]];
- }
- }
- }
- }
-
- return to;
-};
-
-},{}],171:[function(require,module,exports){
+},{"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/warning":25,"object-assign":42}],174:[function(require,module,exports){
(function (process){
'use strict';
@@ -28641,7 +29024,7 @@ Provider.childContextTypes = {
Provider.displayName = 'Provider';
}).call(this,require('_process'))
-},{"../utils/Subscription":180,"../utils/storeShape":182,"../utils/warning":184,"_process":43,"react":"react"}],172:[function(require,module,exports){
+},{"../utils/Subscription":183,"../utils/storeShape":185,"../utils/warning":187,"_process":43,"react":"react"}],175:[function(require,module,exports){
(function (process){
'use strict';
@@ -28921,7 +29304,7 @@ selectorFactory) {
}
}).call(this,require('_process'))
-},{"../utils/Subscription":180,"../utils/storeShape":182,"_process":43,"hoist-non-react-statics":26,"invariant":27,"react":"react"}],173:[function(require,module,exports){
+},{"../utils/Subscription":183,"../utils/storeShape":185,"_process":43,"hoist-non-react-statics":26,"invariant":27,"react":"react"}],176:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -29050,7 +29433,7 @@ function createConnect() {
}
exports.default = createConnect();
-},{"../components/connectAdvanced":172,"../utils/shallowEqual":181,"./mapDispatchToProps":174,"./mapStateToProps":175,"./mergeProps":176,"./selectorFactory":177}],174:[function(require,module,exports){
+},{"../components/connectAdvanced":175,"../utils/shallowEqual":184,"./mapDispatchToProps":177,"./mapStateToProps":178,"./mergeProps":179,"./selectorFactory":180}],177:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -29079,7 +29462,7 @@ function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
}
exports.default = [whenMapDispatchToPropsIsFunction, whenMapDispatchToPropsIsMissing, whenMapDispatchToPropsIsObject];
-},{"./wrapMapToProps":179,"redux":"redux"}],175:[function(require,module,exports){
+},{"./wrapMapToProps":182,"redux":"redux"}],178:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -29099,7 +29482,7 @@ function whenMapStateToPropsIsMissing(mapStateToProps) {
}
exports.default = [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing];
-},{"./wrapMapToProps":179}],176:[function(require,module,exports){
+},{"./wrapMapToProps":182}],179:[function(require,module,exports){
(function (process){
'use strict';
@@ -29161,7 +29544,7 @@ function whenMergePropsIsOmitted(mergeProps) {
exports.default = [whenMergePropsIsFunction, whenMergePropsIsOmitted];
}).call(this,require('_process'))
-},{"../utils/verifyPlainObject":183,"_process":43}],177:[function(require,module,exports){
+},{"../utils/verifyPlainObject":186,"_process":43}],180:[function(require,module,exports){
(function (process){
'use strict';
@@ -29278,7 +29661,7 @@ function finalPropsSelectorFactory(dispatch, _ref2) {
}
}).call(this,require('_process'))
-},{"./verifySubselectors":178,"_process":43}],178:[function(require,module,exports){
+},{"./verifySubselectors":181,"_process":43}],181:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -29305,7 +29688,7 @@ function verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps, dis
verify(mapDispatchToProps, 'mapDispatchToProps', displayName);
verify(mergeProps, 'mergeProps', displayName);
}
-},{"../utils/warning":184}],179:[function(require,module,exports){
+},{"../utils/warning":187}],182:[function(require,module,exports){
(function (process){
'use strict';
@@ -29385,7 +29768,7 @@ function wrapMapToPropsFunc(mapToProps, methodName) {
}
}).call(this,require('_process'))
-},{"../utils/verifyPlainObject":183,"_process":43}],180:[function(require,module,exports){
+},{"../utils/verifyPlainObject":186,"_process":43}],183:[function(require,module,exports){
"use strict";
exports.__esModule = true;
@@ -29479,7 +29862,7 @@ var Subscription = function () {
}();
exports.default = Subscription;
-},{}],181:[function(require,module,exports){
+},{}],184:[function(require,module,exports){
"use strict";
exports.__esModule = true;
@@ -29503,7 +29886,7 @@ function shallowEqual(a, b) {
return countA === countB;
}
-},{}],182:[function(require,module,exports){
+},{}],185:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -29515,7 +29898,7 @@ exports.default = _react.PropTypes.shape({
dispatch: _react.PropTypes.func.isRequired,
getState: _react.PropTypes.func.isRequired
});
-},{"react":"react"}],183:[function(require,module,exports){
+},{"react":"react"}],186:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -29536,7 +29919,7 @@ function verifyPlainObject(value, displayName, methodName) {
(0, _warning2.default)(methodName + '() in ' + displayName + ' must return a plain object. Instead received ' + value + '.');
}
}
-},{"./warning":184,"lodash/isPlainObject":42}],184:[function(require,module,exports){
+},{"./warning":187,"lodash/isPlainObject":41}],187:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -29562,9 +29945,9 @@ function warning(message) {
} catch (e) {}
/* eslint-enable no-empty */
}
-},{}],185:[function(require,module,exports){
-arguments[4][65][0].apply(exports,arguments)
-},{"dup":65}],186:[function(require,module,exports){
+},{}],188:[function(require,module,exports){
+arguments[4][69][0].apply(exports,arguments)
+},{"dup":69}],189:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -29679,7 +30062,7 @@ var PooledClass = {
module.exports = PooledClass;
}).call(this,require('_process'))
-},{"./reactProdInvariant":207,"_process":43,"fbjs/lib/invariant":18}],187:[function(require,module,exports){
+},{"./reactProdInvariant":210,"_process":43,"fbjs/lib/invariant":18}],190:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -29771,7 +30154,7 @@ var React = {
module.exports = React;
}).call(this,require('_process'))
-},{"./ReactChildren":188,"./ReactClass":189,"./ReactComponent":190,"./ReactDOMFactories":193,"./ReactElement":194,"./ReactElementValidator":196,"./ReactPropTypes":199,"./ReactPureComponent":201,"./ReactVersion":202,"./onlyChild":206,"_process":43,"fbjs/lib/warning":25,"object-assign":209}],188:[function(require,module,exports){
+},{"./ReactChildren":191,"./ReactClass":192,"./ReactComponent":193,"./ReactDOMFactories":196,"./ReactElement":197,"./ReactElementValidator":199,"./ReactPropTypes":202,"./ReactPureComponent":204,"./ReactVersion":205,"./onlyChild":209,"_process":43,"fbjs/lib/warning":25,"object-assign":42}],191:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -29962,7 +30345,7 @@ var ReactChildren = {
};
module.exports = ReactChildren;
-},{"./PooledClass":186,"./ReactElement":194,"./traverseAllChildren":208,"fbjs/lib/emptyFunction":10}],189:[function(require,module,exports){
+},{"./PooledClass":189,"./ReactElement":197,"./traverseAllChildren":211,"fbjs/lib/emptyFunction":10}],192:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -30682,7 +31065,7 @@ var ReactClass = {
module.exports = ReactClass;
}).call(this,require('_process'))
-},{"./ReactComponent":190,"./ReactElement":194,"./ReactNoopUpdateQueue":197,"./ReactPropTypeLocationNames":198,"./reactProdInvariant":207,"_process":43,"fbjs/lib/emptyObject":11,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"object-assign":209}],190:[function(require,module,exports){
+},{"./ReactComponent":193,"./ReactElement":197,"./ReactNoopUpdateQueue":200,"./ReactPropTypeLocationNames":201,"./reactProdInvariant":210,"_process":43,"fbjs/lib/emptyObject":11,"fbjs/lib/invariant":18,"fbjs/lib/warning":25,"object-assign":42}],193:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -30803,7 +31186,7 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = ReactComponent;
}).call(this,require('_process'))
-},{"./ReactNoopUpdateQueue":197,"./canDefineProperty":203,"./reactProdInvariant":207,"_process":43,"fbjs/lib/emptyObject":11,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],191:[function(require,module,exports){
+},{"./ReactNoopUpdateQueue":200,"./canDefineProperty":206,"./reactProdInvariant":210,"_process":43,"fbjs/lib/emptyObject":11,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],194:[function(require,module,exports){
(function (process){
/**
* Copyright 2016-present, Facebook, Inc.
@@ -31140,7 +31523,7 @@ var ReactComponentTreeHook = {
module.exports = ReactComponentTreeHook;
}).call(this,require('_process'))
-},{"./ReactCurrentOwner":192,"./reactProdInvariant":207,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],192:[function(require,module,exports){
+},{"./ReactCurrentOwner":195,"./reactProdInvariant":210,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],195:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -31171,7 +31554,7 @@ var ReactCurrentOwner = {
};
module.exports = ReactCurrentOwner;
-},{}],193:[function(require,module,exports){
+},{}],196:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -31344,7 +31727,7 @@ var ReactDOMFactories = {
module.exports = ReactDOMFactories;
}).call(this,require('_process'))
-},{"./ReactElement":194,"./ReactElementValidator":196,"_process":43}],194:[function(require,module,exports){
+},{"./ReactElement":197,"./ReactElementValidator":199,"_process":43}],197:[function(require,module,exports){
(function (process){
/**
* Copyright 2014-present, Facebook, Inc.
@@ -31688,9 +32071,9 @@ ReactElement.isValidElement = function (object) {
module.exports = ReactElement;
}).call(this,require('_process'))
-},{"./ReactCurrentOwner":192,"./ReactElementSymbol":195,"./canDefineProperty":203,"_process":43,"fbjs/lib/warning":25,"object-assign":209}],195:[function(require,module,exports){
-arguments[4][94][0].apply(exports,arguments)
-},{"dup":94}],196:[function(require,module,exports){
+},{"./ReactCurrentOwner":195,"./ReactElementSymbol":198,"./canDefineProperty":206,"_process":43,"fbjs/lib/warning":25,"object-assign":42}],198:[function(require,module,exports){
+arguments[4][98][0].apply(exports,arguments)
+},{"dup":98}],199:[function(require,module,exports){
(function (process){
/**
* Copyright 2014-present, Facebook, Inc.
@@ -31927,7 +32310,7 @@ var ReactElementValidator = {
module.exports = ReactElementValidator;
}).call(this,require('_process'))
-},{"./ReactComponentTreeHook":191,"./ReactCurrentOwner":192,"./ReactElement":194,"./canDefineProperty":203,"./checkReactTypeSpec":204,"./getIteratorFn":205,"_process":43,"fbjs/lib/warning":25}],197:[function(require,module,exports){
+},{"./ReactComponentTreeHook":194,"./ReactCurrentOwner":195,"./ReactElement":197,"./canDefineProperty":206,"./checkReactTypeSpec":207,"./getIteratorFn":208,"_process":43,"fbjs/lib/warning":25}],200:[function(require,module,exports){
(function (process){
/**
* Copyright 2015-present, Facebook, Inc.
@@ -32026,7 +32409,7 @@ var ReactNoopUpdateQueue = {
module.exports = ReactNoopUpdateQueue;
}).call(this,require('_process'))
-},{"_process":43,"fbjs/lib/warning":25}],198:[function(require,module,exports){
+},{"_process":43,"fbjs/lib/warning":25}],201:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -32054,7 +32437,7 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = ReactPropTypeLocationNames;
}).call(this,require('_process'))
-},{"_process":43}],199:[function(require,module,exports){
+},{"_process":43}],202:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -32491,9 +32874,9 @@ function getClassName(propValue) {
module.exports = ReactPropTypes;
}).call(this,require('_process'))
-},{"./ReactElement":194,"./ReactPropTypeLocationNames":198,"./ReactPropTypesSecret":200,"./getIteratorFn":205,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/warning":25}],200:[function(require,module,exports){
-arguments[4][113][0].apply(exports,arguments)
-},{"dup":113}],201:[function(require,module,exports){
+},{"./ReactElement":197,"./ReactPropTypeLocationNames":201,"./ReactPropTypesSecret":203,"./getIteratorFn":208,"_process":43,"fbjs/lib/emptyFunction":10,"fbjs/lib/warning":25}],203:[function(require,module,exports){
+arguments[4][117][0].apply(exports,arguments)
+},{"dup":117}],204:[function(require,module,exports){
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
@@ -32535,9 +32918,9 @@ _assign(ReactPureComponent.prototype, ReactComponent.prototype);
ReactPureComponent.prototype.isPureReactComponent = true;
module.exports = ReactPureComponent;
-},{"./ReactComponent":190,"./ReactNoopUpdateQueue":197,"fbjs/lib/emptyObject":11,"object-assign":209}],202:[function(require,module,exports){
-arguments[4][121][0].apply(exports,arguments)
-},{"dup":121}],203:[function(require,module,exports){
+},{"./ReactComponent":193,"./ReactNoopUpdateQueue":200,"fbjs/lib/emptyObject":11,"object-assign":42}],205:[function(require,module,exports){
+arguments[4][125][0].apply(exports,arguments)
+},{"dup":125}],206:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -32566,7 +32949,7 @@ if (process.env.NODE_ENV !== 'production') {
module.exports = canDefineProperty;
}).call(this,require('_process'))
-},{"_process":43}],204:[function(require,module,exports){
+},{"_process":43}],207:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -32656,9 +33039,9 @@ function checkReactTypeSpec(typeSpecs, values, location, componentName, element,
module.exports = checkReactTypeSpec;
}).call(this,require('_process'))
-},{"./ReactComponentTreeHook":191,"./ReactPropTypeLocationNames":198,"./ReactPropTypesSecret":200,"./reactProdInvariant":207,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],205:[function(require,module,exports){
-arguments[4][154][0].apply(exports,arguments)
-},{"dup":154}],206:[function(require,module,exports){
+},{"./ReactComponentTreeHook":194,"./ReactPropTypeLocationNames":201,"./ReactPropTypesSecret":203,"./reactProdInvariant":210,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],208:[function(require,module,exports){
+arguments[4][158][0].apply(exports,arguments)
+},{"dup":158}],209:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -32699,9 +33082,9 @@ function onlyChild(children) {
module.exports = onlyChild;
}).call(this,require('_process'))
-},{"./ReactElement":194,"./reactProdInvariant":207,"_process":43,"fbjs/lib/invariant":18}],207:[function(require,module,exports){
-arguments[4][163][0].apply(exports,arguments)
-},{"dup":163}],208:[function(require,module,exports){
+},{"./ReactElement":197,"./reactProdInvariant":210,"_process":43,"fbjs/lib/invariant":18}],210:[function(require,module,exports){
+arguments[4][167][0].apply(exports,arguments)
+},{"dup":167}],211:[function(require,module,exports){
(function (process){
/**
* Copyright 2013-present, Facebook, Inc.
@@ -32880,9 +33263,7 @@ function traverseAllChildren(children, callback, traverseContext) {
module.exports = traverseAllChildren;
}).call(this,require('_process'))
-},{"./KeyEscapeUtils":185,"./ReactCurrentOwner":192,"./ReactElementSymbol":195,"./getIteratorFn":205,"./reactProdInvariant":207,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],209:[function(require,module,exports){
-arguments[4][170][0].apply(exports,arguments)
-},{"dup":170}],210:[function(require,module,exports){
+},{"./KeyEscapeUtils":188,"./ReactCurrentOwner":195,"./ReactElementSymbol":198,"./getIteratorFn":208,"./reactProdInvariant":210,"_process":43,"fbjs/lib/invariant":18,"fbjs/lib/warning":25}],212:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -33020,7 +33401,7 @@ function printBuffer(buffer, options) {
}
});
}
-},{"./diff":212,"./helpers":213}],211:[function(require,module,exports){
+},{"./diff":214,"./helpers":215}],213:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -33067,7 +33448,7 @@ exports.default = {
transformer: undefined
};
module.exports = exports["default"];
-},{}],212:[function(require,module,exports){
+},{}],214:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -33162,7 +33543,7 @@ function diffLogger(prevState, newState, logger, isCollapsed) {
}
}
module.exports = exports['default'];
-},{"deep-diff":2}],213:[function(require,module,exports){
+},{"deep-diff":2}],215:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -33182,7 +33563,7 @@ var formatTime = exports.formatTime = function formatTime(time) {
// Use performance API if it's available in order to get better precision
var timer = exports.timer = typeof performance !== "undefined" && performance !== null && typeof performance.now === "function" ? performance : Date;
-},{}],214:[function(require,module,exports){
+},{}],216:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -33241,7 +33622,7 @@ function applyMiddleware() {
};
};
}
-},{"./compose":217}],215:[function(require,module,exports){
+},{"./compose":219}],217:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -33293,7 +33674,7 @@ function bindActionCreators(actionCreators, dispatch) {
}
return boundActionCreators;
}
-},{}],216:[function(require,module,exports){
+},{}],218:[function(require,module,exports){
(function (process){
'use strict';
@@ -33439,7 +33820,7 @@ function combineReducers(reducers) {
}
}).call(this,require('_process'))
-},{"./createStore":218,"./utils/warning":219,"_process":43,"lodash/isPlainObject":42}],217:[function(require,module,exports){
+},{"./createStore":220,"./utils/warning":221,"_process":43,"lodash/isPlainObject":41}],219:[function(require,module,exports){
"use strict";
exports.__esModule = true;
@@ -33478,7 +33859,7 @@ function compose() {
}, last.apply(undefined, arguments));
};
}
-},{}],218:[function(require,module,exports){
+},{}],220:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -33740,7 +34121,7 @@ function createStore(reducer, preloadedState, enhancer) {
replaceReducer: replaceReducer
}, _ref2[_symbolObservable2['default']] = observable, _ref2;
}
-},{"lodash/isPlainObject":42,"symbol-observable":220}],219:[function(require,module,exports){
+},{"lodash/isPlainObject":41,"symbol-observable":223}],221:[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -33766,10 +34147,248 @@ function warning(message) {
} catch (e) {}
/* eslint-enable no-empty */
}
-},{}],220:[function(require,module,exports){
+},{}],222:[function(require,module,exports){
+/**
+ * lodash 3.1.2 (Custom Build) <https://lodash.com/>
+ * Build: `lodash modern modularize exports="npm" -o ./`
+ * Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/>
+ * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
+ * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ * Available under MIT license <https://lodash.com/license>
+ */
+var getNative = require('lodash._getnative'),
+ isArguments = require('lodash.isarguments'),
+ isArray = require('lodash.isarray');
+
+/** Used to detect unsigned integer values. */
+var reIsUint = /^\d+$/;
+
+/** Used for native method references. */
+var objectProto = Object.prototype;
+
+/** Used to check objects for own properties. */
+var hasOwnProperty = objectProto.hasOwnProperty;
+
+/* Native method references for those with the same name as other `lodash` methods. */
+var nativeKeys = getNative(Object, 'keys');
+
+/**
+ * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer)
+ * of an array-like value.
+ */
+var MAX_SAFE_INTEGER = 9007199254740991;
+
+/**
+ * The base implementation of `_.property` without support for deep paths.
+ *
+ * @private
+ * @param {string} key The key of the property to get.
+ * @returns {Function} Returns the new function.
+ */
+function baseProperty(key) {
+ return function(object) {
+ return object == null ? undefined : object[key];
+ };
+}
+
+/**
+ * Gets the "length" property value of `object`.
+ *
+ * **Note:** This function is used to avoid a [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792)
+ * that affects Safari on at least iOS 8.1-8.3 ARM64.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {*} Returns the "length" value.
+ */
+var getLength = baseProperty('length');
+
+/**
+ * Checks if `value` is array-like.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
+ */
+function isArrayLike(value) {
+ return value != null && isLength(getLength(value));
+}
+
+/**
+ * Checks if `value` is a valid array-like index.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
+ * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
+ */
+function isIndex(value, length) {
+ value = (typeof value == 'number' || reIsUint.test(value)) ? +value : -1;
+ length = length == null ? MAX_SAFE_INTEGER : length;
+ return value > -1 && value % 1 == 0 && value < length;
+}
+
+/**
+ * Checks if `value` is a valid array-like length.
+ *
+ * **Note:** This function is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
+ */
+function isLength(value) {
+ return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
+}
+
+/**
+ * A fallback implementation of `Object.keys` which creates an array of the
+ * own enumerable property names of `object`.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ */
+function shimKeys(object) {
+ var props = keysIn(object),
+ propsLength = props.length,
+ length = propsLength && object.length;
+
+ var allowIndexes = !!length && isLength(length) &&
+ (isArray(object) || isArguments(object));
+
+ var index = -1,
+ result = [];
+
+ while (++index < propsLength) {
+ var key = props[index];
+ if ((allowIndexes && isIndex(key, length)) || hasOwnProperty.call(object, key)) {
+ result.push(key);
+ }
+ }
+ return result;
+}
+
+/**
+ * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`.
+ * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is an object, else `false`.
+ * @example
+ *
+ * _.isObject({});
+ * // => true
+ *
+ * _.isObject([1, 2, 3]);
+ * // => true
+ *
+ * _.isObject(1);
+ * // => false
+ */
+function isObject(value) {
+ // Avoid a V8 JIT bug in Chrome 19-20.
+ // See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
+ var type = typeof value;
+ return !!value && (type == 'object' || type == 'function');
+}
+
+/**
+ * Creates an array of the own enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects. See the
+ * [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys)
+ * for more details.
+ *
+ * @static
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keys(new Foo);
+ * // => ['a', 'b'] (iteration order is not guaranteed)
+ *
+ * _.keys('hi');
+ * // => ['0', '1']
+ */
+var keys = !nativeKeys ? shimKeys : function(object) {
+ var Ctor = object == null ? undefined : object.constructor;
+ if ((typeof Ctor == 'function' && Ctor.prototype === object) ||
+ (typeof object != 'function' && isArrayLike(object))) {
+ return shimKeys(object);
+ }
+ return isObject(object) ? nativeKeys(object) : [];
+};
+
+/**
+ * Creates an array of the own and inherited enumerable property names of `object`.
+ *
+ * **Note:** Non-object values are coerced to objects.
+ *
+ * @static
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @returns {Array} Returns the array of property names.
+ * @example
+ *
+ * function Foo() {
+ * this.a = 1;
+ * this.b = 2;
+ * }
+ *
+ * Foo.prototype.c = 3;
+ *
+ * _.keysIn(new Foo);
+ * // => ['a', 'b', 'c'] (iteration order is not guaranteed)
+ */
+function keysIn(object) {
+ if (object == null) {
+ return [];
+ }
+ if (!isObject(object)) {
+ object = Object(object);
+ }
+ var length = object.length;
+ length = (length && isLength(length) &&
+ (isArray(object) || isArguments(object)) && length) || 0;
+
+ var Ctor = object.constructor,
+ index = -1,
+ isProto = typeof Ctor == 'function' && Ctor.prototype === object,
+ result = Array(length),
+ skipIndexes = length > 0;
+
+ while (++index < length) {
+ result[index] = (index + '');
+ }
+ for (var key in object) {
+ if (!(skipIndexes && isIndex(key, length)) &&
+ !(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) {
+ result.push(key);
+ }
+ }
+ return result;
+}
+
+module.exports = keys;
+
+},{"lodash._getnative":28,"lodash.isarguments":30,"lodash.isarray":31}],223:[function(require,module,exports){
module.exports = require('./lib/index');
-},{"./lib/index":221}],221:[function(require,module,exports){
+},{"./lib/index":224}],224:[function(require,module,exports){
(function (global){
'use strict';
@@ -33802,7 +34421,7 @@ var result = (0, _ponyfill2['default'])(root);
exports['default'] = result;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{"./ponyfill":222}],222:[function(require,module,exports){
+},{"./ponyfill":225}],225:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -50965,7 +51584,42 @@ function symbolObservablePonyfill(root) {
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
-},{}],"react-codemirror":[function(require,module,exports){
+},{}],"prop-types":[function(require,module,exports){
+(function (process){
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+if (process.env.NODE_ENV !== 'production') {
+ var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' &&
+ Symbol.for &&
+ Symbol.for('react.element')) ||
+ 0xeac7;
+
+ var isValidElement = function(object) {
+ return typeof object === 'object' &&
+ object !== null &&
+ object.$$typeof === REACT_ELEMENT_TYPE;
+ };
+
+ // By explicitly using `prop-types` you are opting into new development behavior.
+ // http://fb.me/prop-types-in-prod
+ var throwOnDirectAccess = true;
+ module.exports = require('./factoryWithTypeCheckers')(isValidElement, throwOnDirectAccess);
+} else {
+ // By explicitly using `prop-types` you are opting into new production behavior.
+ // http://fb.me/prop-types-in-prod
+ module.exports = require('./factoryWithThrowingShims')();
+}
+
+}).call(this,require('_process'))
+
+},{"./factoryWithThrowingShims":45,"./factoryWithTypeCheckers":46,"_process":43}],"react-codemirror":[function(require,module,exports){
'use strict';
var React = require('react');
@@ -51082,7 +51736,7 @@ module.exports = CodeMirror;
module.exports = require('./lib/ReactDOM');
-},{"./lib/ReactDOM":73}],"react-redux":[function(require,module,exports){
+},{"./lib/ReactDOM":77}],"react-redux":[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -51105,12 +51759,12 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
exports.Provider = _Provider2.default;
exports.connectAdvanced = _connectAdvanced2.default;
exports.connect = _connect2.default;
-},{"./components/Provider":171,"./components/connectAdvanced":172,"./connect/connect":173}],"react":[function(require,module,exports){
+},{"./components/Provider":174,"./components/connectAdvanced":175,"./connect/connect":176}],"react":[function(require,module,exports){
'use strict';
module.exports = require('./lib/React');
-},{"./lib/React":187}],"redux-logger":[function(require,module,exports){
+},{"./lib/React":190}],"redux-logger":[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
@@ -51224,7 +51878,7 @@ function createLogger() {
exports.default = createLogger;
module.exports = exports['default'];
-},{"./core":210,"./defaults":211,"./helpers":213}],"redux-thunk":[function(require,module,exports){
+},{"./core":212,"./defaults":213,"./helpers":215}],"redux-thunk":[function(require,module,exports){
'use strict';
exports.__esModule = true;
@@ -51298,7 +51952,7 @@ exports.applyMiddleware = _applyMiddleware2['default'];
exports.compose = _compose2['default'];
}).call(this,require('_process'))
-},{"./applyMiddleware":214,"./bindActionCreators":215,"./combineReducers":216,"./compose":217,"./createStore":218,"./utils/warning":219,"_process":43}],"shallowequal":[function(require,module,exports){
+},{"./applyMiddleware":216,"./bindActionCreators":217,"./combineReducers":218,"./compose":219,"./createStore":220,"./utils/warning":221,"_process":43}],"shallowequal":[function(require,module,exports){
'use strict';
var fetchKeys = require('lodash.keys');
@@ -51347,6 +52001,6 @@ module.exports = function shallowEqual(objA, objB, compare, compareContext) {
return true;
};
-},{"lodash.keys":32}]},{},[])
+},{"lodash.keys":222}]},{},[])
//# sourceMappingURL=vendor.js.map
diff --git a/mitmproxy/types/multidict.py b/mitmproxy/types/multidict.py
index c4f42580..bd9766a3 100644
--- a/mitmproxy/types/multidict.py
+++ b/mitmproxy/types/multidict.py
@@ -155,22 +155,6 @@ class _MultiDict(MutableMapping, metaclass=ABCMeta):
else:
return super().items()
- def collect(self):
- """
- Returns a list of (key, value) tuples, where values are either
- singular if there is only one matching item for a key, or a list
- if there are more than one. The order of the keys matches the order
- in the underlying fields list.
- """
- coll = []
- for key in self:
- values = self.get_all(key)
- if len(values) == 1:
- coll.append([key, values[0]])
- else:
- coll.append([key, values])
- return coll
-
class MultiDict(_MultiDict, serializable.Serializable):
def __init__(self, fields=()):
diff --git a/mitmproxy/utils/human.py b/mitmproxy/utils/human.py
index 72e96d30..b3934846 100644
--- a/mitmproxy/utils/human.py
+++ b/mitmproxy/utils/human.py
@@ -1,7 +1,7 @@
import datetime
+import ipaddress
import time
-
SIZE_TABLE = [
("b", 1024 ** 0),
("k", 1024 ** 1),
@@ -62,3 +62,20 @@ def format_timestamp(s):
def format_timestamp_with_milli(s):
d = datetime.datetime.fromtimestamp(s)
return d.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
+
+
+def format_address(address: tuple) -> str:
+ """
+ This function accepts IPv4/IPv6 tuples and
+ returns the formatted address string with port number
+ """
+ try:
+ host = ipaddress.ip_address(address[0])
+ if host.version == 4:
+ return "{}:{}".format(str(host), address[1])
+ # If IPv6 is mapped to IPv4
+ elif host.ipv4_mapped:
+ return "{}:{}".format(str(host.ipv4_mapped), address[1])
+ return "[{}]:{}".format(str(host), address[1])
+ except ValueError:
+ return "{}:{}".format(address[0], address[1])
diff --git a/mitmproxy/utils/sliding_window.py b/mitmproxy/utils/sliding_window.py
index 4714b8e3..0a65f5e4 100644
--- a/mitmproxy/utils/sliding_window.py
+++ b/mitmproxy/utils/sliding_window.py
@@ -1,10 +1,10 @@
import itertools
-from typing import TypeVar, Iterator, Tuple, Optional
+from typing import TypeVar, Iterable, Iterator, Tuple, Optional
T = TypeVar('T')
-def window(iterator: Iterator[T], behind: int = 0, ahead: int = 0) -> Iterator[Tuple[Optional[T]]]:
+def window(iterator: Iterable[T], behind: int = 0, ahead: int = 0) -> Iterator[Tuple[Optional[T], ...]]:
"""
Sliding window for an iterator.
diff --git a/mitmproxy/utils/strutils.py b/mitmproxy/utils/strutils.py
index 1b90c2e5..db0cfd2e 100644
--- a/mitmproxy/utils/strutils.py
+++ b/mitmproxy/utils/strutils.py
@@ -25,9 +25,10 @@ def always_str(str_or_bytes: Optional[AnyStr], *decode_args) -> Optional[str]:
raise TypeError("Expected str or bytes, but got {}.".format(type(str_or_bytes).__name__))
-# Translate control characters to "safe" characters. This implementation initially
-# replaced them with the matching control pictures (http://unicode.org/charts/PDF/U2400.pdf),
-# but that turned out to render badly with monospace fonts. We are back to "." therefore.
+# Translate control characters to "safe" characters. This implementation
+# initially replaced them with the matching control pictures
+# (http://unicode.org/charts/PDF/U2400.pdf), but that turned out to render badly
+# with monospace fonts. We are back to "." therefore.
_control_char_trans = {
x: ord(".") # x + 0x2400 for unicode control group pictures
for x in range(32)
diff --git a/mitmproxy/utils/typecheck.py b/mitmproxy/utils/typecheck.py
index e8e2121e..a5f27fee 100644
--- a/mitmproxy/utils/typecheck.py
+++ b/mitmproxy/utils/typecheck.py
@@ -1,20 +1,47 @@
import typing
-def check_type(name: str, value: typing.Any, typeinfo: type) -> None:
+def check_command_type(value: typing.Any, typeinfo: typing.Any) -> bool:
"""
- This function checks if the provided value is an instance of typeinfo
- and raises a TypeError otherwise.
+ Check if the provided value is an instance of typeinfo. Returns True if the
+ types match, False otherwise. This function supports only those types
+ required for command return values.
+ """
+ typename = str(typeinfo)
+ if typename.startswith("typing.Sequence"):
+ try:
+ T = typeinfo.__args__[0] # type: ignore
+ except AttributeError:
+ # Python 3.5.0
+ T = typeinfo.__parameters__[0] # type: ignore
+ if not isinstance(value, (tuple, list)):
+ return False
+ for v in value:
+ if not check_command_type(v, T):
+ return False
+ elif typename.startswith("typing.Union"):
+ try:
+ types = typeinfo.__args__ # type: ignore
+ except AttributeError:
+ # Python 3.5.x
+ types = typeinfo.__union_params__ # type: ignore
+ for T in types:
+ checks = [check_command_type(value, T) for T in types]
+ if not any(checks):
+ return False
+ elif value is None and typeinfo is None:
+ return True
+ elif not isinstance(value, typeinfo):
+ return False
+ return True
- The following types from the typing package have specialized support:
- - Union
- - Tuple
- - IO
+def check_option_type(name: str, value: typing.Any, typeinfo: typing.Any) -> None:
+ """
+ Check if the provided value is an instance of typeinfo and raises a
+ TypeError otherwise. This function supports only those types required for
+ options.
"""
- # If we realize that we need to extend this list substantially, it may make sense
- # to use typeguard for this, but right now it's not worth the hassle for 16 lines of code.
-
e = TypeError("Expected {} for {}, but got {}.".format(
typeinfo,
name,
@@ -32,7 +59,7 @@ def check_type(name: str, value: typing.Any, typeinfo: type) -> None:
for T in types:
try:
- check_type(name, value, T)
+ check_option_type(name, value, T)
except TypeError:
pass
else:
@@ -50,7 +77,7 @@ def check_type(name: str, value: typing.Any, typeinfo: type) -> None:
if len(types) != len(value):
raise e
for i, (x, T) in enumerate(zip(value, types)):
- check_type("{}[{}]".format(name, i), x, T)
+ check_option_type("{}[{}]".format(name, i), x, T)
return
elif typename.startswith("typing.Sequence"):
try:
@@ -58,15 +85,16 @@ def check_type(name: str, value: typing.Any, typeinfo: type) -> None:
except AttributeError:
# Python 3.5.0
T = typeinfo.__parameters__[0] # type: ignore
-
if not isinstance(value, (tuple, list)):
raise e
for v in value:
- check_type(name, v, T)
+ check_option_type(name, v, T)
elif typename.startswith("typing.IO"):
if hasattr(value, "read"):
return
else:
raise e
+ elif typename.startswith("typing.Any"):
+ return
elif not isinstance(value, typeinfo):
raise e
diff --git a/mitmproxy/utils/version_check.py b/mitmproxy/utils/version_check.py
index 4cf2b9e6..22d6d75c 100644
--- a/mitmproxy/utils/version_check.py
+++ b/mitmproxy/utils/version_check.py
@@ -8,17 +8,17 @@ import os.path
import OpenSSL
-PYOPENSSL_MIN_VERSION = (0, 15)
+PYOPENSSL_MIN_VERSION = (16, 0)
def check_pyopenssl_version(min_version=PYOPENSSL_MIN_VERSION, fp=sys.stderr):
- min_version_str = u".".join(str(x) for x in min_version)
+ min_version_str = ".".join(str(x) for x in min_version)
try:
v = tuple(int(x) for x in OpenSSL.__version__.split(".")[:2])
except ValueError:
print(
- u"Cannot parse pyOpenSSL version: {}"
- u"mitmproxy requires pyOpenSSL {} or greater.".format(
+ "Cannot parse pyOpenSSL version: {}"
+ "mitmproxy requires pyOpenSSL {} or greater.".format(
OpenSSL.__version__, min_version_str
),
file=fp
@@ -26,15 +26,15 @@ def check_pyopenssl_version(min_version=PYOPENSSL_MIN_VERSION, fp=sys.stderr):
return
if v < min_version:
print(
- u"You are using an outdated version of pyOpenSSL: "
- u"mitmproxy requires pyOpenSSL {} or greater.".format(min_version_str),
+ "You are using an outdated version of pyOpenSSL: "
+ "mitmproxy requires pyOpenSSL {} or greater.".format(min_version_str),
file=fp
)
# Some users apparently have multiple versions of pyOpenSSL installed.
# Report which one we got.
pyopenssl_path = os.path.dirname(inspect.getfile(OpenSSL))
print(
- u"Your pyOpenSSL {} installation is located at {}".format(
+ "Your pyOpenSSL {} installation is located at {}".format(
OpenSSL.__version__, pyopenssl_path
),
file=fp
diff --git a/mitmproxy/version.py b/mitmproxy/version.py
index 006ec868..3cae2a04 100644
--- a/mitmproxy/version.py
+++ b/mitmproxy/version.py
@@ -4,7 +4,7 @@ PATHOD = "pathod " + VERSION
MITMPROXY = "mitmproxy " + VERSION
# Serialization format version. This is displayed nowhere, it just needs to be incremented by one
-# for each change the the file format.
+# for each change in the file format.
FLOW_FORMAT_VERSION = 5
if __name__ == "__main__":
diff --git a/pathod/language/actions.py b/pathod/language/actions.py
index e85affac..fc57a18b 100644
--- a/pathod/language/actions.py
+++ b/pathod/language/actions.py
@@ -2,9 +2,7 @@ import abc
import copy
import random
from functools import total_ordering
-
import pyparsing as pp
-
from . import base
@@ -52,7 +50,7 @@ class _Action(base.Token):
class PauseAt(_Action):
- unique_name = None
+ unique_name = None # type: ignore
def __init__(self, offset, seconds):
_Action.__init__(self, offset)
@@ -103,7 +101,7 @@ class DisconnectAt(_Action):
class InjectAt(_Action):
- unique_name = None
+ unique_name = None # type: ignore
def __init__(self, offset, value):
_Action.__init__(self, offset)
diff --git a/pathod/language/base.py b/pathod/language/base.py
index 3a810ef0..c8892748 100644
--- a/pathod/language/base.py
+++ b/pathod/language/base.py
@@ -3,10 +3,9 @@ import os
import abc
import functools
import pyparsing as pp
-
from mitmproxy.utils import strutils
from mitmproxy.utils import human
-
+import typing # noqa
from . import generators, exceptions
@@ -84,7 +83,7 @@ class Token:
return None
@property
- def unique_name(self):
+ def unique_name(self) -> typing.Optional[str]:
"""
Controls uniqueness constraints for tokens. No two tokens with the
same name will be allowed. If no uniquness should be applied, this
@@ -334,7 +333,7 @@ class OptionsOrValue(_Component):
Can be any of a specified set of options, or a value specifier.
"""
preamble = ""
- options = []
+ options = [] # type: typing.List[str]
def __init__(self, value):
# If it's a string, we were passed one of the options, so we lower-case
@@ -376,7 +375,7 @@ class OptionsOrValue(_Component):
class Integer(_Component):
- bounds = (None, None)
+ bounds = (None, None) # type: typing.Tuple[typing.Union[int, None], typing.Union[int , None]]
preamble = ""
def __init__(self, value):
@@ -442,7 +441,7 @@ class FixedLengthValue(Value):
A value component lead by an optional preamble.
"""
preamble = ""
- length = None
+ length = None # type: typing.Optional[int]
def __init__(self, value):
Value.__init__(self, value)
@@ -511,7 +510,7 @@ class IntField(_Component):
"""
An integer field, where values can optionally specified by name.
"""
- names = {}
+ names = {} # type: typing.Dict[str, int]
max = 16
preamble = ""
@@ -546,7 +545,7 @@ class NestedMessage(Token):
A nested message, as an escaped string with a preamble.
"""
preamble = ""
- nest_type = None
+ nest_type = None # type: ignore
def __init__(self, value):
Token.__init__(self)
diff --git a/pathod/language/http.py b/pathod/language/http.py
index 8fcf9edc..5cd717a9 100644
--- a/pathod/language/http.py
+++ b/pathod/language/http.py
@@ -54,7 +54,7 @@ class Method(base.OptionsOrValue):
class _HeaderMixin:
- unique_name = None
+ unique_name = None # type: ignore
def format_header(self, key, value):
return [key, b": ", value, b"\r\n"]
@@ -143,7 +143,7 @@ class _HTTPMessage(message.Message):
class Response(_HTTPMessage):
- unique_name = None
+ unique_name = None # type: ignore
comps = (
Header,
ShortcutContentType,
diff --git a/pathod/language/http2.py b/pathod/language/http2.py
index 08c5f6d7..47d6e370 100644
--- a/pathod/language/http2.py
+++ b/pathod/language/http2.py
@@ -1,9 +1,9 @@
import pyparsing as pp
-
from mitmproxy.net import http
from mitmproxy.net.http import user_agents, Headers
from . import base, message
+
"""
Normal HTTP requests:
<method>:<path>:<header>:<body>
@@ -41,7 +41,7 @@ def get_header(val, headers):
class _HeaderMixin:
- unique_name = None
+ unique_name = None # type: ignore
def values(self, settings):
return (
@@ -146,7 +146,7 @@ class Times(base.Integer):
class Response(_HTTP2Message):
- unique_name = None
+ unique_name = None # type: ignore
comps = (
Header,
Body,
diff --git a/pathod/language/message.py b/pathod/language/message.py
index 6cdaaa0b..6b4c5021 100644
--- a/pathod/language/message.py
+++ b/pathod/language/message.py
@@ -1,13 +1,14 @@
import abc
from . import actions, exceptions
from mitmproxy.utils import strutils
+import typing # noqa
LOG_TRUNCATE = 1024
class Message:
__metaclass__ = abc.ABCMeta
- logattrs = []
+ logattrs = [] # type: typing.List[str]
def __init__(self, tokens):
track = set([])
diff --git a/pathod/language/websockets.py b/pathod/language/websockets.py
index a237381c..b4faf59b 100644
--- a/pathod/language/websockets.py
+++ b/pathod/language/websockets.py
@@ -4,6 +4,7 @@ import mitmproxy.net.websockets
from mitmproxy.utils import strutils
import pyparsing as pp
from . import base, generators, actions, message
+import typing # noqa
NESTED_LEADER = b"pathod!"
@@ -20,7 +21,7 @@ class OpCode(base.IntField):
"close": mitmproxy.net.websockets.OPCODE.CLOSE,
"ping": mitmproxy.net.websockets.OPCODE.PING,
"pong": mitmproxy.net.websockets.OPCODE.PONG,
- }
+ } # type: typing.Dict[str, int]
max = 15
preamble = "c"
@@ -239,7 +240,14 @@ class NestedFrame(base.NestedMessage):
nest_type = WebsocketFrame
+COMP = typing.Tuple[
+ typing.Type[OpCode], typing.Type[Length], typing.Type[Fin], typing.Type[RSV1], typing.Type[RSV2], typing.Type[RSV3], typing.Type[Mask],
+ typing.Type[actions.PauseAt], typing.Type[actions.DisconnectAt], typing.Type[actions.InjectAt], typing.Type[KeyNone], typing.Type[Key],
+ typing.Type[Times], typing.Type[Body], typing.Type[RawBody]
+]
+
+
class WebsocketClientFrame(WebsocketFrame):
- components = COMPONENTS + (
+ components = typing.cast(COMP, COMPONENTS + (
NestedFrame,
- )
+ ))
diff --git a/pathod/pathod.py b/pathod/pathod.py
index 7416d325..7c773c3b 100644
--- a/pathod/pathod.py
+++ b/pathod/pathod.py
@@ -3,19 +3,17 @@ import logging
import os
import sys
import threading
-
from mitmproxy.net import tcp
from mitmproxy import certs as mcerts
from mitmproxy.net import websockets
from mitmproxy import version
-
import urllib
from mitmproxy import exceptions
-
from pathod import language
from pathod import utils
from pathod import log
from pathod import protocols
+import typing # noqa
DEFAULT_CERT_DOMAIN = b"pathod.net"
@@ -71,7 +69,7 @@ class SSLOptions:
class PathodHandler(tcp.BaseHandler):
wbufsize = 0
- sni = None
+ sni = None # type: typing.Union[str, None, bool]
def __init__(
self,
diff --git a/pathod/protocols/http2.py b/pathod/protocols/http2.py
index 7c88c5c7..cfc71650 100644
--- a/pathod/protocols/http2.py
+++ b/pathod/protocols/http2.py
@@ -247,13 +247,13 @@ class HTTP2StateProtocol:
raw_bytes = frm.serialize()
self.tcp_handler.wfile.write(raw_bytes)
self.tcp_handler.wfile.flush()
- if not hide and self.dump_frames: # pragma no cover
+ if not hide and self.dump_frames: # pragma: no cover
print(">> " + repr(frm))
def read_frame(self, hide=False):
while True:
frm = http2.parse_frame(*http2.read_raw_frame(self.tcp_handler.rfile))
- if not hide and self.dump_frames: # pragma no cover
+ if not hide and self.dump_frames: # pragma: no cover
print("<< " + repr(frm))
if isinstance(frm, hyperframe.frame.PingFrame):
@@ -337,7 +337,7 @@ class HTTP2StateProtocol:
if end_stream:
frms[0].flags.add('END_STREAM')
- if self.dump_frames: # pragma no cover
+ if self.dump_frames: # pragma: no cover
for frm in frms:
print(">> ", repr(frm))
@@ -355,7 +355,7 @@ class HTTP2StateProtocol:
data=body[i:i + chunk_size]) for i in chunks]
frms[-1].flags.add('END_STREAM')
- if self.dump_frames: # pragma no cover
+ if self.dump_frames: # pragma: no cover
for frm in frms:
print(">> ", repr(frm))
diff --git a/pathod/test.py b/pathod/test.py
index 81f5805f..52f3ba02 100644
--- a/pathod/test.py
+++ b/pathod/test.py
@@ -1,16 +1,16 @@
import io
import time
import queue
-
from . import pathod
from mitmproxy.types import basethread
+import typing # noqa
class Daemon:
IFACE = "127.0.0.1"
- def __init__(self, ssl=None, **daemonargs):
- self.q = queue.Queue()
+ def __init__(self, ssl=None, **daemonargs) -> None:
+ self.q = queue.Queue() # type: queue.Queue
self.logfp = io.StringIO()
daemonargs["logfp"] = self.logfp
self.thread = _PaThread(self.IFACE, self.q, ssl, daemonargs)
@@ -25,18 +25,18 @@ class Daemon:
def __enter__(self):
return self
- def __exit__(self, type, value, traceback):
+ def __exit__(self, type, value, traceback) -> bool:
self.logfp.truncate(0)
self.shutdown()
return False
- def p(self, spec):
+ def p(self, spec: str) -> str:
"""
Return a URL that will render the response in spec.
"""
return "%s/p/%s" % (self.urlbase, spec)
- def text_log(self):
+ def text_log(self) -> str:
return self.logfp.getvalue()
def wait_for_silence(self, timeout=5):
@@ -62,7 +62,7 @@ class Daemon:
return None
return l[-1]
- def log(self):
+ def log(self) -> typing.List[typing.Dict]:
"""
Return the log buffer as a list of dictionaries.
"""
diff --git a/pathod/utils.py b/pathod/utils.py
index 44ad1f87..11b1dccd 100644
--- a/pathod/utils.py
+++ b/pathod/utils.py
@@ -1,6 +1,7 @@
import os
import sys
from mitmproxy.utils import data as mdata
+import typing # noqa
class MemBool:
@@ -9,10 +10,10 @@ class MemBool:
Truth-checking with a memory, for use in chained if statements.
"""
- def __init__(self):
- self.v = None
+ def __init__(self) -> None:
+ self.v = None # type: typing.Optional[bool]
- def __call__(self, v):
+ def __call__(self, v: bool) -> bool:
self.v = v
return bool(v)
diff --git a/release/README.md b/release/README.md
index a30221c8..a60b7f98 100644
--- a/release/README.md
+++ b/release/README.md
@@ -8,7 +8,7 @@ Make sure run all these steps on the correct branch you want to create a new rel
- Wait for tag CI to complete
## GitHub Release
-- Create release notice on Github [https://github.com/mitmproxy/mitmproxy/releases/new](here)
+- Create release notice on Github [here](https://github.com/mitmproxy/mitmproxy/releases/new)
- Attach all files from the new release folder on https://snapshots.mitmproxy.org
## PyPi
@@ -20,6 +20,10 @@ Make sure run all these steps on the correct branch you want to create a new rel
- Update the dependencies in [alpine/requirements.txt](https://github.com/mitmproxy/docker-releases/commit/3d6a9989fde068ad0aea257823ac3d7986ff1613#diff-9b7e0eea8ae74688b1ac13ea080549ba)
* Creating a fresh venv, pip-installing the new wheel in there, and then export all packages:
* `virtualenv -ppython3.5 venv && source venv/bin/activate && pip install mitmproxy && pip freeze`
-- Update `latest` tag [https://hub.docker.com/r/mitmproxy/mitmproxy/~/settings/automated-builds/](here)
+ - Tag the commit with the correct version
+ * `2.0.0` for new major versions
+ * `2.0.2` for new patch versions
+ * `2.0` always points to the latest patch version of the `2.0.x` series (update tag + force push)
+- Update `latest` tag [here](https://hub.docker.com/r/mitmproxy/mitmproxy/~/settings/automated-builds/)
After everything is done, you might want to bump the version on master in [https://github.com/mitmproxy/mitmproxy/blob/master/mitmproxy/version.py](mitmproxy/version.py) if you just created a major release.
diff --git a/setup.cfg b/setup.cfg
index 7fbb7f73..1721975e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -21,23 +21,13 @@ exclude_lines =
[tool:full_coverage]
exclude =
- mitmproxy/contentviews/__init__.py
mitmproxy/contentviews/wbxml.py
- mitmproxy/contentviews/xml_html.py
- mitmproxy/net/tcp.py
- mitmproxy/net/http/cookies.py
- mitmproxy/net/http/encoding.py
- mitmproxy/net/http/message.py
- mitmproxy/net/http/url.py
mitmproxy/proxy/protocol/
mitmproxy/proxy/config.py
mitmproxy/proxy/root_context.py
mitmproxy/proxy/server.py
mitmproxy/tools/
- mitmproxy/controller.py
- mitmproxy/export.py
mitmproxy/flow.py
- mitmproxy/io_compat.py
mitmproxy/master.py
pathod/pathoc.py
pathod/pathod.py
@@ -46,19 +36,16 @@ exclude =
[tool:individual_coverage]
exclude =
- mitmproxy/addonmanager.py
mitmproxy/addons/onboardingapp/app.py
mitmproxy/addons/termlog.py
mitmproxy/contentviews/base.py
mitmproxy/contentviews/wbxml.py
- mitmproxy/contentviews/xml_html.py
mitmproxy/controller.py
mitmproxy/ctx.py
mitmproxy/exceptions.py
- mitmproxy/export.py
mitmproxy/flow.py
- mitmproxy/io.py
- mitmproxy/io_compat.py
+ mitmproxy/io/io.py
+ mitmproxy/io/tnetstring.py
mitmproxy/log.py
mitmproxy/master.py
mitmproxy/net/check.py
@@ -85,7 +72,6 @@ exclude =
mitmproxy/proxy/root_context.py
mitmproxy/proxy/server.py
mitmproxy/stateobject.py
- mitmproxy/types/multidict.py
mitmproxy/utils/bits.py
pathod/language/actions.py
pathod/language/base.py
diff --git a/setup.py b/setup.py
index ec28eded..a03d74fb 100644
--- a/setup.py
+++ b/setup.py
@@ -64,23 +64,23 @@ setup(
"click>=6.2, <7",
"certifi>=2015.11.20.1", # no semver here - this should always be on the last release!
"construct>=2.8, <2.9",
- "cryptography>=1.3, <1.9",
+ "cryptography>=1.4, <1.9",
"cssutils>=1.0.1, <1.1",
- "h2>=2.6.1, <3",
+ "h2>=3.0, <4",
"html2text>=2016.1.8, <=2016.9.19",
- "hyperframe>=4.0.2, <6",
+ "hyperframe>=5.0, <6",
"jsbeautifier>=1.6.3, <1.7",
- "kaitaistruct>=0.6, <0.7",
+ "kaitaistruct>=0.7, <0.8",
+ "ldap3>=2.2.0, <2.3",
"passlib>=1.6.5, <1.8",
"pyasn1>=0.1.9, <0.3",
- "pyOpenSSL>=16.0, <17.0",
+ "pyOpenSSL>=16.0, <17.1",
"pyparsing>=2.1.3, <2.3",
"pyperclip>=1.5.22, <1.6",
"requests>=2.9.1, <3",
- "ruamel.yaml>=0.13.2, <0.14",
- "tornado>=4.3, <4.5",
+ "ruamel.yaml>=0.13.2, <0.15",
+ "tornado>=4.3, <4.6",
"urwid>=1.3.1, <1.4",
- "watchdog>=0.8.3, <0.9",
"brotlipy>=0.5.1, <0.7",
"sortedcontainers>=1.5.4, <1.6",
# transitive from cryptography, we just blacklist here.
@@ -104,7 +104,7 @@ setup(
"pytest-timeout>=1.0.0, <2",
"pytest-xdist>=1.14, <2",
"pytest-faulthandler>=1.3.0, <2",
- "sphinx>=1.3.5, <1.6",
+ "sphinx>=1.3.5, <1.7",
"sphinx-autobuild>=0.5.2, <0.7",
"sphinxcontrib-documentedlist>=0.5.0, <0.7",
"sphinx_rtd_theme>=0.1.9, <0.3",
@@ -112,8 +112,8 @@ setup(
'contentviews': [
],
'examples': [
- "beautifulsoup4>=4.4.1, <4.6",
- "Pillow>=3.2, <4.1",
+ "beautifulsoup4>=4.4.1, <4.7",
+ "Pillow>=3.2, <4.2",
]
}
)
diff --git a/test/examples/__init__.py b/test/examples/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/examples/__init__.py
diff --git a/test/examples/test_examples.py b/test/examples/test_examples.py
new file mode 100644
index 00000000..4c1631ce
--- /dev/null
+++ b/test/examples/test_examples.py
@@ -0,0 +1,101 @@
+from mitmproxy import contentviews
+from mitmproxy.test import tflow
+from mitmproxy.test import tutils
+from mitmproxy.test import taddons
+from mitmproxy.net.http import Headers
+
+from ..mitmproxy import tservers
+
+example_dir = tutils.test_data.push("../examples")
+
+
+class TestScripts(tservers.MasterTest):
+ def test_add_header(self):
+ with taddons.context() as tctx:
+ a = tctx.script(example_dir.path("simple/add_header.py"))
+ f = tflow.tflow(resp=tutils.tresp())
+ a.response(f)
+ assert f.response.headers["newheader"] == "foo"
+
+ def test_custom_contentviews(self):
+ with taddons.context() as tctx:
+ tctx.script(example_dir.path("simple/custom_contentview.py"))
+ swapcase = contentviews.get("swapcase")
+ _, fmt = swapcase(b"<html>Test!</html>")
+ assert any(b'tEST!' in val[0][1] for val in fmt)
+
+ def test_iframe_injector(self):
+ with taddons.context() as tctx:
+ sc = tctx.script(example_dir.path("simple/modify_body_inject_iframe.py"))
+ tctx.configure(
+ sc,
+ iframe = "http://example.org/evil_iframe"
+ )
+ f = tflow.tflow(
+ resp=tutils.tresp(content=b"<html><body>mitmproxy</body></html>")
+ )
+ tctx.master.addons.invoke_addon(sc, "response", f)
+ content = f.response.content
+ assert b'iframe' in content and b'evil_iframe' in content
+
+ def test_modify_form(self):
+ with taddons.context() as tctx:
+ sc = tctx.script(example_dir.path("simple/modify_form.py"))
+
+ form_header = Headers(content_type="application/x-www-form-urlencoded")
+ f = tflow.tflow(req=tutils.treq(headers=form_header))
+ sc.request(f)
+
+ assert f.request.urlencoded_form["mitmproxy"] == "rocks"
+
+ f.request.headers["content-type"] = ""
+ sc.request(f)
+ assert list(f.request.urlencoded_form.items()) == [("foo", "bar")]
+
+ def test_modify_querystring(self):
+ with taddons.context() as tctx:
+ sc = tctx.script(example_dir.path("simple/modify_querystring.py"))
+ f = tflow.tflow(req=tutils.treq(path="/search?q=term"))
+
+ sc.request(f)
+ assert f.request.query["mitmproxy"] == "rocks"
+
+ f.request.path = "/"
+ sc.request(f)
+ assert f.request.query["mitmproxy"] == "rocks"
+
+ def test_redirect_requests(self):
+ with taddons.context() as tctx:
+ sc = tctx.script(example_dir.path("simple/redirect_requests.py"))
+ f = tflow.tflow(req=tutils.treq(host="example.org"))
+ sc.request(f)
+ assert f.request.host == "mitmproxy.org"
+
+ def test_send_reply_from_proxy(self):
+ with taddons.context() as tctx:
+ sc = tctx.script(example_dir.path("simple/send_reply_from_proxy.py"))
+ f = tflow.tflow(req=tutils.treq(host="example.com", port=80))
+ sc.request(f)
+ assert f.response.content == b"Hello World"
+
+ def test_dns_spoofing(self):
+ with taddons.context() as tctx:
+ sc = tctx.script(example_dir.path("complex/dns_spoofing.py"))
+
+ original_host = "example.com"
+
+ host_header = Headers(host=original_host)
+ f = tflow.tflow(req=tutils.treq(headers=host_header, port=80))
+
+ tctx.master.addons.invoke_addon(sc, "requestheaders", f)
+
+ # Rewrite by reverse proxy mode
+ f.request.scheme = "https"
+ f.request.port = 443
+
+ tctx.master.addons.invoke_addon(sc, "request", f)
+
+ assert f.request.scheme == "http"
+ assert f.request.port == 80
+
+ assert f.request.headers["Host"] == original_host
diff --git a/test/examples/test_har_dump.py b/test/examples/test_har_dump.py
new file mode 100644
index 00000000..11cd5c29
--- /dev/null
+++ b/test/examples/test_har_dump.py
@@ -0,0 +1,86 @@
+import json
+
+from mitmproxy.test import tflow
+from mitmproxy.test import tutils
+from mitmproxy.test import taddons
+from mitmproxy.net.http import cookies
+
+example_dir = tutils.test_data.push("../examples")
+
+
+class TestHARDump:
+ def flow(self, resp_content=b'message'):
+ times = dict(
+ timestamp_start=746203272,
+ timestamp_end=746203272,
+ )
+
+ # Create a dummy flow for testing
+ return tflow.tflow(
+ req=tutils.treq(method=b'GET', **times),
+ resp=tutils.tresp(content=resp_content, **times)
+ )
+
+ def test_simple(self, tmpdir):
+ with taddons.context() as tctx:
+ a = tctx.script(example_dir.path("complex/har_dump.py"))
+ path = str(tmpdir.join("somefile"))
+ tctx.configure(a, hardump=path)
+ tctx.invoke(a, "response", self.flow())
+ tctx.invoke(a, "done")
+ with open(path, "r") as inp:
+ har = json.load(inp)
+ assert len(har["log"]["entries"]) == 1
+
+ def test_base64(self, tmpdir):
+ with taddons.context() as tctx:
+ a = tctx.script(example_dir.path("complex/har_dump.py"))
+ path = str(tmpdir.join("somefile"))
+ tctx.configure(a, hardump=path)
+
+ tctx.invoke(
+ a, "response", self.flow(resp_content=b"foo" + b"\xFF" * 10)
+ )
+ tctx.invoke(a, "done")
+ with open(path, "r") as inp:
+ har = json.load(inp)
+ assert har["log"]["entries"][0]["response"]["content"]["encoding"] == "base64"
+
+ def test_format_cookies(self):
+ with taddons.context() as tctx:
+ a = tctx.script(example_dir.path("complex/har_dump.py"))
+
+ CA = cookies.CookieAttrs
+
+ f = a.format_cookies([("n", "v", CA([("k", "v")]))])[0]
+ assert f['name'] == "n"
+ assert f['value'] == "v"
+ assert not f['httpOnly']
+ assert not f['secure']
+
+ f = a.format_cookies([("n", "v", CA([("httponly", None), ("secure", None)]))])[0]
+ assert f['httpOnly']
+ assert f['secure']
+
+ f = a.format_cookies([("n", "v", CA([("expires", "Mon, 24-Aug-2037 00:00:00 GMT")]))])[0]
+ assert f['expires']
+
+ def test_binary(self, tmpdir):
+ with taddons.context() as tctx:
+ a = tctx.script(example_dir.path("complex/har_dump.py"))
+ path = str(tmpdir.join("somefile"))
+ tctx.configure(a, hardump=path)
+
+ f = self.flow()
+ f.request.method = "POST"
+ f.request.headers["content-type"] = "application/x-www-form-urlencoded"
+ f.request.content = b"foo=bar&baz=s%c3%bc%c3%9f"
+ f.response.headers["random-junk"] = bytes(range(256))
+ f.response.content = bytes(range(256))
+
+ tctx.invoke(a, "response", f)
+ tctx.invoke(a, "done")
+
+ with open(path, "r") as inp:
+ har = json.load(inp)
+ assert len(har["log"]["entries"]) == 1
diff --git a/test/mitmproxy/examples/test_xss_scanner.py b/test/examples/test_xss_scanner.py
index 14ee6902..14ee6902 100755..100644
--- a/test/mitmproxy/examples/test_xss_scanner.py
+++ b/test/examples/test_xss_scanner.py
diff --git a/test/full_coverage_plugin.py b/test/full_coverage_plugin.py
index d98c29d6..ec30a9f9 100644
--- a/test/full_coverage_plugin.py
+++ b/test/full_coverage_plugin.py
@@ -1,6 +1,7 @@
import os
import configparser
import pytest
+import sys
here = os.path.abspath(os.path.dirname(__file__))
@@ -59,6 +60,12 @@ def pytest_runtestloop(session):
if os.name == 'nt':
cov.exclude('pragma: windows no cover')
+ if sys.platform == 'darwin':
+ cov.exclude('pragma: osx no cover')
+
+ if os.environ.get("OPENSSL") == "old":
+ cov.exclude('pragma: openssl-old no cover')
+
yield
coverage_values = dict([(name, 0) for name in pytest.config.option.full_cov])
diff --git a/test/helper_tools/ab.exe b/test/helper_tools/ab.exe
deleted file mode 100644
index d68ed0f3..00000000
--- a/test/helper_tools/ab.exe
+++ /dev/null
Binary files differ
diff --git a/test/individual_coverage.py b/test/individual_coverage.py
index 35bcd27f..c975b4c8 100644
--- a/test/individual_coverage.py
+++ b/test/individual_coverage.py
@@ -26,20 +26,20 @@ def run_tests(src, test, fail):
if e == 0:
if fail:
- print("SUCCESS but should have FAILED:", src, "Please remove this file from setup.cfg tool:individual_coverage/exclude.")
+ print("UNEXPECTED SUCCESS:", src, "Please remove this file from setup.cfg tool:individual_coverage/exclude.")
e = 42
else:
- print("SUCCESS:", src)
+ print("SUCCESS: ", src)
else:
if fail:
- print("Ignoring fail:", src)
+ print("IGNORING FAIL: ", src)
e = 0
else:
cov = [l for l in stdout.getvalue().split("\n") if (src in l) or ("was never imported" in l)]
if len(cov) == 1:
- print("FAIL:", cov[0])
+ print("FAIL: ", cov[0])
else:
- print("FAIL:", src, test, stdout.getvalue(), stdout.getvalue())
+ print("FAIL: ", src, test, stdout.getvalue(), stdout.getvalue())
print(stderr.getvalue())
print(stdout.getvalue())
diff --git a/test/mitmproxy/addons/onboardingapp/__init__.py b/test/mitmproxy/addons/onboardingapp/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/mitmproxy/addons/onboardingapp/__init__.py
diff --git a/test/mitmproxy/addons/test_clientplayback.py b/test/mitmproxy/addons/test_clientplayback.py
index f71662f0..7ffda317 100644
--- a/test/mitmproxy/addons/test_clientplayback.py
+++ b/test/mitmproxy/addons/test_clientplayback.py
@@ -26,7 +26,7 @@ class TestClientPlayback:
with taddons.context() as tctx:
assert cp.count() == 0
f = tflow.tflow(resp=True)
- cp.load([f])
+ cp.start_replay([f])
assert cp.count() == 1
RP = "mitmproxy.proxy.protocol.http_replay.RequestReplayThread"
with mock.patch(RP) as rp:
@@ -44,13 +44,30 @@ class TestClientPlayback:
cp.tick()
assert cp.current_thread is None
+ cp.start_replay([f])
+ cp.stop_replay()
+ assert not cp.flows
+
+ def test_load_file(self, tmpdir):
+ cp = clientplayback.ClientPlayback()
+ with taddons.context():
+ fpath = str(tmpdir.join("flows"))
+ tdump(fpath, [tflow.tflow(resp=True)])
+ cp.load_file(fpath)
+ assert cp.flows
+ with pytest.raises(exceptions.CommandError):
+ cp.load_file("/nonexistent")
+
def test_configure(self, tmpdir):
cp = clientplayback.ClientPlayback()
with taddons.context() as tctx:
path = str(tmpdir.join("flows"))
tdump(path, [tflow.tflow()])
tctx.configure(cp, client_replay=[path])
+ cp.configured = False
tctx.configure(cp, client_replay=[])
+ cp.configured = False
tctx.configure(cp)
+ cp.configured = False
with pytest.raises(exceptions.OptionsError):
tctx.configure(cp, client_replay=["nonexistent"])
diff --git a/test/mitmproxy/addons/test_core.py b/test/mitmproxy/addons/test_core.py
new file mode 100644
index 00000000..c132d80a
--- /dev/null
+++ b/test/mitmproxy/addons/test_core.py
@@ -0,0 +1,165 @@
+from mitmproxy.addons import core
+from mitmproxy.test import taddons
+from mitmproxy.test import tflow
+from mitmproxy import exceptions
+import pytest
+
+
+def test_set():
+ sa = core.Core()
+ with taddons.context() as tctx:
+ tctx.master.addons.add(sa)
+
+ assert not tctx.master.options.anticomp
+ tctx.command(sa.set, "anticomp")
+ assert tctx.master.options.anticomp
+
+ with pytest.raises(exceptions.CommandError):
+ tctx.command(sa.set, "nonexistent")
+
+
+def test_resume():
+ sa = core.Core()
+ with taddons.context():
+ f = tflow.tflow()
+ assert not sa.resume([f])
+ f.intercept()
+ sa.resume([f])
+ assert not f.reply.state == "taken"
+
+
+def test_mark():
+ sa = core.Core()
+ with taddons.context():
+ f = tflow.tflow()
+ assert not f.marked
+ sa.mark([f], True)
+ assert f.marked
+
+ sa.mark_toggle([f])
+ assert not f.marked
+ sa.mark_toggle([f])
+ assert f.marked
+
+
+def test_kill():
+ sa = core.Core()
+ with taddons.context():
+ f = tflow.tflow()
+ f.intercept()
+ assert f.killable
+ sa.kill([f])
+ assert not f.killable
+
+
+def test_revert():
+ sa = core.Core()
+ with taddons.context():
+ f = tflow.tflow()
+ f.backup()
+ f.request.content = b"bar"
+ assert f.modified()
+ sa.revert([f])
+ assert not f.modified()
+
+
+def test_flow_set():
+ sa = core.Core()
+ with taddons.context():
+ f = tflow.tflow(resp=True)
+ assert sa.flow_set_options()
+
+ with pytest.raises(exceptions.CommandError):
+ sa.flow_set([f], "flibble", "post")
+
+ assert f.request.method != "post"
+ sa.flow_set([f], "method", "post")
+ assert f.request.method == "POST"
+
+ assert f.request.host != "testhost"
+ sa.flow_set([f], "host", "testhost")
+ assert f.request.host == "testhost"
+
+ assert f.request.path != "/test/path"
+ sa.flow_set([f], "path", "/test/path")
+ assert f.request.path == "/test/path"
+
+ assert f.request.url != "http://foo.com/bar"
+ sa.flow_set([f], "url", "http://foo.com/bar")
+ assert f.request.url == "http://foo.com/bar"
+ with pytest.raises(exceptions.CommandError):
+ sa.flow_set([f], "url", "oink")
+
+ assert f.response.status_code != 404
+ sa.flow_set([f], "status_code", "404")
+ assert f.response.status_code == 404
+ assert f.response.reason == "Not Found"
+ with pytest.raises(exceptions.CommandError):
+ sa.flow_set([f], "status_code", "oink")
+
+ assert f.response.reason != "foo"
+ sa.flow_set([f], "reason", "foo")
+ assert f.response.reason == "foo"
+
+
+def test_encoding():
+ sa = core.Core()
+ with taddons.context():
+ f = tflow.tflow()
+ assert sa.encode_options()
+ sa.encode([f], "request", "deflate")
+ assert f.request.headers["content-encoding"] == "deflate"
+
+ sa.encode([f], "request", "br")
+ assert f.request.headers["content-encoding"] == "deflate"
+
+ sa.decode([f], "request")
+ assert "content-encoding" not in f.request.headers
+
+ sa.encode([f], "request", "br")
+ assert f.request.headers["content-encoding"] == "br"
+
+ sa.encode_toggle([f], "request")
+ assert "content-encoding" not in f.request.headers
+ sa.encode_toggle([f], "request")
+ assert f.request.headers["content-encoding"] == "deflate"
+ sa.encode_toggle([f], "request")
+ assert "content-encoding" not in f.request.headers
+
+ with pytest.raises(exceptions.CommandError):
+ sa.encode([f], "request", "invalid")
+
+
+def test_options(tmpdir):
+ p = str(tmpdir.join("path"))
+ sa = core.Core()
+ with taddons.context() as tctx:
+ tctx.options.stickycookie = "foo"
+ assert tctx.options.stickycookie == "foo"
+ sa.options_reset()
+ assert tctx.options.stickycookie is None
+
+ tctx.options.stickycookie = "foo"
+ tctx.options.stickyauth = "bar"
+ sa.options_reset_one("stickycookie")
+ assert tctx.options.stickycookie is None
+ assert tctx.options.stickyauth == "bar"
+
+ with pytest.raises(exceptions.CommandError):
+ sa.options_reset_one("unknown")
+
+ sa.options_save(p)
+ with pytest.raises(exceptions.CommandError):
+ sa.options_save("/")
+
+ sa.options_reset()
+ assert tctx.options.stickyauth is None
+ sa.options_load(p)
+ assert tctx.options.stickyauth == "bar"
+
+ sa.options_load("/nonexistent")
+
+ with open(p, 'a') as f:
+ f.write("'''")
+ with pytest.raises(exceptions.CommandError):
+ sa.options_load(p)
diff --git a/test/mitmproxy/addons/test_core_option_validation.py b/test/mitmproxy/addons/test_core_option_validation.py
index 0bb2bb0d..6d6d5ba4 100644
--- a/test/mitmproxy/addons/test_core_option_validation.py
+++ b/test/mitmproxy/addons/test_core_option_validation.py
@@ -11,7 +11,7 @@ def test_simple():
with pytest.raises(exceptions.OptionsError):
tctx.configure(sa, body_size_limit = "invalid")
tctx.configure(sa, body_size_limit = "1m")
- assert tctx.options._processed["body_size_limit"]
+ assert tctx.master.options._processed["body_size_limit"]
with pytest.raises(exceptions.OptionsError, match="mutually exclusive"):
tctx.configure(
diff --git a/test/mitmproxy/addons/test_cut.py b/test/mitmproxy/addons/test_cut.py
new file mode 100644
index 00000000..e028331f
--- /dev/null
+++ b/test/mitmproxy/addons/test_cut.py
@@ -0,0 +1,178 @@
+
+from mitmproxy.addons import cut
+from mitmproxy.addons import view
+from mitmproxy import exceptions
+from mitmproxy import certs
+from mitmproxy.test import taddons
+from mitmproxy.test import tflow
+from mitmproxy.test import tutils
+import pytest
+from unittest import mock
+
+
+def test_extract():
+ tf = tflow.tflow(resp=True)
+ tests = [
+ ["q.method", "GET"],
+ ["q.scheme", "http"],
+ ["q.host", "address"],
+ ["q.port", "22"],
+ ["q.path", "/path"],
+ ["q.url", "http://address:22/path"],
+ ["q.text", "content"],
+ ["q.content", b"content"],
+ ["q.raw_content", b"content"],
+ ["q.header[header]", "qvalue"],
+
+ ["s.status_code", "200"],
+ ["s.reason", "OK"],
+ ["s.text", "message"],
+ ["s.content", b"message"],
+ ["s.raw_content", b"message"],
+ ["s.header[header-response]", "svalue"],
+
+ ["cc.address.port", "22"],
+ ["cc.address.host", "address"],
+ ["cc.tls_version", "TLSv1.2"],
+ ["cc.sni", "address"],
+ ["cc.ssl_established", "false"],
+
+ ["sc.address.port", "22"],
+ ["sc.address.host", "address"],
+ ["sc.ip_address.host", "192.168.0.1"],
+ ["sc.tls_version", "TLSv1.2"],
+ ["sc.sni", "address"],
+ ["sc.ssl_established", "false"],
+ ]
+ for t in tests:
+ ret = cut.extract(t[0], tf)
+ if ret != t[1]:
+ raise AssertionError("%s: Expected %s, got %s" % (t[0], t[1], ret))
+
+ with open(tutils.test_data.path("mitmproxy/net/data/text_cert"), "rb") as f:
+ d = f.read()
+ c1 = certs.SSLCert.from_pem(d)
+ tf.server_conn.cert = c1
+ assert "CERTIFICATE" in cut.extract("sc.cert", tf)
+
+
+def test_parse_cutspec():
+ tests = [
+ ("", None, True),
+ ("req.method", ("@all", ["req.method"]), False),
+ (
+ "req.method,req.host",
+ ("@all", ["req.method", "req.host"]),
+ False
+ ),
+ (
+ "req.method,req.host|~b foo",
+ ("~b foo", ["req.method", "req.host"]),
+ False
+ ),
+ (
+ "req.method,req.host|~b foo | ~b bar",
+ ("~b foo | ~b bar", ["req.method", "req.host"]),
+ False
+ ),
+ (
+ "req.method, req.host | ~b foo | ~b bar",
+ ("~b foo | ~b bar", ["req.method", "req.host"]),
+ False
+ ),
+ ]
+ for cutspec, output, err in tests:
+ try:
+ assert cut.parse_cutspec(cutspec) == output
+ except exceptions.CommandError:
+ if not err:
+ raise
+ else:
+ if err:
+ raise AssertionError("Expected error.")
+
+
+def test_headername():
+ with pytest.raises(exceptions.CommandError):
+ cut.headername("header[foo.")
+
+
+def qr(f):
+ with open(f, "rb") as fp:
+ return fp.read()
+
+
+def test_cut_clip():
+ v = view.View()
+ c = cut.Cut()
+ with taddons.context() as tctx:
+ tctx.master.addons.add(v, c)
+ v.add([tflow.tflow(resp=True)])
+
+ with mock.patch('pyperclip.copy') as pc:
+ tctx.command(c.clip, "q.method|@all")
+ assert pc.called
+
+ with mock.patch('pyperclip.copy') as pc:
+ tctx.command(c.clip, "q.content|@all")
+ assert pc.called
+
+ with mock.patch('pyperclip.copy') as pc:
+ tctx.command(c.clip, "q.method,q.content|@all")
+ assert pc.called
+
+
+def test_cut_file(tmpdir):
+ f = str(tmpdir.join("path"))
+ v = view.View()
+ c = cut.Cut()
+ with taddons.context() as tctx:
+ tctx.master.addons.add(v, c)
+
+ v.add([tflow.tflow(resp=True)])
+
+ tctx.command(c.save, "q.method|@all", f)
+ assert qr(f) == b"GET"
+ tctx.command(c.save, "q.content|@all", f)
+ assert qr(f) == b"content"
+ tctx.command(c.save, "q.content|@all", "+" + f)
+ assert qr(f) == b"content\ncontent"
+
+ v.add([tflow.tflow(resp=True)])
+ tctx.command(c.save, "q.method|@all", f)
+ assert qr(f).splitlines() == [b"GET", b"GET"]
+ tctx.command(c.save, "q.method,q.content|@all", f)
+ assert qr(f).splitlines() == [b"GET,content", b"GET,content"]
+
+
+def test_cut():
+ v = view.View()
+ c = cut.Cut()
+ with taddons.context() as tctx:
+ v.add([tflow.tflow(resp=True)])
+ tctx.master.addons.add(v, c)
+ assert c.cut("q.method|@all") == [["GET"]]
+ assert c.cut("q.scheme|@all") == [["http"]]
+ assert c.cut("q.host|@all") == [["address"]]
+ assert c.cut("q.port|@all") == [["22"]]
+ assert c.cut("q.path|@all") == [["/path"]]
+ assert c.cut("q.url|@all") == [["http://address:22/path"]]
+ assert c.cut("q.content|@all") == [[b"content"]]
+ assert c.cut("q.header[header]|@all") == [["qvalue"]]
+ assert c.cut("q.header[unknown]|@all") == [[""]]
+
+ assert c.cut("s.status_code|@all") == [["200"]]
+ assert c.cut("s.reason|@all") == [["OK"]]
+ assert c.cut("s.content|@all") == [[b"message"]]
+ assert c.cut("s.header[header-response]|@all") == [["svalue"]]
+ assert c.cut("moo") == [[""]]
+ with pytest.raises(exceptions.CommandError):
+ assert c.cut("__dict__") == [[""]]
+
+ v = view.View()
+ c = cut.Cut()
+ with taddons.context() as tctx:
+ tctx.master.addons.add(v, c)
+ v.add([tflow.ttcpflow()])
+ assert c.cut("q.method|@all") == [[""]]
+ assert c.cut("s.status|@all") == [[""]]
diff --git a/test/mitmproxy/addons/test_defaults.py b/test/mitmproxy/addons/test_defaults.py
deleted file mode 100644
index e20466f1..00000000
--- a/test/mitmproxy/addons/test_defaults.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from mitmproxy import addons
-
-
-def test_defaults():
- assert addons.default_addons()
diff --git a/test/mitmproxy/addons/test_dumper.py b/test/mitmproxy/addons/test_dumper.py
index fbcc4d16..d8aa593b 100644
--- a/test/mitmproxy/addons/test_dumper.py
+++ b/test/mitmproxy/addons/test_dumper.py
@@ -16,7 +16,7 @@ from mitmproxy import options
def test_configure():
d = dumper.Dumper()
with taddons.context(options=options.Options()) as ctx:
- ctx.configure(d, filtstr="~b foo")
+ ctx.configure(d, view_filter="~b foo")
assert d.filter
f = tflow.tflow(resp=True)
@@ -24,10 +24,10 @@ def test_configure():
f.response.content = b"foo"
assert d.match(f)
- ctx.configure(d, filtstr=None)
+ ctx.configure(d, view_filter=None)
assert not d.filter
with pytest.raises(exceptions.OptionsError):
- ctx.configure(d, filtstr="~~")
+ ctx.configure(d, view_filter="~~")
assert not d.filter
@@ -68,7 +68,6 @@ def test_simple():
ctx.configure(d, flow_detail=4)
flow = tflow.tflow()
flow.request = tutils.treq()
- flow.request.stickycookie = True
flow.client_conn = mock.MagicMock()
flow.client_conn.address[0] = "foo"
flow.response = tutils.tresp(content=None)
diff --git a/test/mitmproxy/addons/test_export.py b/test/mitmproxy/addons/test_export.py
new file mode 100644
index 00000000..233c62d5
--- /dev/null
+++ b/test/mitmproxy/addons/test_export.py
@@ -0,0 +1,109 @@
+import pytest
+import os
+
+from mitmproxy import exceptions
+from mitmproxy.addons import export # heh
+from mitmproxy.test import tflow
+from mitmproxy.test import tutils
+from mitmproxy.test import taddons
+from unittest import mock
+
+
+@pytest.fixture
+def get_request():
+ return tflow.tflow(
+ req=tutils.treq(
+ method=b'GET',
+ content=b'',
+ path=b"/path?a=foo&a=bar&b=baz"
+ )
+ )
+
+
+@pytest.fixture
+def post_request():
+ return tflow.tflow(
+ req=tutils.treq(
+ method=b'POST',
+ headers=(),
+ content=bytes(range(256))
+ )
+ )
+
+
+@pytest.fixture
+def patch_request():
+ return tflow.tflow(
+ req=tutils.treq(method=b'PATCH', path=b"/path?query=param")
+ )
+
+
+@pytest.fixture
+def tcp_flow():
+ return tflow.ttcpflow()
+
+
+class TestExportCurlCommand:
+ def test_get(self, get_request):
+ result = """curl -H 'header:qvalue' -H 'content-length:0' 'http://address:22/path?a=foo&a=bar&b=baz'"""
+ assert export.curl_command(get_request) == result
+
+ def test_post(self, post_request):
+ result = "curl -H 'content-length:256' -X POST 'http://address:22/path' --data-binary '{}'".format(
+ str(bytes(range(256)))[2:-1]
+ )
+ assert export.curl_command(post_request) == result
+
+ def test_patch(self, patch_request):
+ result = """curl -H 'header:qvalue' -H 'content-length:7' -X PATCH 'http://address:22/path?query=param' --data-binary 'content'"""
+ assert export.curl_command(patch_request) == result
+
+ def test_tcp(self, tcp_flow):
+ with pytest.raises(exceptions.CommandError):
+ export.curl_command(tcp_flow)
+
+
+class TestRaw:
+ def test_get(self, get_request):
+ assert b"header: qvalue" in export.raw(get_request)
+
+ def test_tcp(self, tcp_flow):
+ with pytest.raises(exceptions.CommandError):
+ export.raw(tcp_flow)
+
+
+def qr(f):
+ with open(f, "rb") as fp:
+ return fp.read()
+
+
+def test_export(tmpdir):
+ f = str(tmpdir.join("path"))
+ e = export.Export()
+ with taddons.context():
+ assert e.formats() == ["curl", "raw"]
+ with pytest.raises(exceptions.CommandError):
+ e.file("nonexistent", tflow.tflow(resp=True), f)
+
+ e.file("raw", tflow.tflow(resp=True), f)
+ assert qr(f)
+ os.unlink(f)
+
+ e.file("curl", tflow.tflow(resp=True), f)
+ assert qr(f)
+ os.unlink(f)
+
+
+def test_clip(tmpdir):
+ e = export.Export()
+ with taddons.context():
+ with pytest.raises(exceptions.CommandError):
+ e.clip("nonexistent", tflow.tflow(resp=True))
+
+ with mock.patch('pyperclip.copy') as pc:
+ e.clip("raw", tflow.tflow(resp=True))
+ assert pc.called
+
+ with mock.patch('pyperclip.copy') as pc:
+ e.clip("curl", tflow.tflow(resp=True))
+ assert pc.called
diff --git a/test/mitmproxy/addons/test_onboarding.py b/test/mitmproxy/addons/test_onboarding.py
index 63125c23..474e6c3c 100644
--- a/test/mitmproxy/addons/test_onboarding.py
+++ b/test/mitmproxy/addons/test_onboarding.py
@@ -1,4 +1,8 @@
+import pytest
+
from mitmproxy.addons import onboarding
+from mitmproxy.test import taddons
+from mitmproxy import options
from .. import tservers
@@ -7,10 +11,25 @@ class TestApp(tservers.HTTPProxyTest):
return [onboarding.Onboarding()]
def test_basic(self):
- assert self.app("/").status_code == 200
+ with taddons.context() as tctx:
+ tctx.configure(self.addons()[0])
+ assert self.app("/").status_code == 200
- def test_cert(self):
- for ext in ["pem", "p12"]:
+ @pytest.mark.parametrize("ext", ["pem", "p12"])
+ def test_cert(self, ext):
+ with taddons.context() as tctx:
+ tctx.configure(self.addons()[0])
resp = self.app("/cert/%s" % ext)
assert resp.status_code == 200
assert resp.content
+
+ @pytest.mark.parametrize("ext", ["pem", "p12"])
+ def test_head(self, ext):
+ with taddons.context() as tctx:
+ tctx.configure(self.addons()[0])
+ p = self.pathoc()
+ with p.connect():
+ resp = p.request("head:'http://%s/cert/%s'" % (options.APP_HOST, ext))
+ assert resp.status_code == 200
+ assert "Content-Length" in resp.headers
+ assert not resp.content
diff --git a/test/mitmproxy/addons/test_proxyauth.py b/test/mitmproxy/addons/test_proxyauth.py
index 513f3f08..40044bf0 100644
--- a/test/mitmproxy/addons/test_proxyauth.py
+++ b/test/mitmproxy/addons/test_proxyauth.py
@@ -2,6 +2,7 @@ import binascii
import pytest
+from unittest import mock
from mitmproxy import exceptions
from mitmproxy.addons import proxyauth
from mitmproxy.test import taddons
@@ -41,6 +42,22 @@ def test_configure():
ctx.configure(up, proxyauth=None)
assert not up.nonanonymous
+ with mock.patch('ldap3.Server', return_value="ldap://fake_server:389 - cleartext"):
+ with mock.patch('ldap3.Connection', return_value="test"):
+ ctx.configure(up, proxyauth="ldap:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com")
+ assert up.ldapserver
+ ctx.configure(up, proxyauth="ldaps:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com")
+ assert up.ldapserver
+
+ with pytest.raises(exceptions.OptionsError):
+ ctx.configure(up, proxyauth="ldap:test:test:test")
+
+ with pytest.raises(IndexError):
+ ctx.configure(up, proxyauth="ldap:fake_serveruid=?dc=example,dc=com:person")
+
+ with pytest.raises(exceptions.OptionsError):
+ ctx.configure(up, proxyauth="ldapssssssss:fake_server:dn:password:tree")
+
with pytest.raises(exceptions.OptionsError):
ctx.configure(
up,
@@ -66,11 +83,8 @@ def test_configure():
with pytest.raises(exceptions.OptionsError):
ctx.configure(up, proxyauth="any", mode="socks5")
- ctx.configure(up, mode="regular")
- assert up.mode == "regular"
-
-def test_check():
+def test_check(monkeypatch):
up = proxyauth.ProxyAuth()
with taddons.context() as ctx:
ctx.configure(up, proxyauth="any", mode="regular")
@@ -112,6 +126,22 @@ def test_check():
)
assert not up.check(f)
+ with mock.patch('ldap3.Server', return_value="ldap://fake_server:389 - cleartext"):
+ with mock.patch('ldap3.Connection', search="test"):
+ with mock.patch('ldap3.Connection.search', return_value="test"):
+ ctx.configure(
+ up,
+ proxyauth="ldap:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com"
+ )
+ f.request.headers["Proxy-Authorization"] = proxyauth.mkauth(
+ "test", "test"
+ )
+ assert up.check(f)
+ f.request.headers["Proxy-Authorization"] = proxyauth.mkauth(
+ "", ""
+ )
+ assert not up.check(f)
+
def test_authenticate():
up = proxyauth.ProxyAuth()
diff --git a/test/mitmproxy/addons/test_readfile.py b/test/mitmproxy/addons/test_readfile.py
index b30c147b..813aa10e 100644
--- a/test/mitmproxy/addons/test_readfile.py
+++ b/test/mitmproxy/addons/test_readfile.py
@@ -1,62 +1,104 @@
+import io
+from unittest import mock
+
+import pytest
+
+import mitmproxy.io
+from mitmproxy import exceptions
from mitmproxy.addons import readfile
from mitmproxy.test import taddons
from mitmproxy.test import tflow
-from mitmproxy import io
-from mitmproxy import exceptions
-from unittest import mock
-import pytest
+@pytest.fixture
+def data():
+ f = io.BytesIO()
+
+ w = mitmproxy.io.FlowWriter(f)
+ flows = [
+ tflow.tflow(resp=True),
+ tflow.tflow(err=True),
+ tflow.ttcpflow(),
+ tflow.ttcpflow(err=True)
+ ]
+ for flow in flows:
+ w.add(flow)
-def write_data(path, corrupt=False):
- with open(path, "wb") as tf:
- w = io.FlowWriter(tf)
- for i in range(3):
- f = tflow.tflow(resp=True)
- w.add(f)
- for i in range(3):
- f = tflow.tflow(err=True)
- w.add(f)
- f = tflow.ttcpflow()
- w.add(f)
- f = tflow.ttcpflow(err=True)
- w.add(f)
- if corrupt:
- tf.write(b"flibble")
-
-
-@mock.patch('mitmproxy.master.Master.load_flow')
-def test_configure(mck, tmpdir):
-
- rf = readfile.ReadFile()
- with taddons.context() as tctx:
- tf = str(tmpdir.join("tfile"))
- write_data(tf)
- tctx.configure(rf, rfile=str(tf))
- assert not mck.called
- rf.running()
- assert mck.called
-
- write_data(tf, corrupt=True)
- tctx.configure(rf, rfile=str(tf))
- with pytest.raises(exceptions.OptionsError):
+ f.seek(0)
+ return f
+
+
+@pytest.fixture
+def corrupt_data():
+ f = data()
+ f.seek(0, io.SEEK_END)
+ f.write(b"qibble")
+ f.seek(0)
+ return f
+
+
+class TestReadFile:
+ @mock.patch('mitmproxy.master.Master.load_flow')
+ def test_configure(self, mck, tmpdir, data, corrupt_data):
+ rf = readfile.ReadFile()
+ with taddons.context() as tctx:
+ tf = tmpdir.join("tfile")
+
+ tf.write(data.getvalue())
+ tctx.configure(rf, rfile=str(tf))
+ assert not mck.called
rf.running()
+ assert mck.called
+ tf.write(corrupt_data.getvalue())
+ tctx.configure(rf, rfile=str(tf))
+ with pytest.raises(exceptions.OptionsError):
+ rf.running()
-@mock.patch('mitmproxy.master.Master.load_flow')
-def test_corruption(mck, tmpdir):
+ @mock.patch('mitmproxy.master.Master.load_flow')
+ def test_corrupt(self, mck, corrupt_data):
+ rf = readfile.ReadFile()
+ with taddons.context() as tctx:
+ with pytest.raises(exceptions.FlowReadException):
+ rf.load_flows(io.BytesIO(b"qibble"))
+ assert not mck.called
+ assert len(tctx.master.logs) == 1
- rf = readfile.ReadFile()
- with taddons.context() as tctx:
- with pytest.raises(exceptions.FlowReadException):
- rf.load_flows_file("nonexistent")
- assert not mck.called
- assert len(tctx.master.logs) == 1
+ with pytest.raises(exceptions.FlowReadException):
+ rf.load_flows(corrupt_data)
+ assert mck.called
+ assert len(tctx.master.logs) == 2
+
+ def test_nonexisting_file(self):
+ rf = readfile.ReadFile()
+ with taddons.context() as tctx:
+ with pytest.raises(exceptions.FlowReadException):
+ rf.load_flows_from_path("nonexistent")
+ assert len(tctx.master.logs) == 1
+
+
+class TestReadFileStdin:
+ @mock.patch('mitmproxy.master.Master.load_flow')
+ @mock.patch('sys.stdin')
+ def test_stdin(self, stdin, load_flow, data, corrupt_data):
+ rf = readfile.ReadFileStdin()
+ with taddons.context() as tctx:
+ stdin.buffer = data
+ tctx.configure(rf, rfile="-")
+ assert not load_flow.called
+ rf.running()
+ assert load_flow.called
- tfc = str(tmpdir.join("tfile"))
- write_data(tfc, corrupt=True)
+ stdin.buffer = corrupt_data
+ tctx.configure(rf, rfile="-")
+ with pytest.raises(exceptions.OptionsError):
+ rf.running()
- with pytest.raises(exceptions.FlowReadException):
- rf.load_flows_file(tfc)
- assert mck.called
- assert len(tctx.master.logs) == 2
+ @mock.patch('mitmproxy.master.Master.load_flow')
+ def test_normal(self, load_flow, tmpdir, data):
+ rf = readfile.ReadFileStdin()
+ with taddons.context():
+ tfile = tmpdir.join("tfile")
+ tfile.write(data.getvalue())
+ rf.load_flows_from_path(str(tfile))
+ assert load_flow.called
diff --git a/test/mitmproxy/addons/test_readstdin.py b/test/mitmproxy/addons/test_readstdin.py
deleted file mode 100644
index 76b01f4f..00000000
--- a/test/mitmproxy/addons/test_readstdin.py
+++ /dev/null
@@ -1,53 +0,0 @@
-
-import io
-from mitmproxy.addons import readstdin
-from mitmproxy.test import taddons
-from mitmproxy.test import tflow
-import mitmproxy.io
-from unittest import mock
-
-
-def gen_data(corrupt=False):
- tf = io.BytesIO()
- w = mitmproxy.io.FlowWriter(tf)
- for i in range(3):
- f = tflow.tflow(resp=True)
- w.add(f)
- for i in range(3):
- f = tflow.tflow(err=True)
- w.add(f)
- f = tflow.ttcpflow()
- w.add(f)
- f = tflow.ttcpflow(err=True)
- w.add(f)
- if corrupt:
- tf.write(b"flibble")
- tf.seek(0)
- return tf
-
-
-class mStdin:
- def __init__(self, d):
- self.buffer = d
-
- def isatty(self):
- return False
-
-
-@mock.patch('mitmproxy.master.Master.load_flow')
-def test_read(m, tmpdir):
- rf = readstdin.ReadStdin()
- with taddons.context() as tctx:
- assert not m.called
- rf.running(stdin=mStdin(gen_data()))
- assert m.called
-
- rf.running(stdin=mStdin(None))
- assert tctx.master.logs
- tctx.master.clear()
-
- m.reset_mock()
- assert not m.called
- rf.running(stdin=mStdin(gen_data(corrupt=True)))
- assert m.called
- assert tctx.master.logs
diff --git a/test/mitmproxy/addons/test_save.py b/test/mitmproxy/addons/test_save.py
new file mode 100644
index 00000000..85c2a398
--- /dev/null
+++ b/test/mitmproxy/addons/test_save.py
@@ -0,0 +1,83 @@
+import pytest
+
+from mitmproxy.test import taddons
+from mitmproxy.test import tflow
+
+from mitmproxy import io
+from mitmproxy import exceptions
+from mitmproxy import options
+from mitmproxy.addons import save
+from mitmproxy.addons import view
+
+
+def test_configure(tmpdir):
+ sa = save.Save()
+ with taddons.context(options=options.Options()) as tctx:
+ with pytest.raises(exceptions.OptionsError):
+ tctx.configure(sa, save_stream_file=str(tmpdir))
+ with pytest.raises(Exception, match="Invalid filter"):
+ tctx.configure(
+ sa, save_stream_file=str(tmpdir.join("foo")), save_stream_filter="~~"
+ )
+ tctx.configure(sa, save_stream_filter="foo")
+ assert sa.filt
+ tctx.configure(sa, save_stream_filter=None)
+ assert not sa.filt
+
+
+def rd(p):
+ x = io.FlowReader(open(p, "rb"))
+ return list(x.stream())
+
+
+def test_tcp(tmpdir):
+ sa = save.Save()
+ with taddons.context() as tctx:
+ p = str(tmpdir.join("foo"))
+ tctx.configure(sa, save_stream_file=p)
+
+ tt = tflow.ttcpflow()
+ sa.tcp_start(tt)
+ sa.tcp_end(tt)
+ tctx.configure(sa, save_stream_file=None)
+ assert rd(p)
+
+
+def test_save_command(tmpdir):
+ sa = save.Save()
+ with taddons.context() as tctx:
+ p = str(tmpdir.join("foo"))
+ sa.save([tflow.tflow(resp=True)], p)
+ assert len(rd(p)) == 1
+ sa.save([tflow.tflow(resp=True)], p)
+ assert len(rd(p)) == 1
+ sa.save([tflow.tflow(resp=True)], "+" + p)
+ assert len(rd(p)) == 2
+
+ with pytest.raises(exceptions.CommandError):
+ sa.save([tflow.tflow(resp=True)], str(tmpdir))
+
+ v = view.View()
+ tctx.master.addons.add(v)
+ tctx.master.addons.add(sa)
+ tctx.master.commands.call_args("save.file", ["@shown", p])
+
+
+def test_simple(tmpdir):
+ sa = save.Save()
+ with taddons.context() as tctx:
+ p = str(tmpdir.join("foo"))
+
+ tctx.configure(sa, save_stream_file=p)
+
+ f = tflow.tflow(resp=True)
+ sa.request(f)
+ sa.response(f)
+ tctx.configure(sa, save_stream_file=None)
+ assert rd(p)[0].response
+
+ tctx.configure(sa, save_stream_file="+" + p)
+ f = tflow.tflow()
+ sa.request(f)
+ tctx.configure(sa, save_stream_file=None)
+ assert not rd(p)[1].response
diff --git a/test/mitmproxy/addons/test_script.py b/test/mitmproxy/addons/test_script.py
index 16827488..dd5349cb 100644
--- a/test/mitmproxy/addons/test_script.py
+++ b/test/mitmproxy/addons/test_script.py
@@ -1,155 +1,103 @@
import traceback
import sys
import time
-import re
-import watchdog.events
import pytest
+from unittest import mock
from mitmproxy.test import tflow
from mitmproxy.test import tutils
from mitmproxy.test import taddons
+from mitmproxy import addonmanager
from mitmproxy import exceptions
-from mitmproxy import options
-from mitmproxy import proxy
-from mitmproxy import master
-from mitmproxy import utils
from mitmproxy.addons import script
-from ...conftest import skip_not_windows
-
-def test_scriptenv():
+def test_load_script():
with taddons.context() as tctx:
- with script.scriptenv("path", []):
- raise SystemExit
- assert tctx.master.has_log("exited", "error")
-
- tctx.master.clear()
- with script.scriptenv("path", []):
- raise ValueError("fooo")
- assert tctx.master.has_log("fooo", "error")
-
-
-class Called:
- def __init__(self):
- self.called = False
-
- def __call__(self, *args, **kwargs):
- self.called = True
-
-
-def test_reloadhandler():
- rh = script.ReloadHandler(Called())
- assert not rh.filter(watchdog.events.DirCreatedEvent("path"))
- assert not rh.filter(watchdog.events.FileModifiedEvent("/foo/.bar"))
- assert not rh.filter(watchdog.events.FileModifiedEvent("/foo/bar"))
- assert rh.filter(watchdog.events.FileModifiedEvent("/foo/bar.py"))
-
- assert not rh.callback.called
- rh.on_modified(watchdog.events.FileModifiedEvent("/foo/bar"))
- assert not rh.callback.called
- rh.on_modified(watchdog.events.FileModifiedEvent("/foo/bar.py"))
- assert rh.callback.called
- rh.callback.called = False
-
- rh.on_created(watchdog.events.FileCreatedEvent("foo"))
- assert not rh.callback.called
- rh.on_created(watchdog.events.FileCreatedEvent("foo.py"))
- assert rh.callback.called
-
-
-class TestParseCommand:
- def test_empty_command(self):
- with pytest.raises(ValueError):
- script.parse_command("")
-
- with pytest.raises(ValueError):
- script.parse_command(" ")
-
- def test_no_script_file(self, tmpdir):
- with pytest.raises(Exception, match="not found"):
- script.parse_command("notfound")
-
- with pytest.raises(Exception, match="Not a file"):
- script.parse_command(str(tmpdir))
-
- def test_parse_args(self):
- with utils.chdir(tutils.test_data.dirname):
- assert script.parse_command(
- "mitmproxy/data/addonscripts/recorder.py"
- ) == ("mitmproxy/data/addonscripts/recorder.py", [])
- assert script.parse_command(
- "mitmproxy/data/addonscripts/recorder.py foo bar"
- ) == ("mitmproxy/data/addonscripts/recorder.py", ["foo", "bar"])
- assert script.parse_command(
- "mitmproxy/data/addonscripts/recorder.py 'foo bar'"
- ) == ("mitmproxy/data/addonscripts/recorder.py", ["foo bar"])
-
- @skip_not_windows
- def test_parse_windows(self):
- with utils.chdir(tutils.test_data.dirname):
- assert script.parse_command(
- "mitmproxy/data\\addonscripts\\recorder.py"
- ) == ("mitmproxy/data\\addonscripts\\recorder.py", [])
- assert script.parse_command(
- "mitmproxy/data\\addonscripts\\recorder.py 'foo \\ bar'"
- ) == ("mitmproxy/data\\addonscripts\\recorder.py", ['foo \\ bar'])
+ ns = script.load_script(
+ tctx.ctx(),
+ tutils.test_data.path(
+ "mitmproxy/data/addonscripts/recorder/recorder.py"
+ )
+ )
+ assert ns.addons
+ ns = script.load_script(
+ tctx.ctx(),
+ "nonexistent"
+ )
+ assert not ns
-def test_load_script():
- ns = script.load_script(
- tutils.test_data.path(
- "mitmproxy/data/addonscripts/recorder.py"
- ), []
- )
- assert ns.start
+
+def test_script_print_stdout():
+ with taddons.context() as tctx:
+ with mock.patch('mitmproxy.ctx.log.warn') as mock_warn:
+ with addonmanager.safecall():
+ ns = script.load_script(
+ tctx.ctx(),
+ tutils.test_data.path(
+ "mitmproxy/data/addonscripts/print.py"
+ )
+ )
+ ns.load(addonmanager.Loader(tctx.master))
+ mock_warn.assert_called_once_with("stdoutprint")
class TestScript:
- def test_simple(self):
+ def test_notfound(self):
with taddons.context():
+ with pytest.raises(exceptions.OptionsError):
+ script.Script("nonexistent")
+
+ def test_simple(self):
+ with taddons.context() as tctx:
sc = script.Script(
tutils.test_data.path(
- "mitmproxy/data/addonscripts/recorder.py"
+ "mitmproxy/data/addonscripts/recorder/recorder.py"
)
)
- sc.load_script()
- assert sc.ns.call_log[0][0:2] == ("solo", "start")
+ tctx.master.addons.add(sc)
+ tctx.configure(sc)
+ sc.tick()
+
+ rec = tctx.master.addons.get("recorder")
- sc.ns.call_log = []
+ assert rec.call_log[0][0:2] == ("recorder", "load")
+
+ rec.call_log = []
f = tflow.tflow(resp=True)
- sc.request(f)
+ tctx.master.addons.trigger("request", f)
- recf = sc.ns.call_log[0]
- assert recf[1] == "request"
+ assert rec.call_log[0][1] == "request"
def test_reload(self, tmpdir):
with taddons.context() as tctx:
f = tmpdir.join("foo.py")
f.ensure(file=True)
+ f.write("\n")
sc = script.Script(str(f))
tctx.configure(sc)
- for _ in range(100):
- f.write(".")
+ sc.tick()
+ for _ in range(3):
+ sc.last_load, sc.last_mtime = 0, 0
sc.tick()
time.sleep(0.1)
- if tctx.master.logs:
- return
- raise AssertionError("Change event not detected.")
+ tctx.master.has_log("Loading")
def test_exception(self):
with taddons.context() as tctx:
sc = script.Script(
tutils.test_data.path("mitmproxy/data/addonscripts/error.py")
)
- sc.start(tctx.options)
+ tctx.master.addons.add(sc)
+ tctx.configure(sc)
+ sc.tick()
+
f = tflow.tflow(resp=True)
- sc.request(f)
- assert tctx.master.logs[0].level == "error"
- assert len(tctx.master.logs[0].msg.splitlines()) == 6
- assert re.search(r'addonscripts[\\/]error.py", line \d+, in request', tctx.master.logs[0].msg)
- assert re.search(r'addonscripts[\\/]error.py", line \d+, in mkerr', tctx.master.logs[0].msg)
- assert tctx.master.logs[0].msg.endswith("ValueError: Error!\n")
+ tctx.master.addons.trigger("request", f)
+
+ tctx.master.has_log("ValueError: Error!")
+ tctx.master.has_log("error.py")
def test_addon(self):
with taddons.context() as tctx:
@@ -158,10 +106,11 @@ class TestScript:
"mitmproxy/data/addonscripts/addon.py"
)
)
- sc.start(tctx.options)
+ tctx.master.addons.add(sc)
tctx.configure(sc)
+ sc.tick()
assert sc.ns.event_log == [
- 'scriptstart', 'addonstart', 'addonconfigure'
+ 'scriptload', 'addonload', 'scriptconfigure', 'addonconfigure'
]
@@ -176,123 +125,131 @@ class TestCutTraceback:
self.raise_(4)
except RuntimeError:
tb = sys.exc_info()[2]
- tb_cut = script.cut_traceback(tb, "test_simple")
+ tb_cut = addonmanager.cut_traceback(tb, "test_simple")
assert len(traceback.extract_tb(tb_cut)) == 5
- tb_cut2 = script.cut_traceback(tb, "nonexistent")
+ tb_cut2 = addonmanager.cut_traceback(tb, "nonexistent")
assert len(traceback.extract_tb(tb_cut2)) == len(traceback.extract_tb(tb))
class TestScriptLoader:
- def test_run_once(self):
- o = options.Options(scripts=[])
- m = master.Master(o, proxy.DummyServer())
- sl = script.ScriptLoader()
- m.addons.add(sl)
-
- f = tflow.tflow(resp=True)
- with m.handlecontext():
- sc = sl.run_once(
- tutils.test_data.path(
- "mitmproxy/data/addonscripts/recorder.py"
- ), [f]
- )
- evts = [i[1] for i in sc.ns.call_log]
- assert evts == ['start', 'requestheaders', 'request', 'responseheaders', 'response', 'done']
-
- f = tflow.tflow(resp=True)
- with m.handlecontext():
- with pytest.raises(Exception, match="file not found"):
- sl.run_once("nonexistent", [f])
-
- def test_simple(self):
- o = options.Options(scripts=[])
- m = master.Master(o, proxy.DummyServer())
+ def test_script_run(self):
+ rp = tutils.test_data.path(
+ "mitmproxy/data/addonscripts/recorder/recorder.py"
+ )
sc = script.ScriptLoader()
- m.addons.add(sc)
- assert len(m.addons) == 1
- o.update(
- scripts = [
- tutils.test_data.path("mitmproxy/data/addonscripts/recorder.py")
+ with taddons.context() as tctx:
+ sc.script_run([tflow.tflow(resp=True)], rp)
+ debug = [i.msg for i in tctx.master.logs if i.level == "debug"]
+ assert debug == [
+ 'recorder load', 'recorder running', 'recorder configure',
+ 'recorder tick',
+ 'recorder requestheaders', 'recorder request',
+ 'recorder responseheaders', 'recorder response'
]
- )
- assert len(m.addons) == 2
- o.update(scripts = [])
- assert len(m.addons) == 1
- def test_dupes(self):
+ def test_script_run_nonexistent(self):
+ sc = script.ScriptLoader()
+ with taddons.context():
+ with pytest.raises(exceptions.CommandError):
+ sc.script_run([tflow.tflow(resp=True)], "/")
+
+ def test_simple(self):
sc = script.ScriptLoader()
with taddons.context() as tctx:
tctx.master.addons.add(sc)
- with pytest.raises(exceptions.OptionsError):
- tctx.configure(
- sc,
- scripts = ["one", "one"]
- )
+ sc.running()
+ assert len(tctx.master.addons) == 1
+ tctx.master.options.update(
+ scripts = [
+ tutils.test_data.path(
+ "mitmproxy/data/addonscripts/recorder/recorder.py"
+ )
+ ]
+ )
+ assert len(tctx.master.addons) == 1
+ assert len(sc.addons) == 1
+ tctx.master.options.update(scripts = [])
+ assert len(tctx.master.addons) == 1
+ assert len(sc.addons) == 0
- def test_nonexistent(self):
+ def test_dupes(self):
sc = script.ScriptLoader()
with taddons.context() as tctx:
tctx.master.addons.add(sc)
with pytest.raises(exceptions.OptionsError):
tctx.configure(
sc,
- scripts = ["nonexistent"]
+ scripts = ["one", "one"]
)
def test_order(self):
- rec = tutils.test_data.path("mitmproxy/data/addonscripts/recorder.py")
+ rec = tutils.test_data.path("mitmproxy/data/addonscripts/recorder")
sc = script.ScriptLoader()
+ sc.is_running = True
with taddons.context() as tctx:
- tctx.master.addons.add(sc)
- sc.running()
tctx.configure(
sc,
scripts = [
- "%s %s" % (rec, "a"),
- "%s %s" % (rec, "b"),
- "%s %s" % (rec, "c"),
+ "%s/a.py" % rec,
+ "%s/b.py" % rec,
+ "%s/c.py" % rec,
]
)
+ tctx.master.addons.invoke_addon(sc, "tick")
debug = [i.msg for i in tctx.master.logs if i.level == "debug"]
assert debug == [
- 'a start',
- 'a configure',
+ 'a load',
'a running',
+ 'a configure',
+ 'a tick',
- 'b start',
- 'b configure',
+ 'b load',
'b running',
+ 'b configure',
+ 'b tick',
- 'c start',
- 'c configure',
+ 'c load',
'c running',
+ 'c configure',
+ 'c tick',
]
+
tctx.master.logs = []
tctx.configure(
sc,
scripts = [
- "%s %s" % (rec, "c"),
- "%s %s" % (rec, "a"),
- "%s %s" % (rec, "b"),
+ "%s/c.py" % rec,
+ "%s/a.py" % rec,
+ "%s/b.py" % rec,
]
)
+
debug = [i.msg for i in tctx.master.logs if i.level == "debug"]
- assert debug == []
+ assert debug == [
+ 'c configure',
+ 'a configure',
+ 'b configure',
+ ]
tctx.master.logs = []
tctx.configure(
sc,
scripts = [
- "%s %s" % (rec, "x"),
- "%s %s" % (rec, "a"),
+ "%s/e.py" % rec,
+ "%s/a.py" % rec,
]
)
+ tctx.master.addons.invoke_addon(sc, "tick")
+
debug = [i.msg for i in tctx.master.logs if i.level == "debug"]
assert debug == [
'c done',
'b done',
- 'x start',
- 'x configure',
- 'x running',
+ 'a configure',
+ 'e load',
+ 'e running',
+ 'e configure',
+ 'e tick',
+ 'a tick',
]
diff --git a/test/mitmproxy/addons/test_serverplayback.py b/test/mitmproxy/addons/test_serverplayback.py
index 02642c35..3ceab3fa 100644
--- a/test/mitmproxy/addons/test_serverplayback.py
+++ b/test/mitmproxy/addons/test_serverplayback.py
@@ -6,7 +6,6 @@ from mitmproxy.test import tflow
import mitmproxy.test.tutils
from mitmproxy.addons import serverplayback
-from mitmproxy import options
from mitmproxy import exceptions
from mitmproxy import io
@@ -17,12 +16,24 @@ def tdump(path, flows):
w.add(i)
+def test_load_file(tmpdir):
+ s = serverplayback.ServerPlayback()
+ with taddons.context():
+ fpath = str(tmpdir.join("flows"))
+ tdump(fpath, [tflow.tflow(resp=True)])
+ s.load_file(fpath)
+ assert s.flowmap
+ with pytest.raises(exceptions.CommandError):
+ s.load_file("/nonexistent")
+
+
def test_config(tmpdir):
s = serverplayback.ServerPlayback()
with taddons.context() as tctx:
fpath = str(tmpdir.join("flows"))
tdump(fpath, [tflow.tflow(resp=True)])
tctx.configure(s, server_replay=[fpath])
+ s.configured = False
with pytest.raises(exceptions.OptionsError):
tctx.configure(s, server_replay=[str(tmpdir)])
@@ -39,86 +50,88 @@ def test_tick():
def test_server_playback():
sp = serverplayback.ServerPlayback()
- sp.configure(options.Options(), [])
- f = tflow.tflow(resp=True)
+ with taddons.context() as tctx:
+ tctx.configure(sp)
+ f = tflow.tflow(resp=True)
- assert not sp.flowmap
+ assert not sp.flowmap
- sp.load([f])
- assert sp.flowmap
- assert sp.next_flow(f)
- assert not sp.flowmap
+ sp.load_flows([f])
+ assert sp.flowmap
+ assert sp.next_flow(f)
+ assert not sp.flowmap
- sp.load([f])
- assert sp.flowmap
- sp.clear()
- assert not sp.flowmap
+ sp.load_flows([f])
+ assert sp.flowmap
+ sp.clear()
+ assert not sp.flowmap
def test_ignore_host():
sp = serverplayback.ServerPlayback()
- sp.configure(options.Options(server_replay_ignore_host=True), [])
+ with taddons.context() as tctx:
+ tctx.configure(sp, server_replay_ignore_host=True)
- r = tflow.tflow(resp=True)
- r2 = tflow.tflow(resp=True)
+ r = tflow.tflow(resp=True)
+ r2 = tflow.tflow(resp=True)
- r.request.host = "address"
- r2.request.host = "address"
- assert sp._hash(r) == sp._hash(r2)
- r2.request.host = "wrong_address"
- assert sp._hash(r) == sp._hash(r2)
+ r.request.host = "address"
+ r2.request.host = "address"
+ assert sp._hash(r) == sp._hash(r2)
+ r2.request.host = "wrong_address"
+ assert sp._hash(r) == sp._hash(r2)
def test_ignore_content():
s = serverplayback.ServerPlayback()
- s.configure(options.Options(server_replay_ignore_content=False), [])
+ with taddons.context() as tctx:
+ tctx.configure(s, server_replay_ignore_content=False)
- r = tflow.tflow(resp=True)
- r2 = tflow.tflow(resp=True)
+ r = tflow.tflow(resp=True)
+ r2 = tflow.tflow(resp=True)
- r.request.content = b"foo"
- r2.request.content = b"foo"
- assert s._hash(r) == s._hash(r2)
- r2.request.content = b"bar"
- assert not s._hash(r) == s._hash(r2)
+ r.request.content = b"foo"
+ r2.request.content = b"foo"
+ assert s._hash(r) == s._hash(r2)
+ r2.request.content = b"bar"
+ assert not s._hash(r) == s._hash(r2)
- s.configure(options.Options(server_replay_ignore_content=True), [])
- r = tflow.tflow(resp=True)
- r2 = tflow.tflow(resp=True)
- r.request.content = b"foo"
- r2.request.content = b"foo"
- assert s._hash(r) == s._hash(r2)
- r2.request.content = b"bar"
- assert s._hash(r) == s._hash(r2)
- r2.request.content = b""
- assert s._hash(r) == s._hash(r2)
- r2.request.content = None
- assert s._hash(r) == s._hash(r2)
+ tctx.configure(s, server_replay_ignore_content=True)
+ r = tflow.tflow(resp=True)
+ r2 = tflow.tflow(resp=True)
+ r.request.content = b"foo"
+ r2.request.content = b"foo"
+ assert s._hash(r) == s._hash(r2)
+ r2.request.content = b"bar"
+ assert s._hash(r) == s._hash(r2)
+ r2.request.content = b""
+ assert s._hash(r) == s._hash(r2)
+ r2.request.content = None
+ assert s._hash(r) == s._hash(r2)
def test_ignore_content_wins_over_params():
s = serverplayback.ServerPlayback()
- s.configure(
- options.Options(
+ with taddons.context() as tctx:
+ tctx.configure(
+ s,
server_replay_ignore_content=True,
server_replay_ignore_payload_params=[
"param1", "param2"
]
- ),
- []
- )
- # NOTE: parameters are mutually exclusive in options
+ )
- r = tflow.tflow(resp=True)
- r.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
- r.request.content = b"paramx=y"
+ # NOTE: parameters are mutually exclusive in options
+ r = tflow.tflow(resp=True)
+ r.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
+ r.request.content = b"paramx=y"
- r2 = tflow.tflow(resp=True)
- r2.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
- r2.request.content = b"paramx=x"
+ r2 = tflow.tflow(resp=True)
+ r2.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
+ r2.request.content = b"paramx=x"
- # same parameters
- assert s._hash(r) == s._hash(r2)
+ # same parameters
+ assert s._hash(r) == s._hash(r2)
def test_ignore_payload_params_other_content_type():
@@ -147,136 +160,139 @@ def test_ignore_payload_params_other_content_type():
def test_hash():
s = serverplayback.ServerPlayback()
- s.configure(options.Options(), [])
+ with taddons.context() as tctx:
+ tctx.configure(s)
- r = tflow.tflow()
- r2 = tflow.tflow()
+ r = tflow.tflow()
+ r2 = tflow.tflow()
- assert s._hash(r)
- assert s._hash(r) == s._hash(r2)
- r.request.headers["foo"] = "bar"
- assert s._hash(r) == s._hash(r2)
- r.request.path = "voing"
- assert s._hash(r) != s._hash(r2)
+ assert s._hash(r)
+ assert s._hash(r) == s._hash(r2)
+ r.request.headers["foo"] = "bar"
+ assert s._hash(r) == s._hash(r2)
+ r.request.path = "voing"
+ assert s._hash(r) != s._hash(r2)
- r.request.path = "path?blank_value"
- r2.request.path = "path?"
- assert s._hash(r) != s._hash(r2)
+ r.request.path = "path?blank_value"
+ r2.request.path = "path?"
+ assert s._hash(r) != s._hash(r2)
def test_headers():
s = serverplayback.ServerPlayback()
- s.configure(options.Options(server_replay_use_headers=["foo"]), [])
+ with taddons.context() as tctx:
+ tctx.configure(s, server_replay_use_headers=["foo"])
- r = tflow.tflow(resp=True)
- r.request.headers["foo"] = "bar"
- r2 = tflow.tflow(resp=True)
- assert not s._hash(r) == s._hash(r2)
- r2.request.headers["foo"] = "bar"
- assert s._hash(r) == s._hash(r2)
- r2.request.headers["oink"] = "bar"
- assert s._hash(r) == s._hash(r2)
+ r = tflow.tflow(resp=True)
+ r.request.headers["foo"] = "bar"
+ r2 = tflow.tflow(resp=True)
+ assert not s._hash(r) == s._hash(r2)
+ r2.request.headers["foo"] = "bar"
+ assert s._hash(r) == s._hash(r2)
+ r2.request.headers["oink"] = "bar"
+ assert s._hash(r) == s._hash(r2)
- r = tflow.tflow(resp=True)
- r2 = tflow.tflow(resp=True)
- assert s._hash(r) == s._hash(r2)
+ r = tflow.tflow(resp=True)
+ r2 = tflow.tflow(resp=True)
+ assert s._hash(r) == s._hash(r2)
def test_load():
s = serverplayback.ServerPlayback()
- s.configure(options.Options(), [])
+ with taddons.context() as tctx:
+ tctx.configure(s)
- r = tflow.tflow(resp=True)
- r.request.headers["key"] = "one"
+ r = tflow.tflow(resp=True)
+ r.request.headers["key"] = "one"
- r2 = tflow.tflow(resp=True)
- r2.request.headers["key"] = "two"
+ r2 = tflow.tflow(resp=True)
+ r2.request.headers["key"] = "two"
- s.load([r, r2])
+ s.load_flows([r, r2])
- assert s.count() == 2
+ assert s.count() == 2
- n = s.next_flow(r)
- assert n.request.headers["key"] == "one"
- assert s.count() == 1
+ n = s.next_flow(r)
+ assert n.request.headers["key"] == "one"
+ assert s.count() == 1
- n = s.next_flow(r)
- assert n.request.headers["key"] == "two"
- assert not s.flowmap
- assert s.count() == 0
+ n = s.next_flow(r)
+ assert n.request.headers["key"] == "two"
+ assert not s.flowmap
+ assert s.count() == 0
- assert not s.next_flow(r)
+ assert not s.next_flow(r)
def test_load_with_server_replay_nopop():
s = serverplayback.ServerPlayback()
- s.configure(options.Options(server_replay_nopop=True), [])
+ with taddons.context() as tctx:
+ tctx.configure(s, server_replay_nopop=True)
- r = tflow.tflow(resp=True)
- r.request.headers["key"] = "one"
+ r = tflow.tflow(resp=True)
+ r.request.headers["key"] = "one"
- r2 = tflow.tflow(resp=True)
- r2.request.headers["key"] = "two"
+ r2 = tflow.tflow(resp=True)
+ r2.request.headers["key"] = "two"
- s.load([r, r2])
+ s.load_flows([r, r2])
- assert s.count() == 2
- s.next_flow(r)
- assert s.count() == 2
+ assert s.count() == 2
+ s.next_flow(r)
+ assert s.count() == 2
def test_ignore_params():
s = serverplayback.ServerPlayback()
- s.configure(
- options.Options(
+ with taddons.context() as tctx:
+ tctx.configure(
+ s,
server_replay_ignore_params=["param1", "param2"]
- ),
- []
- )
+ )
- r = tflow.tflow(resp=True)
- r.request.path = "/test?param1=1"
- r2 = tflow.tflow(resp=True)
- r2.request.path = "/test"
- assert s._hash(r) == s._hash(r2)
- r2.request.path = "/test?param1=2"
- assert s._hash(r) == s._hash(r2)
- r2.request.path = "/test?param2=1"
- assert s._hash(r) == s._hash(r2)
- r2.request.path = "/test?param3=2"
- assert not s._hash(r) == s._hash(r2)
+ r = tflow.tflow(resp=True)
+ r.request.path = "/test?param1=1"
+ r2 = tflow.tflow(resp=True)
+ r2.request.path = "/test"
+ assert s._hash(r) == s._hash(r2)
+ r2.request.path = "/test?param1=2"
+ assert s._hash(r) == s._hash(r2)
+ r2.request.path = "/test?param2=1"
+ assert s._hash(r) == s._hash(r2)
+ r2.request.path = "/test?param3=2"
+ assert not s._hash(r) == s._hash(r2)
def thash(r, r2, setter):
s = serverplayback.ServerPlayback()
- s.configure(
- options.Options(
+ with taddons.context() as tctx:
+ s = serverplayback.ServerPlayback()
+ tctx.configure(
+ s,
server_replay_ignore_payload_params=["param1", "param2"]
- ),
- []
- )
-
- setter(r, paramx="x", param1="1")
-
- setter(r2, paramx="x", param1="1")
- # same parameters
- assert s._hash(r) == s._hash(r2)
- # ignored parameters !=
- setter(r2, paramx="x", param1="2")
- assert s._hash(r) == s._hash(r2)
- # missing parameter
- setter(r2, paramx="x")
- assert s._hash(r) == s._hash(r2)
- # ignorable parameter added
- setter(r2, paramx="x", param1="2")
- assert s._hash(r) == s._hash(r2)
- # not ignorable parameter changed
- setter(r2, paramx="y", param1="1")
- assert not s._hash(r) == s._hash(r2)
- # not ignorable parameter missing
- setter(r2, param1="1")
- r2.request.content = b"param1=1"
- assert not s._hash(r) == s._hash(r2)
+ )
+
+ setter(r, paramx="x", param1="1")
+
+ setter(r2, paramx="x", param1="1")
+ # same parameters
+ assert s._hash(r) == s._hash(r2)
+ # ignored parameters !=
+ setter(r2, paramx="x", param1="2")
+ assert s._hash(r) == s._hash(r2)
+ # missing parameter
+ setter(r2, paramx="x")
+ assert s._hash(r) == s._hash(r2)
+ # ignorable parameter added
+ setter(r2, paramx="x", param1="2")
+ assert s._hash(r) == s._hash(r2)
+ # not ignorable parameter changed
+ setter(r2, paramx="y", param1="1")
+ assert not s._hash(r) == s._hash(r2)
+ # not ignorable parameter missing
+ setter(r2, param1="1")
+ r2.request.content = b"param1=1"
+ assert not s._hash(r) == s._hash(r2)
def test_ignore_payload_params():
@@ -319,7 +335,7 @@ def test_server_playback_full():
f = tflow.tflow()
f.response = mitmproxy.test.tutils.tresp(content=f.request.content)
- s.load([f, f])
+ s.load_flows([f, f])
tf = tflow.tflow()
assert not tf.response
@@ -352,7 +368,7 @@ def test_server_playback_kill():
f = tflow.tflow()
f.response = mitmproxy.test.tutils.tresp(content=f.request.content)
- s.load([f])
+ s.load_flows([f])
f = tflow.tflow()
f.request.host = "nonexistent"
diff --git a/test/mitmproxy/addons/test_stickycookie.py b/test/mitmproxy/addons/test_stickycookie.py
index 9092e09b..f77d019d 100644
--- a/test/mitmproxy/addons/test_stickycookie.py
+++ b/test/mitmproxy/addons/test_stickycookie.py
@@ -110,8 +110,8 @@ class TestStickyCookie:
f.response.headers["Set-Cookie"] = c2
sc.response(f)
googlekey = list(sc.jar.keys())[0]
- assert len(sc.jar[googlekey].keys()) == 1
- assert list(sc.jar[googlekey]["somecookie"].items())[0][1] == "newvalue"
+ assert len(sc.jar[googlekey]) == 1
+ assert sc.jar[googlekey]["somecookie"] == "newvalue"
def test_response_delete(self):
sc = stickycookie.StickyCookie()
diff --git a/test/mitmproxy/addons/test_streamfile.py b/test/mitmproxy/addons/test_streamfile.py
deleted file mode 100644
index 3f78521c..00000000
--- a/test/mitmproxy/addons/test_streamfile.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import pytest
-
-from mitmproxy.test import taddons
-from mitmproxy.test import tflow
-
-from mitmproxy import io
-from mitmproxy import exceptions
-from mitmproxy import options
-from mitmproxy.addons import streamfile
-
-
-def test_configure(tmpdir):
- sa = streamfile.StreamFile()
- with taddons.context(options=options.Options()) as tctx:
- with pytest.raises(exceptions.OptionsError):
- tctx.configure(sa, streamfile=str(tmpdir))
- with pytest.raises(Exception, match="Invalid filter"):
- tctx.configure(sa, streamfile=str(tmpdir.join("foo")), filtstr="~~")
- tctx.configure(sa, filtstr="foo")
- assert sa.filt
- tctx.configure(sa, filtstr=None)
- assert not sa.filt
-
-
-def rd(p):
- x = io.FlowReader(open(p, "rb"))
- return list(x.stream())
-
-
-def test_tcp(tmpdir):
- sa = streamfile.StreamFile()
- with taddons.context() as tctx:
- p = str(tmpdir.join("foo"))
- tctx.configure(sa, streamfile=p)
-
- tt = tflow.ttcpflow()
- sa.tcp_start(tt)
- sa.tcp_end(tt)
- tctx.configure(sa, streamfile=None)
- assert rd(p)
-
-
-def test_simple(tmpdir):
- sa = streamfile.StreamFile()
- with taddons.context() as tctx:
- p = str(tmpdir.join("foo"))
-
- tctx.configure(sa, streamfile=p)
-
- f = tflow.tflow(resp=True)
- sa.request(f)
- sa.response(f)
- tctx.configure(sa, streamfile=None)
- assert rd(p)[0].response
-
- tctx.configure(sa, streamfile="+" + p)
- f = tflow.tflow()
- sa.request(f)
- tctx.configure(sa, streamfile=None)
- assert not rd(p)[1].response
diff --git a/test/mitmproxy/addons/test_termstatus.py b/test/mitmproxy/addons/test_termstatus.py
index 7becc857..2debaff5 100644
--- a/test/mitmproxy/addons/test_termstatus.py
+++ b/test/mitmproxy/addons/test_termstatus.py
@@ -5,6 +5,7 @@ from mitmproxy.test import taddons
def test_configure():
ts = termstatus.TermStatus()
with taddons.context() as ctx:
+ ctx.configure(ts, server=False)
ts.running()
assert not ctx.master.logs
ctx.configure(ts, server=True)
diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py
index b7842314..6da13650 100644
--- a/test/mitmproxy/addons/test_view.py
+++ b/test/mitmproxy/addons/test_view.py
@@ -4,7 +4,8 @@ from mitmproxy.test import tflow
from mitmproxy.addons import view
from mitmproxy import flowfilter
-from mitmproxy import options
+from mitmproxy import exceptions
+from mitmproxy import io
from mitmproxy.test import taddons
@@ -25,12 +26,12 @@ def test_order_refresh():
v.sig_view_refresh.connect(save)
tf = tflow.tflow(resp=True)
- with taddons.context(options=options.Options()) as tctx:
+ with taddons.context() as tctx:
tctx.configure(v, console_order="time")
- v.add(tf)
+ v.add([tf])
tf.request.timestamp_start = 1
assert not sargs
- v.update(tf)
+ v.update([tf])
assert sargs
@@ -130,9 +131,154 @@ def test_filter():
assert len(v) == 4
+def tdump(path, flows):
+ w = io.FlowWriter(open(path, "wb"))
+ for i in flows:
+ w.add(i)
+
+
+def test_create():
+ v = view.View()
+ with taddons.context():
+ v.create("get", "http://foo.com")
+ assert len(v) == 1
+ assert v[0].request.url == "http://foo.com/"
+ v.create("get", "http://foo.com")
+ assert len(v) == 2
+
+
+def test_orders():
+ v = view.View()
+ with taddons.context():
+ assert v.order_options()
+
+
+def test_load(tmpdir):
+ path = str(tmpdir.join("path"))
+ v = view.View()
+ with taddons.context() as tctx:
+ tctx.master.addons.add(v)
+ tdump(
+ path,
+ [
+ tflow.tflow(resp=True),
+ tflow.tflow(resp=True)
+ ]
+ )
+ v.load_file(path)
+ assert len(v) == 2
+ v.load_file(path)
+ assert len(v) == 4
+
+
+def test_resolve():
+ v = view.View()
+ with taddons.context() as tctx:
+ assert tctx.command(v.resolve, "@all") == []
+ assert tctx.command(v.resolve, "@focus") == []
+ assert tctx.command(v.resolve, "@shown") == []
+ assert tctx.command(v.resolve, "@hidden") == []
+ assert tctx.command(v.resolve, "@marked") == []
+ assert tctx.command(v.resolve, "@unmarked") == []
+ assert tctx.command(v.resolve, "~m get") == []
+ v.request(tft(method="get"))
+ assert len(tctx.command(v.resolve, "~m get")) == 1
+ assert len(tctx.command(v.resolve, "@focus")) == 1
+ assert len(tctx.command(v.resolve, "@all")) == 1
+ assert len(tctx.command(v.resolve, "@shown")) == 1
+ assert len(tctx.command(v.resolve, "@unmarked")) == 1
+ assert tctx.command(v.resolve, "@hidden") == []
+ assert tctx.command(v.resolve, "@marked") == []
+ v.request(tft(method="put"))
+ assert len(tctx.command(v.resolve, "@focus")) == 1
+ assert len(tctx.command(v.resolve, "@shown")) == 2
+ assert len(tctx.command(v.resolve, "@all")) == 2
+ assert tctx.command(v.resolve, "@hidden") == []
+ assert tctx.command(v.resolve, "@marked") == []
+
+ v.request(tft(method="get"))
+ v.request(tft(method="put"))
+
+ f = flowfilter.parse("~m get")
+ v.set_filter(f)
+ v[0].marked = True
+
+ def m(l):
+ return [i.request.method for i in l]
+
+ assert m(tctx.command(v.resolve, "~m get")) == ["GET", "GET"]
+ assert m(tctx.command(v.resolve, "~m put")) == ["PUT", "PUT"]
+ assert m(tctx.command(v.resolve, "@shown")) == ["GET", "GET"]
+ assert m(tctx.command(v.resolve, "@hidden")) == ["PUT", "PUT"]
+ assert m(tctx.command(v.resolve, "@marked")) == ["GET"]
+ assert m(tctx.command(v.resolve, "@unmarked")) == ["PUT", "GET", "PUT"]
+ assert m(tctx.command(v.resolve, "@all")) == ["GET", "PUT", "GET", "PUT"]
+
+ with pytest.raises(exceptions.CommandError, match="Invalid flow filter"):
+ tctx.command(v.resolve, "~")
+
+
+def test_movement():
+ v = view.View()
+ with taddons.context():
+ v.go(0)
+ v.add([
+ tflow.tflow(),
+ tflow.tflow(),
+ tflow.tflow(),
+ tflow.tflow(),
+ tflow.tflow(),
+ ])
+ assert v.focus.index == 0
+ v.go(-1)
+ assert v.focus.index == 4
+ v.go(0)
+ assert v.focus.index == 0
+ v.go(1)
+ assert v.focus.index == 1
+ v.go(999)
+ assert v.focus.index == 4
+ v.go(-999)
+ assert v.focus.index == 0
+
+ v.focus_next()
+ assert v.focus.index == 1
+ v.focus_prev()
+ assert v.focus.index == 0
+
+
+def test_duplicate():
+ v = view.View()
+ with taddons.context():
+ f = [
+ tflow.tflow(),
+ tflow.tflow(),
+ ]
+ v.add(f)
+ assert len(v) == 2
+ v.duplicate(f)
+ assert len(v) == 4
+ assert v.focus.index == 2
+
+
+def test_setgetval():
+ v = view.View()
+ with taddons.context():
+ f = tflow.tflow()
+ v.add([f])
+ v.setvalue([f], "key", "value")
+ assert v.getvalue(f, "key", "default") == "value"
+ assert v.getvalue(f, "unknow", "default") == "default"
+
+ v.setvalue_toggle([f], "key")
+ assert v.getvalue(f, "key", "default") == "true"
+ v.setvalue_toggle([f], "key")
+ assert v.getvalue(f, "key", "default") == "false"
+
+
def test_order():
v = view.View()
- with taddons.context(options=options.Options()) as tctx:
+ with taddons.context() as tctx:
v.request(tft(method="get", start=1))
v.request(tft(method="put", start=2))
v.request(tft(method="get", start=3))
@@ -180,14 +326,14 @@ def test_update():
assert f in v
f.request.method = "put"
- v.update(f)
+ v.update([f])
assert f not in v
f.request.method = "get"
- v.update(f)
+ v.update([f])
assert f in v
- v.update(f)
+ v.update([f])
assert f in v
@@ -226,7 +372,7 @@ def test_signals():
assert not any([rec_add, rec_update, rec_remove, rec_refresh])
# Simple add
- v.add(tft())
+ v.add([tft()])
assert rec_add
assert not any([rec_update, rec_remove, rec_refresh])
@@ -241,14 +387,14 @@ def test_signals():
# An update that results in a flow being added to the view
clearrec()
v[0].request.method = "PUT"
- v.update(v[0])
+ v.update([v[0]])
assert rec_remove
assert not any([rec_update, rec_refresh, rec_add])
# An update that does not affect the view just sends update
v.set_filter(flowfilter.parse("~m put"))
clearrec()
- v.update(v[0])
+ v.update([v[0]])
assert rec_update
assert not any([rec_remove, rec_refresh, rec_add])
@@ -257,33 +403,33 @@ def test_signals():
v.set_filter(flowfilter.parse("~m get"))
assert not len(v)
clearrec()
- v.update(f)
+ v.update([f])
assert not any([rec_add, rec_update, rec_remove, rec_refresh])
def test_focus_follow():
v = view.View()
- with taddons.context(options=options.Options()) as tctx:
- tctx.configure(v, console_focus_follow=True, filter="~m get")
+ with taddons.context() as tctx:
+ tctx.configure(v, console_focus_follow=True, view_filter="~m get")
- v.add(tft(start=5))
+ v.add([tft(start=5)])
assert v.focus.index == 0
- v.add(tft(start=4))
+ v.add([tft(start=4)])
assert v.focus.index == 0
assert v.focus.flow.request.timestamp_start == 4
- v.add(tft(start=7))
+ v.add([tft(start=7)])
assert v.focus.index == 2
assert v.focus.flow.request.timestamp_start == 7
mod = tft(method="put", start=6)
- v.add(mod)
+ v.add([mod])
assert v.focus.index == 2
assert v.focus.flow.request.timestamp_start == 7
mod.request.method = "GET"
- v.update(mod)
+ v.update([mod])
assert v.focus.index == 2
assert v.focus.flow.request.timestamp_start == 6
@@ -291,7 +437,7 @@ def test_focus_follow():
def test_focus():
# Special case - initialising with a view that already contains data
v = view.View()
- v.add(tft())
+ v.add([tft()])
f = view.Focus(v)
assert f.index is 0
assert f.flow is v[0]
@@ -302,7 +448,7 @@ def test_focus():
assert f.index is None
assert f.flow is None
- v.add(tft(start=1))
+ v.add([tft(start=1)])
assert f.index == 0
assert f.flow is v[0]
@@ -312,11 +458,11 @@ def test_focus():
with pytest.raises(ValueError):
f.__setattr__("index", 99)
- v.add(tft(start=0))
+ v.add([tft(start=0)])
assert f.index == 1
assert f.flow is v[1]
- v.add(tft(start=2))
+ v.add([tft(start=2)])
assert f.index == 1
assert f.flow is v[1]
@@ -324,22 +470,25 @@ def test_focus():
assert f.index == 0
f.index = 1
- v.remove(v[1])
+ v.remove([v[1]])
+ v[1].intercept()
assert f.index == 1
assert f.flow is v[1]
- v.remove(v[1])
+ v.remove([v[1]])
assert f.index == 0
assert f.flow is v[0]
- v.remove(v[0])
+ v.remove([v[0]])
assert f.index is None
assert f.flow is None
- v.add(tft(method="get", start=0))
- v.add(tft(method="get", start=1))
- v.add(tft(method="put", start=2))
- v.add(tft(method="get", start=3))
+ v.add([
+ tft(method="get", start=0),
+ tft(method="get", start=1),
+ tft(method="put", start=2),
+ tft(method="get", start=3),
+ ])
f.flow = v[2]
assert f.flow.request.method == "PUT"
@@ -359,16 +508,16 @@ def test_settings():
with pytest.raises(KeyError):
v.settings[f]
- v.add(f)
+ v.add([f])
v.settings[f]["foo"] = "bar"
assert v.settings[f]["foo"] == "bar"
assert len(list(v.settings)) == 1
- v.remove(f)
+ v.remove([f])
with pytest.raises(KeyError):
v.settings[f]
assert not v.settings.keys()
- v.add(f)
+ v.add([f])
v.settings[f]["foo"] = "bar"
assert v.settings.keys()
v.clear()
@@ -377,10 +526,10 @@ def test_settings():
def test_configure():
v = view.View()
- with taddons.context(options=options.Options()) as tctx:
- tctx.configure(v, filter="~q")
+ with taddons.context() as tctx:
+ tctx.configure(v, view_filter="~q")
with pytest.raises(Exception, match="Invalid interception filter"):
- tctx.configure(v, filter="~~")
+ tctx.configure(v, view_filter="~~")
tctx.configure(v, console_order="method")
with pytest.raises(Exception, match="Unknown flow order"):
@@ -388,7 +537,5 @@ def test_configure():
tctx.configure(v, console_order_reversed=True)
- tctx.configure(v, console_order=None)
-
tctx.configure(v, console_focus_follow=True)
assert v.focus_follow
diff --git a/test/mitmproxy/console/test_flowlist.py b/test/mitmproxy/console/test_flowlist.py
deleted file mode 100644
index 7c442b63..00000000
--- a/test/mitmproxy/console/test_flowlist.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from unittest import mock
-
-import mitmproxy.tools.console.flowlist as flowlist
-from mitmproxy.tools import console
-from mitmproxy import proxy
-from mitmproxy import options
-
-
-class TestFlowlist:
- def mkmaster(self, **opts):
- if "verbosity" not in opts:
- opts["verbosity"] = 1
- o = options.Options(**opts)
- return console.master.ConsoleMaster(o, proxy.DummyServer())
-
- def test_new_request(self):
- m = self.mkmaster()
- x = flowlist.FlowListBox(m)
- with mock.patch('mitmproxy.tools.console.signals.status_message.send') as mock_thing:
- x.new_request("nonexistent url", "GET")
- mock_thing.assert_called_once_with(message="Invalid URL: No hostname given")
diff --git a/test/mitmproxy/contentviews/test_api.py b/test/mitmproxy/contentviews/test_api.py
index 95d83af9..c072c86f 100644
--- a/test/mitmproxy/contentviews/test_api.py
+++ b/test/mitmproxy/contentviews/test_api.py
@@ -9,23 +9,28 @@ from mitmproxy.test import tutils
class TestContentView(contentviews.View):
name = "test"
- prompt = ("t", "test")
+ prompt = ("test", "t")
content_types = ["test/123"]
def test_add_remove():
tcv = TestContentView()
contentviews.add(tcv)
+ assert tcv in contentviews.views
# repeated addition causes exception
- with pytest.raises(ContentViewException):
+ with pytest.raises(ContentViewException, match="Duplicate view"):
contentviews.add(tcv)
+ tcv2 = TestContentView()
+ tcv2.name = "test2"
+ tcv2.prompt = ("test2", "t")
# Same shortcut doesn't work either.
- with pytest.raises(ContentViewException):
- contentviews.add(TestContentView())
+ with pytest.raises(ContentViewException, match="Duplicate view shortcut"):
+ contentviews.add(tcv2)
contentviews.remove(tcv)
+ assert tcv not in contentviews.views
def test_get_content_view():
@@ -43,6 +48,7 @@ def test_get_content_view():
headers=Headers(content_type="application/json")
)
assert desc == "JSON"
+ assert list(lines)
desc, lines, err = contentviews.get_content_view(
contentviews.get("JSON"),
@@ -84,3 +90,4 @@ def test_get_message_content_view():
def test_get_by_shortcut():
assert contentviews.get_by_shortcut("s")
+ assert not contentviews.get_by_shortcut("b")
diff --git a/test/mitmproxy/contentviews/test_xml_html.py b/test/mitmproxy/contentviews/test_xml_html.py
index 2b0aee4d..8148fd4c 100644
--- a/test/mitmproxy/contentviews/test_xml_html.py
+++ b/test/mitmproxy/contentviews/test_xml_html.py
@@ -11,6 +11,13 @@ def test_simple():
v = full_eval(xml_html.ViewXmlHtml())
assert v(b"foo") == ('XML', [[('text', 'foo')]])
assert v(b"<html></html>") == ('HTML', [[('text', '<html></html>')]])
+ assert v(b"<>") == ('XML', [[('text', '<>')]])
+ assert v(b"<p") == ('XML', [[('text', '<p')]])
+
+ with open(data.path("simple.html")) as f:
+ input = f.read()
+ tokens = xml_html.tokenize(input)
+ assert str(next(tokens)) == "Tag(<!DOCTYPE html>)"
@pytest.mark.parametrize("filename", [
@@ -18,6 +25,7 @@ def test_simple():
"cdata.xml",
"comment.xml",
"inline.html",
+ "test.html"
])
def test_format_xml(filename):
path = data.path(filename)
diff --git a/test/mitmproxy/contentviews/test_xml_html_data/test-formatted.html b/test/mitmproxy/contentviews/test_xml_html_data/test-formatted.html
new file mode 100644
index 00000000..0eb60004
--- /dev/null
+++ b/test/mitmproxy/contentviews/test_xml_html_data/test-formatted.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Title</title>
+</head>
+<body>
+ <p>
+ Lorem ipsum dolor
+ <p>
+ sit amet, consectetur
+ <p>
+ adipiscing elit, sed
+ <p>
+ do eiusmod tempor
+ <p>
+ incididunt ut
+ <p>
+ labore et dolore
+ <p>
+ magna aliqua.
+ <p>
+ Ut enim ad minim
+ <p>
+ veniam, quis nostrud
+ <p>
+ exercitation
+ <p>
+ ullamco laboris
+ <p>
+ nisi ut aliquip ex ea
+ <p>
+ commodo consequat.
+ <p>
+ Duis aute irure
+ <p>
+ dolor in reprehenderit
+ <p>
+ in voluptate velit
+ <p>
+ esse cillum dolore
+ <p>eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
+</body>
+</html>
diff --git a/test/mitmproxy/contentviews/test_xml_html_data/test.html b/test/mitmproxy/contentviews/test_xml_html_data/test.html
new file mode 100644
index 00000000..e74ac314
--- /dev/null
+++ b/test/mitmproxy/contentviews/test_xml_html_data/test.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Title</title>
+</head>
+<body>
+<p>Lorem ipsum dolor<p>sit amet, consectetur <p>adipiscing elit, sed<p>do eiusmod tempor<p> incididunt ut<p> labore et dolore<p> magna aliqua.
+ <p>Ut enim ad minim <p>veniam, quis nostrud <p>exercitation <p>ullamco laboris <p>
+ nisi ut aliquip ex ea <p>commodo consequat.<p>Duis aute irure <p>dolor in reprehenderit <p>in voluptate velit<p> esse cillum dolore <p>eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/test/mitmproxy/data/addonscripts/addon.py b/test/mitmproxy/data/addonscripts/addon.py
index f34f41cb..8c834d82 100644
--- a/test/mitmproxy/data/addonscripts/addon.py
+++ b/test/mitmproxy/data/addonscripts/addon.py
@@ -6,17 +6,19 @@ class Addon:
def event_log(self):
return event_log
- def start(self, opts):
- event_log.append("addonstart")
+ def load(self, opts):
+ event_log.append("addonload")
- def configure(self, options, updated):
+ def configure(self, updated):
event_log.append("addonconfigure")
-def configure(options, updated):
- event_log.append("addonconfigure")
+def configure(updated):
+ event_log.append("scriptconfigure")
-def start(opts):
- event_log.append("scriptstart")
- return Addon()
+def load(l):
+ event_log.append("scriptload")
+
+
+addons = [Addon()]
diff --git a/test/mitmproxy/data/addonscripts/concurrent_decorator.py b/test/mitmproxy/data/addonscripts/concurrent_decorator.py
index 162c00f4..d1ab6c6c 100644
--- a/test/mitmproxy/data/addonscripts/concurrent_decorator.py
+++ b/test/mitmproxy/data/addonscripts/concurrent_decorator.py
@@ -1,4 +1,5 @@
import time
+import sys
from mitmproxy.script import concurrent
diff --git a/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py b/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py
index 10ba24cd..2a7d300c 100644
--- a/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py
+++ b/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py
@@ -9,5 +9,4 @@ class ConcurrentClass:
time.sleep(0.1)
-def start(opts):
- return ConcurrentClass()
+addons = [ConcurrentClass()]
diff --git a/test/mitmproxy/data/addonscripts/concurrent_decorator_err.py b/test/mitmproxy/data/addonscripts/concurrent_decorator_err.py
index 7bc28182..4f80e98a 100644
--- a/test/mitmproxy/data/addonscripts/concurrent_decorator_err.py
+++ b/test/mitmproxy/data/addonscripts/concurrent_decorator_err.py
@@ -2,5 +2,5 @@ from mitmproxy.script import concurrent
@concurrent
-def start(opts):
+def load(v):
pass
diff --git a/test/mitmproxy/data/addonscripts/print.py b/test/mitmproxy/data/addonscripts/print.py
new file mode 100644
index 00000000..93b65a64
--- /dev/null
+++ b/test/mitmproxy/data/addonscripts/print.py
@@ -0,0 +1,2 @@
+def load(l):
+ print("stdoutprint")
diff --git a/test/mitmproxy/data/addonscripts/recorder/a.py b/test/mitmproxy/data/addonscripts/recorder/a.py
new file mode 100644
index 00000000..df81d86b
--- /dev/null
+++ b/test/mitmproxy/data/addonscripts/recorder/a.py
@@ -0,0 +1,3 @@
+import recorder
+
+addons = [recorder.Recorder("a")]
diff --git a/test/mitmproxy/data/addonscripts/recorder/b.py b/test/mitmproxy/data/addonscripts/recorder/b.py
new file mode 100644
index 00000000..ccbae705
--- /dev/null
+++ b/test/mitmproxy/data/addonscripts/recorder/b.py
@@ -0,0 +1,3 @@
+import recorder
+
+addons = [recorder.Recorder("b")]
diff --git a/test/mitmproxy/data/addonscripts/recorder/c.py b/test/mitmproxy/data/addonscripts/recorder/c.py
new file mode 100644
index 00000000..b8b0915e
--- /dev/null
+++ b/test/mitmproxy/data/addonscripts/recorder/c.py
@@ -0,0 +1,3 @@
+import recorder
+
+addons = [recorder.Recorder("c")]
diff --git a/test/mitmproxy/data/addonscripts/recorder/e.py b/test/mitmproxy/data/addonscripts/recorder/e.py
new file mode 100644
index 00000000..eb5eff5e
--- /dev/null
+++ b/test/mitmproxy/data/addonscripts/recorder/e.py
@@ -0,0 +1,3 @@
+import recorder
+
+addons = [recorder.Recorder("e")]
diff --git a/test/mitmproxy/data/addonscripts/recorder.py b/test/mitmproxy/data/addonscripts/recorder/recorder.py
index aff524a8..a962d3df 100644
--- a/test/mitmproxy/data/addonscripts/recorder.py
+++ b/test/mitmproxy/data/addonscripts/recorder/recorder.py
@@ -1,13 +1,12 @@
from mitmproxy import controller
from mitmproxy import eventsequence
from mitmproxy import ctx
-import sys
-class CallLogger:
+class Recorder:
call_log = []
- def __init__(self, name = "solo"):
+ def __init__(self, name = "recorder"):
self.name = name
def __getattr__(self, attr):
@@ -22,5 +21,4 @@ class CallLogger:
raise AttributeError
-def start(opts):
- return CallLogger(*sys.argv[1:])
+addons = [Recorder()]
diff --git a/test/mitmproxy/data/dumpfile-018 b/test/mitmproxy/data/dumpfile-018
index abe8b0b1..6a27b5a6 100644
--- a/test/mitmproxy/data/dumpfile-018
+++ b/test/mitmproxy/data/dumpfile-018
@@ -1,4 +1,4 @@
-5243:5:error;0:~11:intercepted;5:false!6:marked;5:false!2:id;36:55367415-10f5-4938-b69f-8a523394f947;7:request;396:10:stickyauth;5:false!7:content;0:,15:timestamp_start;18:1482157523.9086578^9:is_replay;5:false!4:path;1:/,4:host;15:www.example.com,17:first_line_format;8:relative;12:stickycookie;5:false!12:http_version;8:HTTP/1.1,6:method;3:GET,4:port;3:443#13:timestamp_end;18:1482157523.9086578^6:scheme;5:https,7:headers;82:29:10:User-Agent,11:curl/7.35.0,]26:4:Host,15:www.example.com,]15:6:Accept,3:*/*,]]}8:response;1851:6:reason;2:OK,12:http_version;8:HTTP/1.1,13:timestamp_end;17:1482157524.361187^11:status_code;3:200#7:content;1270:<!doctype html>
+7816:4:type;4:http;2:id;36:55367415-10f5-4938-b69f-8a523394f947;8:response;1851:15:timestamp_start;17:1482157524.361187^12:http_version;8:HTTP/1.1,7:content;1270:<!doctype html>
<html>
<head>
<title>Example Domain</title>
@@ -48,7 +48,7 @@
</div>
</body>
</html>
-,7:headers;410:25:13:Accept-Ranges,5:bytes,]35:13:Cache-Control,14:max-age=604800,]28:12:Content-Type,9:text/html,]40:4:Date,29:Mon, 19 Dec 2016 14:25:24 GMT,]22:4:Etag,11:"359670651",]43:7:Expires,29:Mon, 26 Dec 2016 14:25:24 GMT,]50:13:Last-Modified,29:Fri, 09 Aug 2013 23:54:35 GMT,]27:6:Server,14:ECS (iad/18CB),]26:4:Vary,15:Accept-Encoding,]16:7:X-Cache,3:HIT,]25:17:x-ec-custom-error,1:1,]25:14:Content-Length,4:1270,]]15:timestamp_start;17:1482157524.361187^}4:type;4:http;11:server_conn;2570:15:ssl_established;4:true!7:address;58:7:address;25:15:www.example.com;3:443#]8:use_ipv6;5:false!}10:ip_address;56:7:address;23:13:93.184.216.34;3:443#]8:use_ipv6;5:false!}3:via;0:~14:source_address;57:7:address;24:12:10.67.53.133;5:52775#]8:use_ipv6;5:false!}13:timestamp_end;0:~4:cert;2122:-----BEGIN CERTIFICATE-----
+,13:timestamp_end;17:1482157524.361187^11:status_code;3:200#6:reason;2:OK,7:headers;410:25:13:Accept-Ranges,5:bytes,]35:13:Cache-Control,14:max-age=604800,]28:12:Content-Type,9:text/html,]40:4:Date,29:Mon, 19 Dec 2016 14:25:24 GMT,]22:4:Etag,11:"359670651",]43:7:Expires,29:Mon, 26 Dec 2016 14:25:24 GMT,]50:13:Last-Modified,29:Fri, 09 Aug 2013 23:54:35 GMT,]27:6:Server,14:ECS (iad/18CB),]26:4:Vary,15:Accept-Encoding,]16:7:X-Cache,3:HIT,]25:17:x-ec-custom-error,1:1,]25:14:Content-Length,4:1270,]]}7:request;396:9:is_replay;5:false!17:first_line_format;8:relative;4:port;3:443#7:content;0:,12:stickycookie;5:false!6:method;3:GET,7:headers;82:29:10:User-Agent,11:curl/7.35.0,]26:4:Host,15:www.example.com,]15:6:Accept,3:*/*,]]15:timestamp_start;18:1482157523.9086578^12:http_version;8:HTTP/1.1,13:timestamp_end;18:1482157523.9086578^4:path;1:/,10:stickyauth;5:false!4:host;15:www.example.com,6:scheme;5:https,}7:version;13:1:0#2:18#1:2#]5:error;0:~11:intercepted;5:false!11:server_conn;5143:10:ip_address;56:8:use_ipv6;5:false!7:address;23:13:93.184.216.34;3:443#]}15:timestamp_start;18:1482157523.9086578^19:timestamp_tcp_setup;18:1482157524.0081189^15:ssl_established;4:true!14:source_address;57:8:use_ipv6;5:false!7:address;24:12:10.67.53.133;5:52775#]}19:timestamp_ssl_setup;17:1482157524.260993^4:cert;2122:-----BEGIN CERTIFICATE-----
MIIF8jCCBNqgAwIBAgIQDmTF+8I2reFLFyrrQceMsDANBgkqhkiG9w0BAQsFADBw
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMS8wLQYDVQQDEyZEaWdpQ2VydCBTSEEyIEhpZ2ggQXNz
@@ -82,4 +82,38 @@ ieqRbcuFjmqfyPmUv1U9QoI4TQikpw7TZU0zYZANP4C/gj4Ry48/znmUaRvy2kvI
l7gRQ21qJTK5suoiYoYNo3J9T+pXPGU7Lydz/HwW+w0DpArtAaukI8aNX4ohFUKS
wDSiIIWIWJiJGbEeIO0TIFwEVWTOnbNl/faPXpk5IRXicapqiII=
-----END CERTIFICATE-----
-,15:timestamp_start;18:1482157523.9086578^3:sni;15:www.example.com;19:timestamp_ssl_setup;17:1482157524.260993^19:timestamp_tcp_setup;18:1482157524.0081189^}11:client_conn;216:15:ssl_established;4:true!7:address;53:7:address;20:9:127.0.0.1;5:52774#]8:use_ipv6;5:false!}10:clientcert;0:~13:timestamp_end;0:~15:timestamp_start;18:1482157522.8949482^19:timestamp_ssl_setup;18:1482157523.9086578^}7:version;13:1:0#2:18#1:2#]} \ No newline at end of file
+,13:timestamp_end;0:~3:via;2570:15:ssl_established;4:true!19:timestamp_tcp_setup;18:1482157524.0081189^19:timestamp_ssl_setup;17:1482157524.260993^3:via;0:~3:sni;15:www.example.com;10:ip_address;56:8:use_ipv6;5:false!7:address;23:13:93.184.216.34;3:443#]}15:timestamp_start;18:1482157523.9086578^14:source_address;57:8:use_ipv6;5:false!7:address;24:12:10.67.53.133;5:52775#]}4:cert;2122:-----BEGIN CERTIFICATE-----
+MIIF8jCCBNqgAwIBAgIQDmTF+8I2reFLFyrrQceMsDANBgkqhkiG9w0BAQsFADBw
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMS8wLQYDVQQDEyZEaWdpQ2VydCBTSEEyIEhpZ2ggQXNz
+dXJhbmNlIFNlcnZlciBDQTAeFw0xNTExMDMwMDAwMDBaFw0xODExMjgxMjAwMDBa
+MIGlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxML
+TG9zIEFuZ2VsZXMxPDA6BgNVBAoTM0ludGVybmV0IENvcnBvcmF0aW9uIGZvciBB
+c3NpZ25lZCBOYW1lcyBhbmQgTnVtYmVyczETMBEGA1UECxMKVGVjaG5vbG9neTEY
+MBYGA1UEAxMPd3d3LmV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAs0CWL2FjPiXBl61lRfvvE0KzLJmG9LWAC3bcBjgsH6NiVVo2dt6u
+Xfzi5bTm7F3K7srfUBYkLO78mraM9qizrHoIeyofrV/n+pZZJauQsPjCPxMEJnRo
+D8Z4KpWKX0LyDu1SputoI4nlQ/htEhtiQnuoBfNZxF7WxcxGwEsZuS1KcXIkHl5V
+RJOreKFHTaXcB1qcZ/QRaBIv0yhxvK1yBTwWddT4cli6GfHcCe3xGMaSL328Fgs3
+jYrvG29PueB6VJi/tbbPu6qTfwp/H1brqdjh29U52Bhb0fJkM9DWxCP/Cattcc7a
+z8EXnCO+LK8vkhw/kAiJWPKx4RBvgy73nwIDAQABo4ICUDCCAkwwHwYDVR0jBBgw
+FoAUUWj/kK8CB3U8zNllZGKiErhZcjswHQYDVR0OBBYEFKZPYB4fLdHn8SOgKpUW
+5Oia6m5IMIGBBgNVHREEejB4gg93d3cuZXhhbXBsZS5vcmeCC2V4YW1wbGUuY29t
+ggtleGFtcGxlLmVkdYILZXhhbXBsZS5uZXSCC2V4YW1wbGUub3Jngg93d3cuZXhh
+bXBsZS5jb22CD3d3dy5leGFtcGxlLmVkdYIPd3d3LmV4YW1wbGUubmV0MA4GA1Ud
+DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0f
+BG4wbDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItaGEtc2Vy
+dmVyLWc0LmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTIt
+aGEtc2VydmVyLWc0LmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgGCCsG
+AQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAECAjCB
+gwYIKwYBBQUHAQEEdzB1MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
+dC5jb20wTQYIKwYBBQUHMAKGQWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E
+aWdpQ2VydFNIQTJIaWdoQXNzdXJhbmNlU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQC
+MAAwDQYJKoZIhvcNAQELBQADggEBAISomhGn2L0LJn5SJHuyVZ3qMIlRCIdvqe0Q
+6ls+C8ctRwRO3UU3x8q8OH+2ahxlQmpzdC5al4XQzJLiLjiJ2Q1p+hub8MFiMmVP
+PZjb2tZm2ipWVuMRM+zgpRVM6nVJ9F3vFfUSHOb4/JsEIUvPY+d8/Krc+kPQwLvy
+ieqRbcuFjmqfyPmUv1U9QoI4TQikpw7TZU0zYZANP4C/gj4Ry48/znmUaRvy2kvI
+l7gRQ21qJTK5suoiYoYNo3J9T+pXPGU7Lydz/HwW+w0DpArtAaukI8aNX4ohFUKS
+wDSiIIWIWJiJGbEeIO0TIFwEVWTOnbNl/faPXpk5IRXicapqiII=
+-----END CERTIFICATE-----
+,13:timestamp_end;0:~7:address;58:8:use_ipv6;5:false!7:address;25:15:www.example.com;3:443#]}}7:address;58:8:use_ipv6;5:false!7:address;25:15:www.example.com;3:443#]}3:sni;15:www.example.com;}11:client_conn;216:15:timestamp_start;18:1482157522.8949482^15:ssl_established;4:true!13:timestamp_end;0:~10:clientcert;0:~7:address;53:8:use_ipv6;5:false!7:address;20:9:127.0.0.1;5:52774#]}19:timestamp_ssl_setup;18:1482157523.9086578^}6:marked;5:false!} \ No newline at end of file
diff --git a/test/mitmproxy/data/test_flow_export/locust_get.py b/test/mitmproxy/data/test_flow_export/locust_get.py
deleted file mode 100644
index 632d5d53..00000000
--- a/test/mitmproxy/data/test_flow_export/locust_get.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from locust import HttpLocust, TaskSet, task
-
-class UserBehavior(TaskSet):
- def on_start(self):
- ''' on_start is called when a Locust start before any task is scheduled '''
- self.path()
-
- @task()
- def path(self):
- url = self.locust.host + '/path'
-
- headers = {
- 'header': 'qvalue',
- 'content-length': '7',
- }
-
- params = {
- 'a': ['foo', 'bar'],
- 'b': 'baz',
- }
-
- self.response = self.client.request(
- method='GET',
- url=url,
- headers=headers,
- params=params,
- )
-
- ### Additional tasks can go here ###
-
-
-class WebsiteUser(HttpLocust):
- task_set = UserBehavior
- min_wait = 1000
- max_wait = 3000
diff --git a/test/mitmproxy/data/test_flow_export/locust_patch.py b/test/mitmproxy/data/test_flow_export/locust_patch.py
deleted file mode 100644
index f64e0857..00000000
--- a/test/mitmproxy/data/test_flow_export/locust_patch.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from locust import HttpLocust, TaskSet, task
-
-class UserBehavior(TaskSet):
- def on_start(self):
- ''' on_start is called when a Locust start before any task is scheduled '''
- self.path()
-
- @task()
- def path(self):
- url = self.locust.host + '/path'
-
- headers = {
- 'header': 'qvalue',
- 'content-length': '7',
- }
-
- params = {
- 'query': 'param',
- }
-
- data = '''content'''
-
- self.response = self.client.request(
- method='PATCH',
- url=url,
- headers=headers,
- params=params,
- data=data,
- )
-
- ### Additional tasks can go here ###
-
-
-class WebsiteUser(HttpLocust):
- task_set = UserBehavior
- min_wait = 1000
- max_wait = 3000
diff --git a/test/mitmproxy/data/test_flow_export/locust_post.py b/test/mitmproxy/data/test_flow_export/locust_post.py
deleted file mode 100644
index df23476a..00000000
--- a/test/mitmproxy/data/test_flow_export/locust_post.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from locust import HttpLocust, TaskSet, task
-
-class UserBehavior(TaskSet):
- def on_start(self):
- ''' on_start is called when a Locust start before any task is scheduled '''
- self.path()
-
- @task()
- def path(self):
- url = self.locust.host + '/path'
-
- data = '''content'''
-
- self.response = self.client.request(
- method='POST',
- url=url,
- data=data,
- )
-
- ### Additional tasks can go here ###
-
-
-class WebsiteUser(HttpLocust):
- task_set = UserBehavior
- min_wait = 1000
- max_wait = 3000
diff --git a/test/mitmproxy/data/test_flow_export/locust_task_get.py b/test/mitmproxy/data/test_flow_export/locust_task_get.py
deleted file mode 100644
index 03821cd8..00000000
--- a/test/mitmproxy/data/test_flow_export/locust_task_get.py
+++ /dev/null
@@ -1,20 +0,0 @@
- @task()
- def path(self):
- url = self.locust.host + '/path'
-
- headers = {
- 'header': 'qvalue',
- 'content-length': '7',
- }
-
- params = {
- 'a': ['foo', 'bar'],
- 'b': 'baz',
- }
-
- self.response = self.client.request(
- method='GET',
- url=url,
- headers=headers,
- params=params,
- )
diff --git a/test/mitmproxy/data/test_flow_export/locust_task_patch.py b/test/mitmproxy/data/test_flow_export/locust_task_patch.py
deleted file mode 100644
index d425209c..00000000
--- a/test/mitmproxy/data/test_flow_export/locust_task_patch.py
+++ /dev/null
@@ -1,22 +0,0 @@
- @task()
- def path(self):
- url = self.locust.host + '/path'
-
- headers = {
- 'header': 'qvalue',
- 'content-length': '7',
- }
-
- params = {
- 'query': 'param',
- }
-
- data = '''content'''
-
- self.response = self.client.request(
- method='PATCH',
- url=url,
- headers=headers,
- params=params,
- data=data,
- )
diff --git a/test/mitmproxy/data/test_flow_export/locust_task_post.py b/test/mitmproxy/data/test_flow_export/locust_task_post.py
deleted file mode 100644
index 989df455..00000000
--- a/test/mitmproxy/data/test_flow_export/locust_task_post.py
+++ /dev/null
@@ -1,11 +0,0 @@
- @task()
- def path(self):
- url = self.locust.host + '/path'
-
- data = '''content'''
-
- self.response = self.client.request(
- method='POST',
- url=url,
- data=data,
- )
diff --git a/test/mitmproxy/data/test_flow_export/python_get.py b/test/mitmproxy/data/test_flow_export/python_get.py
deleted file mode 100644
index e9ed072a..00000000
--- a/test/mitmproxy/data/test_flow_export/python_get.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import requests
-
-response = requests.get(
- 'http://address:22/path',
- params=[('a', 'foo'), ('a', 'bar'), ('b', 'baz')],
- headers={'header': 'qvalue'}
-)
-
-print(response.text) \ No newline at end of file
diff --git a/test/mitmproxy/data/test_flow_export/python_patch.py b/test/mitmproxy/data/test_flow_export/python_patch.py
deleted file mode 100644
index d83a57b9..00000000
--- a/test/mitmproxy/data/test_flow_export/python_patch.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import requests
-
-response = requests.patch(
- 'http://address:22/path',
- params=[('query', 'param')],
- headers={'header': 'qvalue'},
- data=b'content'
-)
-
-print(response.text) \ No newline at end of file
diff --git a/test/mitmproxy/data/test_flow_export/python_post.py b/test/mitmproxy/data/test_flow_export/python_post.py
deleted file mode 100644
index 6254adfb..00000000
--- a/test/mitmproxy/data/test_flow_export/python_post.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import requests
-
-response = requests.post(
- 'http://address:22/path',
- data=b'content'
-)
-
-print(response.text)
diff --git a/test/mitmproxy/data/test_flow_export/python_post_json.py b/test/mitmproxy/data/test_flow_export/python_post_json.py
deleted file mode 100644
index d6ae6357..00000000
--- a/test/mitmproxy/data/test_flow_export/python_post_json.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import requests
-
-response = requests.post(
- 'http://address:22/path',
- headers={'content-type': 'application/json'},
- json={'email': 'example@example.com', 'name': 'example'}
-)
-
-print(response.text) \ No newline at end of file
diff --git a/test/mitmproxy/test_io_compat.py b/test/mitmproxy/io/test_compat.py
index 288de4fc..288de4fc 100644
--- a/test/mitmproxy/test_io_compat.py
+++ b/test/mitmproxy/io/test_compat.py
diff --git a/test/mitmproxy/test_io.py b/test/mitmproxy/io/test_io.py
index 777ab4dd..777ab4dd 100644
--- a/test/mitmproxy/test_io.py
+++ b/test/mitmproxy/io/test_io.py
diff --git a/test/mitmproxy/contrib/test_tnetstring.py b/test/mitmproxy/io/test_tnetstring.py
index 05c4a7c9..f7141de0 100644
--- a/test/mitmproxy/contrib/test_tnetstring.py
+++ b/test/mitmproxy/io/test_tnetstring.py
@@ -4,7 +4,7 @@ import math
import io
import struct
-from mitmproxy.contrib import tnetstring
+from mitmproxy.io import tnetstring
MAXINT = 2 ** (struct.Struct('i').size * 8 - 1) - 1
diff --git a/test/mitmproxy/net/http/http1/test_read.py b/test/mitmproxy/net/http/http1/test_read.py
index 642b91c0..b3589c92 100644
--- a/test/mitmproxy/net/http/http1/test_read.py
+++ b/test/mitmproxy/net/http/http1/test_read.py
@@ -243,6 +243,7 @@ def test_read_request_line():
def test_parse_authority_form():
assert _parse_authority_form(b"foo:42") == (b"foo", 42)
+ assert _parse_authority_form(b"[2001:db8:42::]:443") == (b"2001:db8:42::", 443)
with pytest.raises(exceptions.HttpSyntaxException):
_parse_authority_form(b"foo")
with pytest.raises(exceptions.HttpSyntaxException):
diff --git a/test/mitmproxy/net/http/test_cookies.py b/test/mitmproxy/net/http/test_cookies.py
index 5c30dbdb..77549d9e 100644
--- a/test/mitmproxy/net/http/test_cookies.py
+++ b/test/mitmproxy/net/http/test_cookies.py
@@ -269,6 +269,9 @@ def test_refresh_cookie():
c = "MOO=BAR; Expires=Tue, 08-Mar-2011 00:20:38 GMT; Path=foo.com; Secure"
assert "00:21:38" in cookies.refresh_set_cookie_header(c, 60)
+ c = "rfoo=bar; Domain=reddit.com; expires=Thu, 31 Dec 2037; Path=/"
+ assert "expires" not in cookies.refresh_set_cookie_header(c, 60)
+
c = "foo,bar"
with pytest.raises(ValueError):
cookies.refresh_set_cookie_header(c, 60)
@@ -283,6 +286,10 @@ def test_refresh_cookie():
c = "foo/bar=bla"
assert cookies.refresh_set_cookie_header(c, 0)
+ # https://github.com/mitmproxy/mitmproxy/issues/2250
+ c = ""
+ assert cookies.refresh_set_cookie_header(c, 60) == ""
+
@mock.patch('time.time')
def test_get_expiration_ts(*args):
diff --git a/test/mitmproxy/net/http/test_message.py b/test/mitmproxy/net/http/test_message.py
index b75bc7c2..512f3199 100644
--- a/test/mitmproxy/net/http/test_message.py
+++ b/test/mitmproxy/net/http/test_message.py
@@ -48,6 +48,12 @@ class TestMessageData:
assert data != 0
+ def test_serializable(self):
+ data1 = tutils.tresp(timestamp_start=42, timestamp_end=42).data
+ data2 = tutils.tresp().data.from_state(data1.get_state()) # ResponseData.from_state()
+
+ assert data1 == data2
+
class TestMessage:
@@ -117,6 +123,14 @@ class TestMessageContentEncoding:
assert r.content == b"message"
assert r.raw_content != b"message"
+ def test_update_content_length_header(self):
+ r = tutils.tresp()
+ assert int(r.headers["content-length"]) == 7
+ r.encode("gzip")
+ assert int(r.headers["content-length"]) == 27
+ r.decode()
+ assert int(r.headers["content-length"]) == 7
+
def test_modify(self):
r = tutils.tresp()
assert "content-encoding" not in r.headers
diff --git a/test/mitmproxy/net/test_imports.py b/test/mitmproxy/net/test_imports.py
deleted file mode 100644
index b88ef26d..00000000
--- a/test/mitmproxy/net/test_imports.py
+++ /dev/null
@@ -1 +0,0 @@
-# These are actually tests!
diff --git a/test/mitmproxy/net/test_tcp.py b/test/mitmproxy/net/test_tcp.py
index 8b26784a..81d51888 100644
--- a/test/mitmproxy/net/test_tcp.py
+++ b/test/mitmproxy/net/test_tcp.py
@@ -529,10 +529,10 @@ class TestTimeOut(tservers.ServerTestBase):
class TestCryptographyALPN:
def test_has_alpn(self):
- if 'OPENSSL_ALPN' in os.environ:
+ if os.environ.get("OPENSSL") == "with-alpn":
assert tcp.HAS_ALPN
assert SSL._lib.Cryptography_HAS_ALPN
- elif 'OPENSSL_OLD' in os.environ:
+ elif os.environ.get("OPENSSL") == "old":
assert not tcp.HAS_ALPN
assert not SSL._lib.Cryptography_HAS_ALPN
@@ -603,13 +603,36 @@ class TestDHParams(tservers.ServerTestBase):
assert ret[0] == "DHE-RSA-AES256-SHA"
-class TestTCPClient:
+class TestTCPClient(tservers.ServerTestBase):
def test_conerr(self):
c = tcp.TCPClient(("127.0.0.1", 0))
- with pytest.raises(exceptions.TcpException):
+ with pytest.raises(exceptions.TcpException, match="Error connecting"):
c.connect()
+ def test_timeout(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ with c.create_connection(timeout=20) as conn:
+ assert conn.gettimeout() == 20
+
+ def test_spoof_address(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port), spoof_source_address=("127.0.0.1", 0))
+ with pytest.raises(exceptions.TcpException, match="Failed to spoof"):
+ c.connect()
+
+
+class TestTCPServer:
+
+ def test_binderr(self):
+ with pytest.raises(socket.error, match="prohibited"):
+ tcp.TCPServer(("localhost", 8080))
+
+ def test_wait_for_silence(self):
+ s = tcp.TCPServer(("127.0.0.1", 0))
+ with s.handler_counter:
+ with pytest.raises(exceptions.Timeout):
+ s.wait_for_silence()
+
class TestFileLike:
@@ -811,7 +834,7 @@ class TestSSLKeyLogger(tservers.ServerTestBase):
assert not tcp.SSLKeyLogger.create_logfun(False)
-class TestSSLInvalidMethod(tservers.ServerTestBase):
+class TestSSLInvalid(tservers.ServerTestBase):
handler = EchoHandler
ssl = True
@@ -821,3 +844,13 @@ class TestSSLInvalidMethod(tservers.ServerTestBase):
with c.connect():
with pytest.raises(exceptions.TlsException):
c.convert_to_ssl(method=fake_ssl_method)
+
+ def test_alpn_error(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ with c.connect():
+ if tcp.HAS_ALPN:
+ with pytest.raises(exceptions.TlsException, match="must be a function"):
+ c.create_ssl_context(alpn_select_callback="foo")
+
+ with pytest.raises(exceptions.TlsException, match="ALPN error"):
+ c.create_ssl_context(alpn_select="foo", alpn_select_callback="bar")
diff --git a/test/mitmproxy/platform/__init__.py b/test/mitmproxy/platform/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/mitmproxy/platform/__init__.py
diff --git a/test/mitmproxy/proxy/protocol/test_http2.py b/test/mitmproxy/proxy/protocol/test_http2.py
index 1f695cc5..b07257b3 100644
--- a/test/mitmproxy/proxy/protocol/test_http2.py
+++ b/test/mitmproxy/proxy/protocol/test_http2.py
@@ -11,7 +11,7 @@ from mitmproxy import options
from mitmproxy.proxy.config import ProxyConfig
import mitmproxy.net
-from ....mitmproxy.net import tservers as net_tservers
+from ...net import tservers as net_tservers
from mitmproxy import exceptions
from mitmproxy.net.http import http1, http2
@@ -36,7 +36,11 @@ class _Http2ServerBase(net_tservers.ServerTestBase):
class handler(mitmproxy.net.tcp.BaseHandler):
def handle(self):
- h2_conn = h2.connection.H2Connection(client_side=False, header_encoding=False)
+ config = h2.config.H2Configuration(
+ client_side=False,
+ validate_outbound_headers=False,
+ validate_inbound_headers=False)
+ h2_conn = h2.connection.H2Connection(config)
preamble = self.rfile.read(24)
h2_conn.initiate_connection()
@@ -138,7 +142,11 @@ class _Http2TestBase:
client.convert_to_ssl(alpn_protos=[b'h2'])
- h2_conn = h2.connection.H2Connection(client_side=True, header_encoding=False)
+ config = h2.config.H2Configuration(
+ client_side=True,
+ validate_outbound_headers=False,
+ validate_inbound_headers=False)
+ h2_conn = h2.connection.H2Connection(config)
h2_conn.initiate_connection()
client.wfile.write(h2_conn.data_to_send())
client.wfile.flush()
@@ -756,7 +764,7 @@ class TestMaxConcurrentStreams(_Http2Test):
@classmethod
def setup_class(cls):
_Http2TestBase.setup_class()
- _Http2ServerBase.setup_class(h2_server_settings={h2.settings.MAX_CONCURRENT_STREAMS: 2})
+ _Http2ServerBase.setup_class(h2_server_settings={h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 2})
@classmethod
def handle_server_event(cls, event, h2_conn, rfile, wfile):
diff --git a/test/mitmproxy/proxy/protocol/test_websocket.py b/test/mitmproxy/proxy/protocol/test_websocket.py
index 486e9d64..8dfc4f2b 100644
--- a/test/mitmproxy/proxy/protocol/test_websocket.py
+++ b/test/mitmproxy/proxy/protocol/test_websocket.py
@@ -11,7 +11,7 @@ from mitmproxy.proxy.config import ProxyConfig
from mitmproxy.net import tcp
from mitmproxy.net import http
-from ....mitmproxy.net import tservers as net_tservers
+from ...net import tservers as net_tservers
from ... import tservers
from mitmproxy.net import websockets
diff --git a/test/mitmproxy/proxy/test_server.py b/test/mitmproxy/proxy/test_server.py
index 447b15a5..b4bb46bb 100644
--- a/test/mitmproxy/proxy/test_server.py
+++ b/test/mitmproxy/proxy/test_server.py
@@ -296,8 +296,8 @@ class TestHTTP(tservers.HTTPProxyTest, CommonMixin):
class TestHTTPAuth(tservers.HTTPProxyTest):
def test_auth(self):
self.master.addons.add(proxyauth.ProxyAuth())
- self.master.addons.configure_all(
- self.master.options, self.master.options.keys()
+ self.master.addons.trigger(
+ "configure", self.master.options.keys()
)
self.master.options.proxyauth = "test:test"
assert self.pathod("202").status_code == 407
diff --git a/test/mitmproxy/script/test_concurrent.py b/test/mitmproxy/script/test_concurrent.py
index 0e397b8f..ceff9fb9 100644
--- a/test/mitmproxy/script/test_concurrent.py
+++ b/test/mitmproxy/script/test_concurrent.py
@@ -3,8 +3,6 @@ from mitmproxy.test import tutils
from mitmproxy.test import taddons
from mitmproxy import controller
-from mitmproxy.addons import script
-
import time
from .. import tservers
@@ -19,13 +17,11 @@ class Thing:
class TestConcurrent(tservers.MasterTest):
def test_concurrent(self):
with taddons.context() as tctx:
- sc = script.Script(
+ sc = tctx.script(
tutils.test_data.path(
"mitmproxy/data/addonscripts/concurrent_decorator.py"
)
)
- sc.start(tctx.options)
-
f1, f2 = tflow.tflow(), tflow.tflow()
tctx.cycle(sc, f1)
tctx.cycle(sc, f2)
@@ -37,23 +33,20 @@ class TestConcurrent(tservers.MasterTest):
def test_concurrent_err(self):
with taddons.context() as tctx:
- sc = script.Script(
+ tctx.script(
tutils.test_data.path(
"mitmproxy/data/addonscripts/concurrent_decorator_err.py"
)
)
- sc.start(tctx.options)
assert tctx.master.has_log("decorator not supported")
def test_concurrent_class(self):
with taddons.context() as tctx:
- sc = script.Script(
+ sc = tctx.script(
tutils.test_data.path(
"mitmproxy/data/addonscripts/concurrent_decorator_class.py"
)
)
- sc.start(tctx.options)
-
f1, f2 = tflow.tflow(), tflow.tflow()
tctx.cycle(sc, f1)
tctx.cycle(sc, f2)
diff --git a/test/mitmproxy/test_addonmanager.py b/test/mitmproxy/test_addonmanager.py
index e7be25b8..678bc1b7 100644
--- a/test/mitmproxy/test_addonmanager.py
+++ b/test/mitmproxy/test_addonmanager.py
@@ -1,17 +1,27 @@
import pytest
+from mitmproxy import addons
from mitmproxy import addonmanager
from mitmproxy import exceptions
from mitmproxy import options
+from mitmproxy import command
from mitmproxy import master
from mitmproxy import proxy
+from mitmproxy.test import taddons
+from mitmproxy.test import tflow
class TAddon:
- def __init__(self, name):
+ def __init__(self, name, addons=None):
self.name = name
self.tick = True
self.custom_called = False
+ if addons:
+ self.addons = addons
+
+ @command.command("test.command")
+ def testcommand(self) -> str:
+ return "here"
def __repr__(self):
return "Addon(%s)" % self.name
@@ -23,25 +33,148 @@ class TAddon:
self.custom_called = True
-def test_simple():
+class THalt:
+ def event_custom(self):
+ raise exceptions.AddonHalt
+
+
+class AOption:
+ def load(self, l):
+ l.add_option("custom_option", bool, False, "help")
+
+
+def test_command():
+ with taddons.context() as tctx:
+ tctx.master.addons.add(TAddon("test"))
+ assert tctx.master.commands.call("test.command") == "here"
+
+
+def test_halt():
o = options.Options()
m = master.Master(o, proxy.DummyServer(o))
a = addonmanager.AddonManager(m)
- with pytest.raises(exceptions.AddonError):
- a.invoke_addon(TAddon("one"), "done")
+ halt = THalt()
+ end = TAddon("end")
+ a.add(halt)
+ a.add(end)
+
+ a.trigger("custom")
+ assert not end.custom_called
+
+ a.remove(halt)
+ a.trigger("custom")
+ assert end.custom_called
- a.add(TAddon("one"))
- assert a.get("one")
- assert not a.get("two")
- a.clear()
- assert not a.chain
+def test_lifecycle():
+ o = options.Options()
+ m = master.Master(o, proxy.DummyServer(o))
+ a = addonmanager.AddonManager(m)
a.add(TAddon("one"))
- a.trigger("done")
- with pytest.raises(exceptions.AddonError):
+
+ with pytest.raises(exceptions.AddonManagerError):
+ a.add(TAddon("one"))
+ with pytest.raises(exceptions.AddonManagerError):
+ a.remove(TAddon("nonexistent"))
+
+ f = tflow.tflow()
+ a.handle_lifecycle("request", f)
+
+ a._configure_all(o, o.keys())
+
+
+def test_defaults():
+ assert addons.default_addons()
+
+
+def test_loader():
+ with taddons.context() as tctx:
+ l = addonmanager.Loader(tctx.master)
+ l.add_option("custom_option", bool, False, "help")
+ l.add_option("custom_option", bool, False, "help")
+
+ def cmd(a: str) -> str:
+ return "foo"
+
+ l.add_command("test.command", cmd)
+
+
+def test_simple():
+ with taddons.context() as tctx:
+ a = tctx.master.addons
+
+ assert len(a) == 0
+ a.add(TAddon("one"))
+ assert a.get("one")
+ assert not a.get("two")
+ assert len(a) == 1
+ a.clear()
+ assert len(a) == 0
+ assert not a.chain
+
+ a.add(TAddon("one"))
+ a.trigger("done")
a.trigger("tick")
+ tctx.master.has_log("not callable")
+
+ a.remove(a.get("one"))
+ assert not a.get("one")
+
+ ta = TAddon("one")
+ a.add(ta)
+ a.trigger("custom")
+ assert ta.custom_called
+
+
+def test_load_option():
+ o = options.Options()
+ m = master.Master(o, proxy.DummyServer(o))
+ a = addonmanager.AddonManager(m)
+ a.add(AOption())
+ assert "custom_option" in m.options._options
+
+
+def test_nesting():
+ o = options.Options()
+ m = master.Master(o, proxy.DummyServer(o))
+ a = addonmanager.AddonManager(m)
+
+ a.add(
+ TAddon(
+ "one",
+ addons=[
+ TAddon("two"),
+ TAddon("three", addons=[TAddon("four")])
+ ]
+ )
+ )
+ assert len(a.chain) == 1
+ assert a.get("one")
+ assert a.get("two")
+ assert a.get("three")
+ assert a.get("four")
- ta = TAddon("one")
- a.add(ta)
a.trigger("custom")
- assert ta.custom_called
+ assert a.get("one").custom_called
+ assert a.get("two").custom_called
+ assert a.get("three").custom_called
+ assert a.get("four").custom_called
+
+ a.remove(a.get("three"))
+ assert not a.get("three")
+ assert not a.get("four")
+
+
+class D:
+ def __init__(self):
+ self.w = None
+
+ def log(self, x):
+ self.w = x
+
+
+def test_streamlog():
+ dummy = D()
+ s = addonmanager.StreamLog(dummy.log)
+ s.write("foo")
+ assert dummy.w == "foo"
diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py
new file mode 100644
index 00000000..958328b2
--- /dev/null
+++ b/test/mitmproxy/test_command.py
@@ -0,0 +1,165 @@
+import typing
+from mitmproxy import command
+from mitmproxy import flow
+from mitmproxy import exceptions
+from mitmproxy.test import tflow
+from mitmproxy.test import taddons
+import io
+import pytest
+
+
+class TAddon:
+ def cmd1(self, foo: str) -> str:
+ """cmd1 help"""
+ return "ret " + foo
+
+ def cmd2(self, foo: str) -> str:
+ return 99
+
+ def cmd3(self, foo: int) -> int:
+ return foo
+
+ def empty(self) -> None:
+ pass
+
+ def varargs(self, one: str, *var: typing.Sequence[str]) -> typing.Sequence[str]:
+ return list(var)
+
+
+class TestCommand:
+ def test_varargs(self):
+ with taddons.context() as tctx:
+ cm = command.CommandManager(tctx.master)
+ a = TAddon()
+ c = command.Command(cm, "varargs", a.varargs)
+ assert c.signature_help() == "varargs str *str -> [str]"
+ assert c.call(["one", "two", "three"]) == ["two", "three"]
+ with pytest.raises(exceptions.CommandError):
+ c.call(["one", "two", 3])
+
+ def test_call(self):
+ with taddons.context() as tctx:
+ cm = command.CommandManager(tctx.master)
+ a = TAddon()
+ c = command.Command(cm, "cmd.path", a.cmd1)
+ assert c.call(["foo"]) == "ret foo"
+ assert c.signature_help() == "cmd.path str -> str"
+
+ c = command.Command(cm, "cmd.two", a.cmd2)
+ with pytest.raises(exceptions.CommandError):
+ c.call(["foo"])
+
+ c = command.Command(cm, "cmd.three", a.cmd3)
+ assert c.call(["1"]) == 1
+
+
+def test_simple():
+ with taddons.context() as tctx:
+ c = command.CommandManager(tctx.master)
+ a = TAddon()
+ c.add("one.two", a.cmd1)
+ assert c.commands["one.two"].help == "cmd1 help"
+ assert(c.call("one.two foo") == "ret foo")
+ with pytest.raises(exceptions.CommandError, match="Unknown"):
+ c.call("nonexistent")
+ with pytest.raises(exceptions.CommandError, match="Invalid"):
+ c.call("")
+ with pytest.raises(exceptions.CommandError, match="Usage"):
+ c.call("one.two too many args")
+
+ c.add("empty", a.empty)
+ c.call("empty")
+
+ fp = io.StringIO()
+ c.dump(fp)
+ assert fp.getvalue()
+
+
+def test_typename():
+ assert command.typename(str, True) == "str"
+ assert command.typename(typing.Sequence[flow.Flow], True) == "[flow]"
+ assert command.typename(typing.Sequence[flow.Flow], False) == "flowspec"
+
+ assert command.typename(command.Cuts, False) == "cutspec"
+ assert command.typename(command.Cuts, True) == "[cuts]"
+
+ assert command.typename(flow.Flow, False) == "flow"
+ assert command.typename(typing.Sequence[str], False) == "[str]"
+
+
+class DummyConsole:
+ @command.command("view.resolve")
+ def resolve(self, spec: str) -> typing.Sequence[flow.Flow]:
+ n = int(spec)
+ return [tflow.tflow(resp=True)] * n
+
+ @command.command("cut")
+ def cut(self, spec: str) -> command.Cuts:
+ return [["test"]]
+
+
+def test_parsearg():
+ with taddons.context() as tctx:
+ tctx.master.addons.add(DummyConsole())
+ assert command.parsearg(tctx.master.commands, "foo", str) == "foo"
+
+ assert command.parsearg(tctx.master.commands, "1", int) == 1
+ with pytest.raises(exceptions.CommandError):
+ command.parsearg(tctx.master.commands, "foo", int)
+
+ assert command.parsearg(tctx.master.commands, "true", bool) is True
+ assert command.parsearg(tctx.master.commands, "false", bool) is False
+ with pytest.raises(exceptions.CommandError):
+ command.parsearg(tctx.master.commands, "flobble", bool)
+
+ assert len(command.parsearg(
+ tctx.master.commands, "2", typing.Sequence[flow.Flow]
+ )) == 2
+ assert command.parsearg(tctx.master.commands, "1", flow.Flow)
+ with pytest.raises(exceptions.CommandError):
+ command.parsearg(tctx.master.commands, "2", flow.Flow)
+ with pytest.raises(exceptions.CommandError):
+ command.parsearg(tctx.master.commands, "0", flow.Flow)
+ with pytest.raises(exceptions.CommandError):
+ command.parsearg(tctx.master.commands, "foo", Exception)
+
+ assert command.parsearg(
+ tctx.master.commands, "foo", command.Cuts
+ ) == [["test"]]
+
+ assert command.parsearg(
+ tctx.master.commands, "foo", typing.Sequence[str]
+ ) == ["foo"]
+ assert command.parsearg(
+ tctx.master.commands, "foo, bar", typing.Sequence[str]
+ ) == ["foo", "bar"]
+
+
+class TDec:
+ @command.command("cmd1")
+ def cmd1(self, foo: str) -> str:
+ """cmd1 help"""
+ return "ret " + foo
+
+ @command.command("cmd2")
+ def cmd2(self, foo: str) -> str:
+ return 99
+
+ @command.command("empty")
+ def empty(self) -> None:
+ pass
+
+
+def test_decorator():
+ with taddons.context() as tctx:
+ c = command.CommandManager(tctx.master)
+ a = TDec()
+ c.collect_commands(a)
+ assert "cmd1" in c.commands
+ assert c.call("cmd1 bar") == "ret bar"
+ assert "empty" in c.commands
+ assert c.call("empty") is None
+
+ with taddons.context() as tctx:
+ tctx.master.addons.add(a)
+ assert tctx.master.commands.call("cmd1 bar") == "ret bar"
diff --git a/test/mitmproxy/test_connections.py b/test/mitmproxy/test_connections.py
index 67a6552f..e320885d 100644
--- a/test/mitmproxy/test_connections.py
+++ b/test/mitmproxy/test_connections.py
@@ -99,7 +99,7 @@ class TestServerConnection:
c.alpn_proto_negotiated = b'h2'
assert 'address:22' in repr(c)
assert 'ALPN' in repr(c)
- assert 'TLS: foobar' in repr(c)
+ assert 'TLSv1.2: foobar' in repr(c)
c.sni = None
c.tls_established = True
diff --git a/test/mitmproxy/test_controller.py b/test/mitmproxy/test_controller.py
index ccc8bf35..2e13d298 100644
--- a/test/mitmproxy/test_controller.py
+++ b/test/mitmproxy/test_controller.py
@@ -176,6 +176,8 @@ class TestDummyReply:
reply = controller.DummyReply()
reply.ack()
reply.take()
+ with pytest.raises(ControlException):
+ reply.mark_reset()
reply.commit()
reply.mark_reset()
assert reply.state == "committed"
diff --git a/test/mitmproxy/test_examples.py b/test/mitmproxy/test_examples.py
deleted file mode 100644
index 030f2c4e..00000000
--- a/test/mitmproxy/test_examples.py
+++ /dev/null
@@ -1,204 +0,0 @@
-import json
-import shlex
-import pytest
-
-from mitmproxy import options
-from mitmproxy import contentviews
-from mitmproxy import proxy
-from mitmproxy import master
-from mitmproxy.addons import script
-
-from mitmproxy.test import tflow
-from mitmproxy.test import tutils
-from mitmproxy.net.http import Headers
-from mitmproxy.net.http import cookies
-
-from . import tservers
-
-example_dir = tutils.test_data.push("../examples")
-
-
-class ScriptError(Exception):
- pass
-
-
-class RaiseMaster(master.Master):
- def add_log(self, e, level):
- if level in ("warn", "error"):
- raise ScriptError(e)
-
-
-def tscript(cmd, args=""):
- o = options.Options()
- cmd = example_dir.path(cmd) + " " + args
- m = RaiseMaster(o, proxy.DummyServer())
- sc = script.Script(cmd)
- m.addons.add(sc)
- return m, sc
-
-
-class TestScripts(tservers.MasterTest):
- def test_add_header(self):
- m, _ = tscript("simple/add_header.py")
- f = tflow.tflow(resp=tutils.tresp())
- m.addons.handle_lifecycle("response", f)
- assert f.response.headers["newheader"] == "foo"
-
- def test_custom_contentviews(self):
- m, sc = tscript("simple/custom_contentview.py")
- swapcase = contentviews.get("swapcase")
- _, fmt = swapcase(b"<html>Test!</html>")
- assert any(b'tEST!' in val[0][1] for val in fmt)
-
- def test_iframe_injector(self):
- with pytest.raises(ScriptError):
- tscript("simple/modify_body_inject_iframe.py")
-
- m, sc = tscript("simple/modify_body_inject_iframe.py", "http://example.org/evil_iframe")
- f = tflow.tflow(resp=tutils.tresp(content=b"<html><body>mitmproxy</body></html>"))
- m.addons.handle_lifecycle("response", f)
- content = f.response.content
- assert b'iframe' in content and b'evil_iframe' in content
-
- def test_modify_form(self):
- m, sc = tscript("simple/modify_form.py")
-
- form_header = Headers(content_type="application/x-www-form-urlencoded")
- f = tflow.tflow(req=tutils.treq(headers=form_header))
- m.addons.handle_lifecycle("request", f)
-
- assert f.request.urlencoded_form["mitmproxy"] == "rocks"
-
- f.request.headers["content-type"] = ""
- m.addons.handle_lifecycle("request", f)
- assert list(f.request.urlencoded_form.items()) == [("foo", "bar")]
-
- def test_modify_querystring(self):
- m, sc = tscript("simple/modify_querystring.py")
- f = tflow.tflow(req=tutils.treq(path="/search?q=term"))
-
- m.addons.handle_lifecycle("request", f)
- assert f.request.query["mitmproxy"] == "rocks"
-
- f.request.path = "/"
- m.addons.handle_lifecycle("request", f)
- assert f.request.query["mitmproxy"] == "rocks"
-
- def test_arguments(self):
- m, sc = tscript("simple/script_arguments.py", "mitmproxy rocks")
- f = tflow.tflow(resp=tutils.tresp(content=b"I <3 mitmproxy"))
- m.addons.handle_lifecycle("response", f)
- assert f.response.content == b"I <3 rocks"
-
- def test_redirect_requests(self):
- m, sc = tscript("simple/redirect_requests.py")
- f = tflow.tflow(req=tutils.treq(host="example.org"))
- m.addons.handle_lifecycle("request", f)
- assert f.request.host == "mitmproxy.org"
-
- def test_send_reply_from_proxy(self):
- m, sc = tscript("simple/send_reply_from_proxy.py")
- f = tflow.tflow(req=tutils.treq(host="example.com", port=80))
- m.addons.handle_lifecycle("request", f)
- assert f.response.content == b"Hello World"
-
- def test_dns_spoofing(self):
- m, sc = tscript("complex/dns_spoofing.py")
- original_host = "example.com"
-
- host_header = Headers(host=original_host)
- f = tflow.tflow(req=tutils.treq(headers=host_header, port=80))
-
- m.addons.handle_lifecycle("requestheaders", f)
-
- # Rewrite by reverse proxy mode
- f.request.scheme = "https"
- f.request.port = 443
-
- m.addons.handle_lifecycle("request", f)
-
- assert f.request.scheme == "http"
- assert f.request.port == 80
-
- assert f.request.headers["Host"] == original_host
-
-
-class TestHARDump:
-
- def flow(self, resp_content=b'message'):
- times = dict(
- timestamp_start=746203272,
- timestamp_end=746203272,
- )
-
- # Create a dummy flow for testing
- return tflow.tflow(
- req=tutils.treq(method=b'GET', **times),
- resp=tutils.tresp(content=resp_content, **times)
- )
-
- def test_no_file_arg(self):
- with pytest.raises(ScriptError):
- tscript("complex/har_dump.py")
-
- def test_simple(self, tmpdir):
- path = str(tmpdir.join("somefile"))
-
- m, sc = tscript("complex/har_dump.py", shlex.quote(path))
- m.addons.trigger("response", self.flow())
- m.addons.remove(sc)
-
- with open(path, "r") as inp:
- har = json.load(inp)
- assert len(har["log"]["entries"]) == 1
-
- def test_base64(self, tmpdir):
- path = str(tmpdir.join("somefile"))
-
- m, sc = tscript("complex/har_dump.py", shlex.quote(path))
- m.addons.trigger(
- "response", self.flow(resp_content=b"foo" + b"\xFF" * 10)
- )
- m.addons.remove(sc)
-
- with open(path, "r") as inp:
- har = json.load(inp)
- assert har["log"]["entries"][0]["response"]["content"]["encoding"] == "base64"
-
- def test_format_cookies(self):
- m, sc = tscript("complex/har_dump.py", "-")
- format_cookies = sc.ns.format_cookies
-
- CA = cookies.CookieAttrs
-
- f = format_cookies([("n", "v", CA([("k", "v")]))])[0]
- assert f['name'] == "n"
- assert f['value'] == "v"
- assert not f['httpOnly']
- assert not f['secure']
-
- f = format_cookies([("n", "v", CA([("httponly", None), ("secure", None)]))])[0]
- assert f['httpOnly']
- assert f['secure']
-
- f = format_cookies([("n", "v", CA([("expires", "Mon, 24-Aug-2037 00:00:00 GMT")]))])[0]
- assert f['expires']
-
- def test_binary(self, tmpdir):
-
- f = self.flow()
- f.request.method = "POST"
- f.request.headers["content-type"] = "application/x-www-form-urlencoded"
- f.request.content = b"foo=bar&baz=s%c3%bc%c3%9f"
- f.response.headers["random-junk"] = bytes(range(256))
- f.response.content = bytes(range(256))
-
- path = str(tmpdir.join("somefile"))
-
- m, sc = tscript("complex/har_dump.py", shlex.quote(path))
- m.addons.trigger("response", f)
- m.addons.remove(sc)
-
- with open(path, "r") as inp:
- har = json.load(inp)
- assert len(har["log"]["entries"]) == 1
diff --git a/test/mitmproxy/test_export.py b/test/mitmproxy/test_export.py
deleted file mode 100644
index 457d8836..00000000
--- a/test/mitmproxy/test_export.py
+++ /dev/null
@@ -1,106 +0,0 @@
-from mitmproxy.test import tflow
-import re
-
-from mitmproxy.net.http import Headers
-from mitmproxy import export # heh
-from mitmproxy.test import tutils
-
-
-def clean_blanks(s):
- return re.sub(r"^(\s+)$", "", s, flags=re.MULTILINE)
-
-
-def python_equals(testdata, text):
- """
- Compare two bits of Python code, disregarding non-significant differences
- like whitespace on blank lines and trailing space.
- """
- d = open(tutils.test_data.path(testdata)).read()
- assert clean_blanks(text).rstrip() == clean_blanks(d).rstrip()
-
-
-def req_get():
- return tutils.treq(method=b'GET', content=b'', path=b"/path?a=foo&a=bar&b=baz")
-
-
-def req_post():
- return tutils.treq(method=b'POST', headers=())
-
-
-def req_patch():
- return tutils.treq(method=b'PATCH', path=b"/path?query=param")
-
-
-class TestExportCurlCommand:
- def test_get(self):
- flow = tflow.tflow(req=req_get())
- result = """curl -H 'header:qvalue' -H 'content-length:7' 'http://address:22/path?a=foo&a=bar&b=baz'"""
- assert export.curl_command(flow) == result
-
- def test_post(self):
- flow = tflow.tflow(req=req_post())
- result = """curl -X POST 'http://address:22/path' --data-binary 'content'"""
- assert export.curl_command(flow) == result
-
- def test_patch(self):
- flow = tflow.tflow(req=req_patch())
- result = """curl -H 'header:qvalue' -H 'content-length:7' -X PATCH 'http://address:22/path?query=param' --data-binary 'content'"""
- assert export.curl_command(flow) == result
-
-
-class TestExportPythonCode:
- def test_get(self):
- flow = tflow.tflow(req=req_get())
- python_equals("mitmproxy/data/test_flow_export/python_get.py", export.python_code(flow))
-
- def test_post(self):
- flow = tflow.tflow(req=req_post())
- python_equals("mitmproxy/data/test_flow_export/python_post.py", export.python_code(flow))
-
- def test_post_json(self):
- p = req_post()
- p.content = b'{"name": "example", "email": "example@example.com"}'
- p.headers = Headers(content_type="application/json")
- flow = tflow.tflow(req=p)
- python_equals("mitmproxy/data/test_flow_export/python_post_json.py", export.python_code(flow))
-
- def test_patch(self):
- flow = tflow.tflow(req=req_patch())
- python_equals("mitmproxy/data/test_flow_export/python_patch.py", export.python_code(flow))
-
-
-class TestExportLocustCode:
- def test_get(self):
- flow = tflow.tflow(req=req_get())
- python_equals("mitmproxy/data/test_flow_export/locust_get.py", export.locust_code(flow))
-
- def test_post(self):
- p = req_post()
- p.content = b'content'
- p.headers = ''
- flow = tflow.tflow(req=p)
- python_equals("mitmproxy/data/test_flow_export/locust_post.py", export.locust_code(flow))
-
- def test_patch(self):
- flow = tflow.tflow(req=req_patch())
- python_equals("mitmproxy/data/test_flow_export/locust_patch.py", export.locust_code(flow))
-
-
-class TestExportLocustTask:
- def test_get(self):
- flow = tflow.tflow(req=req_get())
- python_equals("mitmproxy/data/test_flow_export/locust_task_get.py", export.locust_task(flow))
-
- def test_post(self):
- flow = tflow.tflow(req=req_post())
- python_equals("mitmproxy/data/test_flow_export/locust_task_post.py", export.locust_task(flow))
-
- def test_patch(self):
- flow = tflow.tflow(req=req_patch())
- python_equals("mitmproxy/data/test_flow_export/locust_task_patch.py", export.locust_task(flow))
-
-
-class TestURL:
- def test_url(self):
- flow = tflow.tflow()
- assert export.url(flow) == "http://address:22/path"
diff --git a/test/mitmproxy/test_flow.py b/test/mitmproxy/test_flow.py
index 630fc7e4..19f0e7d9 100644
--- a/test/mitmproxy/test_flow.py
+++ b/test/mitmproxy/test_flow.py
@@ -6,7 +6,7 @@ import mitmproxy.io
from mitmproxy import flowfilter
from mitmproxy import options
from mitmproxy.proxy import config
-from mitmproxy.contrib import tnetstring
+from mitmproxy.io import tnetstring
from mitmproxy.exceptions import FlowReadException
from mitmproxy import flow
from mitmproxy import http
@@ -113,10 +113,6 @@ class TestFlowMaster:
with pytest.raises(Exception, match="live"):
fm.replay_request(f)
- def test_create_flow(self):
- fm = master.Master(None, DummyServer())
- assert fm.create_request("GET", "http://example.com/")
-
def test_all(self):
s = tservers.TestState()
fm = master.Master(None, DummyServer())
diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py
index df392829..04ec7ded 100644
--- a/test/mitmproxy/test_optmanager.py
+++ b/test/mitmproxy/test_optmanager.py
@@ -14,6 +14,7 @@ class TO(optmanager.OptManager):
self.add_option("one", typing.Optional[int], None, "help")
self.add_option("two", typing.Optional[int], 2, "help")
self.add_option("bool", bool, False, "help")
+ self.add_option("required_int", int, 2, "help")
class TD(optmanager.OptManager):
@@ -37,12 +38,6 @@ class TM(optmanager.OptManager):
self.add_option("one", typing.Optional[str], None, "help")
-def test_add_option():
- o = TO()
- with pytest.raises(ValueError, match="already exists"):
- o.add_option("one", typing.Optional[int], None, "help")
-
-
def test_defaults():
o = TD2()
defaults = {
@@ -72,9 +67,15 @@ def test_defaults():
assert not o.has_changed(k)
+def test_required_int():
+ o = TO()
+ with pytest.raises(exceptions.OptionsError):
+ o.parse_setval("required_int", None)
+
+
def test_options():
o = TO()
- assert o.keys() == {"bool", "one", "two"}
+ assert o.keys() == {"bool", "one", "two", "required_int"}
assert o.one is None
assert o.two == 2
@@ -140,6 +141,18 @@ class Rec():
def test_subscribe():
o = TO()
r = Rec()
+
+ # pytest.raises keeps a reference here that interferes with the cleanup test
+ # further down.
+ try:
+ o.subscribe(r, ["unknown"])
+ except exceptions.OptionsError:
+ pass
+ else:
+ raise AssertionError
+
+ assert len(o.changed.receivers) == 0
+
o.subscribe(r, ["two"])
o.one = 2
assert not r.called
@@ -151,6 +164,21 @@ def test_subscribe():
o.two = 4
assert len(o.changed.receivers) == 0
+ class binder:
+ def __init__(self):
+ self.o = TO()
+ self.called = False
+ self.o.subscribe(self.bound, ["two"])
+
+ def bound(self, *args, **kwargs):
+ self.called = True
+
+ t = binder()
+ t.o.one = 3
+ assert not t.called
+ t.o.two = 3
+ assert t.called
+
def test_rollback():
o = TO()
@@ -176,8 +204,12 @@ def test_rollback():
o.errored.connect(errsub)
assert o.one is None
- o.one = 10
- o.bool = True
+ with pytest.raises(exceptions.OptionsError):
+ o.one = 10
+ assert o.one is None
+ with pytest.raises(exceptions.OptionsError):
+ o.bool = True
+ assert o.bool is False
assert isinstance(recerr[0]["exc"], exceptions.OptionsError)
assert o.one is None
assert o.bool is False
@@ -258,6 +290,20 @@ def test_saving(tmpdir):
with pytest.raises(exceptions.OptionsError):
optmanager.load_paths(o, dst)
+ with open(dst, 'wb') as f:
+ f.write(b"\x01\x02\x03")
+ with pytest.raises(exceptions.OptionsError):
+ optmanager.load_paths(o, dst)
+ with pytest.raises(exceptions.OptionsError):
+ optmanager.save(o, dst)
+
+ with open(dst, 'wb') as f:
+ f.write(b"\xff\xff\xff")
+ with pytest.raises(exceptions.OptionsError):
+ optmanager.load_paths(o, dst)
+ with pytest.raises(exceptions.OptionsError):
+ optmanager.save(o, dst)
+
def test_merge():
m = TM()
@@ -270,14 +316,14 @@ def test_merge():
def test_option():
- o = optmanager._Option("test", int, 1, None, None)
+ o = optmanager._Option("test", int, 1, "help", None)
assert o.current() == 1
with pytest.raises(TypeError):
o.set("foo")
with pytest.raises(TypeError):
- optmanager._Option("test", str, 1, None, None)
+ optmanager._Option("test", str, 1, "help", None)
- o2 = optmanager._Option("test", int, 1, None, None)
+ o2 = optmanager._Option("test", int, 1, "help", None)
assert o2 == o
o2.set(5)
assert o2 != o
@@ -335,6 +381,11 @@ def test_set():
with pytest.raises(exceptions.OptionsError):
opts.set("bool=wobble")
+ opts.set("bool=toggle")
+ assert opts.bool is False
+ opts.set("bool=toggle")
+ assert opts.bool is True
+
opts.set("int=1")
assert opts.int == 1
with pytest.raises(exceptions.OptionsError):
diff --git a/test/mitmproxy/test_proxy.py b/test/mitmproxy/test_proxy.py
index 7a49c530..e1d0da00 100644
--- a/test/mitmproxy/test_proxy.py
+++ b/test/mitmproxy/test_proxy.py
@@ -3,7 +3,6 @@ from unittest import mock
from OpenSSL import SSL
import pytest
-
from mitmproxy.tools import cmdline
from mitmproxy.tools import main
from mitmproxy import options
diff --git a/test/mitmproxy/test_taddons.py b/test/mitmproxy/test_taddons.py
index 42371cfe..5a4c99fc 100644
--- a/test/mitmproxy/test_taddons.py
+++ b/test/mitmproxy/test_taddons.py
@@ -1,4 +1,6 @@
+import io
from mitmproxy.test import taddons
+from mitmproxy.test import tutils
from mitmproxy import ctx
@@ -9,3 +11,21 @@ def test_recordingmaster():
ctx.log.error("foo")
assert not tctx.master.has_log("foo", level="debug")
assert tctx.master.has_log("foo", level="error")
+
+
+def test_dumplog():
+ with taddons.context() as tctx:
+ ctx.log.info("testing")
+ s = io.StringIO()
+ tctx.master.dump_log(s)
+ assert s.getvalue()
+
+
+def test_load_script():
+ with taddons.context() as tctx:
+ s = tctx.script(
+ tutils.test_data.path(
+ "mitmproxy/data/addonscripts/recorder/recorder.py"
+ )
+ )
+ assert s
diff --git a/test/mitmproxy/test_websocket.py b/test/mitmproxy/test_websocket.py
index 62f69e2d..7c53a4b0 100644
--- a/test/mitmproxy/test_websocket.py
+++ b/test/mitmproxy/test_websocket.py
@@ -1,7 +1,7 @@
import io
import pytest
-from mitmproxy.contrib import tnetstring
+from mitmproxy.io import tnetstring
from mitmproxy import flowfilter
from mitmproxy.test import tflow
diff --git a/test/mitmproxy/tools/console/test_help.py b/test/mitmproxy/tools/console/test_help.py
index ac3011e6..0ebc2d6a 100644
--- a/test/mitmproxy/tools/console/test_help.py
+++ b/test/mitmproxy/tools/console/test_help.py
@@ -9,9 +9,3 @@ class TestHelp:
def test_helptext(self):
h = help.HelpView(None)
assert h.helptext()
-
- def test_keypress(self):
- h = help.HelpView([1, 2, 3])
- assert not h.keypress((0, 0), "q")
- assert not h.keypress((0, 0), "?")
- assert h.keypress((0, 0), "o") == "o"
diff --git a/test/mitmproxy/tools/console/test_keymap.py b/test/mitmproxy/tools/console/test_keymap.py
new file mode 100644
index 00000000..6a75800e
--- /dev/null
+++ b/test/mitmproxy/tools/console/test_keymap.py
@@ -0,0 +1,29 @@
+from mitmproxy.tools.console import keymap
+from mitmproxy.test import taddons
+from unittest import mock
+import pytest
+
+
+def test_bind():
+ with taddons.context() as tctx:
+ km = keymap.Keymap(tctx.master)
+ km.executor = mock.Mock()
+
+ with pytest.raises(ValueError):
+ km.add("foo", "bar", ["unsupported"])
+
+ km.add("key", "str", ["options", "commands"])
+ assert km.get("options", "key")
+ assert km.get("commands", "key")
+ assert not km.get("flowlist", "key")
+
+ km.handle("unknown", "unknown")
+ assert not km.executor.called
+
+ km.handle("options", "key")
+ assert km.executor.called
+
+ km.add("glob", "str", ["global"])
+ km.executor = mock.Mock()
+ km.handle("options", "glob")
+ assert km.executor.called
diff --git a/test/mitmproxy/tools/console/test_master.py b/test/mitmproxy/tools/console/test_master.py
index 44b9ff3f..c87c9e83 100644
--- a/test/mitmproxy/tools/console/test_master.py
+++ b/test/mitmproxy/tools/console/test_master.py
@@ -30,7 +30,7 @@ class TestMaster(tservers.MasterTest):
opts["verbosity"] = 1
o = options.Options(**opts)
m = console.master.ConsoleMaster(o, proxy.DummyServer())
- m.addons.configure_all(o, o.keys())
+ m.addons.trigger("configure", o.keys())
return m
def test_basic(self):
@@ -42,12 +42,6 @@ class TestMaster(tservers.MasterTest):
pass
assert len(m.view) == i
- def test_run_script_once(self):
- m = self.mkmaster()
- f = tflow.tflow(resp=True)
- m.run_script_once("nonexistent", [f])
- assert any("Input error" in str(l) for l in m.logbuffer)
-
def test_intercept(self):
"""regression test for https://github.com/mitmproxy/mitmproxy/issues/1605"""
m = self.mkmaster(intercept="~b bar")
diff --git a/test/mitmproxy/tools/console/test_pathedit.py b/test/mitmproxy/tools/console/test_pathedit.py
index bd064e5f..b9f51f5a 100644
--- a/test/mitmproxy/tools/console/test_pathedit.py
+++ b/test/mitmproxy/tools/console/test_pathedit.py
@@ -1,10 +1,10 @@
import os
from os.path import normpath
+from unittest import mock
+
from mitmproxy.tools.console import pathedit
from mitmproxy.test import tutils
-from unittest.mock import patch
-
class TestPathCompleter:
@@ -56,8 +56,8 @@ class TestPathEdit:
pe = pathedit.PathEdit("", "")
- with patch('urwid.widget.Edit.get_edit_text') as get_text, \
- patch('urwid.widget.Edit.set_edit_text') as set_text:
+ with mock.patch('urwid.widget.Edit.get_edit_text') as get_text, \
+ mock.patch('urwid.widget.Edit.set_edit_text') as set_text:
cd = os.path.normpath(tutils.test_data.path("mitmproxy/completion"))
get_text.return_value = os.path.join(cd, "a")
diff --git a/test/mitmproxy/tools/test_dump.py b/test/mitmproxy/tools/test_dump.py
index 8e2fa5b2..69a76d2e 100644
--- a/test/mitmproxy/tools/test_dump.py
+++ b/test/mitmproxy/tools/test_dump.py
@@ -12,7 +12,7 @@ from .. import tservers
class TestDumpMaster(tservers.MasterTest):
def mkmaster(self, flt, **opts):
- o = options.Options(filtstr=flt, verbosity=-1, flow_detail=0, **opts)
+ o = options.Options(view_filter=flt, verbosity=-1, flow_detail=0, **opts)
m = dump.DumpMaster(o, proxy.DummyServer(), with_termlog=False, with_dumper=False)
return m
diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py
index e3d5dc44..5427b995 100644
--- a/test/mitmproxy/tools/web/test_app.py
+++ b/test/mitmproxy/tools/web/test_app.py
@@ -1,5 +1,6 @@
import json as _json
from unittest import mock
+import os
import tornado.testing
from tornado import httpclient
@@ -23,8 +24,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
m = webmaster.WebMaster(o, proxy.DummyServer(), with_termlog=False)
f = tflow.tflow(resp=True)
f.id = "42"
- m.view.add(f)
- m.view.add(tflow.tflow(err=True))
+ m.view.add([f])
+ m.view.add([tflow.tflow(err=True)])
m.add_log("test log", "info")
self.master = m
self.view = m.view
@@ -78,7 +79,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
# restore
for f in flows:
- self.view.add(f)
+ self.view.add([f])
self.events.data = events
def test_resume(self):
@@ -110,7 +111,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
assert self.fetch("/flows/42", method="DELETE").code == 200
assert not self.view.get_by_id("42")
- self.view.add(f)
+ self.view.add([f])
assert self.fetch("/flows/1234", method="DELETE").code == 404
@@ -162,7 +163,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
f = self.view.get_by_id(resp.body.decode())
assert f
assert f.id != "42"
- self.view.remove(f)
+ self.view.remove([f])
def test_flow_revert(self):
f = self.view.get_by_id("42")
@@ -275,3 +276,20 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
# trigger on_close by opening a second connection.
ws_client2 = yield websocket.websocket_connect(ws_url)
ws_client2.close()
+
+ def test_generate_tflow_js(self):
+ _tflow = app.flow_to_json(tflow.tflow(resp=True, err=True))
+ # Set some value as constant, so that _tflow.js would not change every time.
+ _tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"
+ _tflow['id'] = "d91165be-ca1f-4612-88a9-c0f8696f3e29"
+ _tflow['error']['timestamp'] = 1495370312.4814785
+ _tflow['response']['timestamp_end'] = 1495370312.4814625
+ _tflow['response']['timestamp_start'] = 1495370312.481462
+ _tflow['server_conn']['id'] = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8"
+ tflow_json = _json.dumps(_tflow, indent=4, sort_keys=True)
+ here = os.path.abspath(os.path.dirname(__file__))
+ web_root = os.path.join(here, os.pardir, os.pardir, os.pardir, os.pardir, 'web')
+ tflow_path = os.path.join(web_root, 'src/js/__tests__/ducks/_tflow.js')
+ content = """export default function(){{\n return {tflow_json}\n}}""".format(tflow_json=tflow_json)
+ with open(tflow_path, 'w') as f:
+ f.write(content)
diff --git a/test/mitmproxy/tservers.py b/test/mitmproxy/tservers.py
index b737b82a..b8005529 100644
--- a/test/mitmproxy/tservers.py
+++ b/test/mitmproxy/tservers.py
@@ -74,7 +74,7 @@ class TestMaster(taddons.RecordingMaster):
self.state = TestState()
self.addons.add(self.state)
self.addons.add(*addons)
- self.addons.configure_all(self.options, self.options.keys())
+ self.addons.trigger("configure", self.options.keys())
self.addons.trigger("running")
def reset(self, addons):
diff --git a/test/mitmproxy/utils/test_human.py b/test/mitmproxy/utils/test_human.py
index 3d65dfd1..76dc2f88 100644
--- a/test/mitmproxy/utils/test_human.py
+++ b/test/mitmproxy/utils/test_human.py
@@ -46,3 +46,10 @@ def test_pretty_duration():
assert human.pretty_duration(10000) == "10000s"
assert human.pretty_duration(1.123) == "1.12s"
assert human.pretty_duration(0.123) == "123ms"
+
+
+def test_format_address():
+ assert human.format_address(("::1", "54010", "0", "0")) == "[::1]:54010"
+ assert human.format_address(("::ffff:127.0.0.1", "54010", "0", "0")) == "127.0.0.1:54010"
+ assert human.format_address(("127.0.0.1", "54010")) == "127.0.0.1:54010"
+ assert human.format_address(("example.com", "54010")) == "example.com:54010"
diff --git a/test/mitmproxy/utils/test_typecheck.py b/test/mitmproxy/utils/test_typecheck.py
index d99a914f..fe33070e 100644
--- a/test/mitmproxy/utils/test_typecheck.py
+++ b/test/mitmproxy/utils/test_typecheck.py
@@ -4,6 +4,7 @@ from unittest import mock
import pytest
from mitmproxy.utils import typecheck
+from mitmproxy import command
class TBase:
@@ -16,66 +17,97 @@ class T(TBase):
super(T, self).__init__(42)
-def test_check_type():
- typecheck.check_type("foo", 42, int)
+def test_check_option_type():
+ typecheck.check_option_type("foo", 42, int)
with pytest.raises(TypeError):
- typecheck.check_type("foo", 42, str)
+ typecheck.check_option_type("foo", 42, str)
with pytest.raises(TypeError):
- typecheck.check_type("foo", None, str)
+ typecheck.check_option_type("foo", None, str)
with pytest.raises(TypeError):
- typecheck.check_type("foo", b"foo", str)
+ typecheck.check_option_type("foo", b"foo", str)
def test_check_union():
- typecheck.check_type("foo", 42, typing.Union[int, str])
- typecheck.check_type("foo", "42", typing.Union[int, str])
+ typecheck.check_option_type("foo", 42, typing.Union[int, str])
+ typecheck.check_option_type("foo", "42", typing.Union[int, str])
with pytest.raises(TypeError):
- typecheck.check_type("foo", [], typing.Union[int, str])
+ typecheck.check_option_type("foo", [], typing.Union[int, str])
# Python 3.5 only defines __union_params__
m = mock.Mock()
m.__str__ = lambda self: "typing.Union"
m.__union_params__ = (int,)
- typecheck.check_type("foo", 42, m)
+ typecheck.check_option_type("foo", 42, m)
def test_check_tuple():
- typecheck.check_type("foo", (42, "42"), typing.Tuple[int, str])
+ typecheck.check_option_type("foo", (42, "42"), typing.Tuple[int, str])
with pytest.raises(TypeError):
- typecheck.check_type("foo", None, typing.Tuple[int, str])
+ typecheck.check_option_type("foo", None, typing.Tuple[int, str])
with pytest.raises(TypeError):
- typecheck.check_type("foo", (), typing.Tuple[int, str])
+ typecheck.check_option_type("foo", (), typing.Tuple[int, str])
with pytest.raises(TypeError):
- typecheck.check_type("foo", (42, 42), typing.Tuple[int, str])
+ typecheck.check_option_type("foo", (42, 42), typing.Tuple[int, str])
with pytest.raises(TypeError):
- typecheck.check_type("foo", ("42", 42), typing.Tuple[int, str])
+ typecheck.check_option_type("foo", ("42", 42), typing.Tuple[int, str])
# Python 3.5 only defines __tuple_params__
m = mock.Mock()
m.__str__ = lambda self: "typing.Tuple"
m.__tuple_params__ = (int, str)
- typecheck.check_type("foo", (42, "42"), m)
+ typecheck.check_option_type("foo", (42, "42"), m)
def test_check_sequence():
- typecheck.check_type("foo", [10], typing.Sequence[int])
+ typecheck.check_option_type("foo", [10], typing.Sequence[int])
with pytest.raises(TypeError):
- typecheck.check_type("foo", ["foo"], typing.Sequence[int])
+ typecheck.check_option_type("foo", ["foo"], typing.Sequence[int])
with pytest.raises(TypeError):
- typecheck.check_type("foo", [10, "foo"], typing.Sequence[int])
+ typecheck.check_option_type("foo", [10, "foo"], typing.Sequence[int])
with pytest.raises(TypeError):
- typecheck.check_type("foo", [b"foo"], typing.Sequence[str])
+ typecheck.check_option_type("foo", [b"foo"], typing.Sequence[str])
with pytest.raises(TypeError):
- typecheck.check_type("foo", "foo", typing.Sequence[str])
+ typecheck.check_option_type("foo", "foo", typing.Sequence[str])
# Python 3.5 only defines __parameters__
m = mock.Mock()
m.__str__ = lambda self: "typing.Sequence"
m.__parameters__ = (int,)
- typecheck.check_type("foo", [10], m)
+ typecheck.check_option_type("foo", [10], m)
def test_check_io():
- typecheck.check_type("foo", io.StringIO(), typing.IO[str])
+ typecheck.check_option_type("foo", io.StringIO(), typing.IO[str])
with pytest.raises(TypeError):
- typecheck.check_type("foo", "foo", typing.IO[str])
+ typecheck.check_option_type("foo", "foo", typing.IO[str])
+
+
+def test_check_any():
+ typecheck.check_option_type("foo", 42, typing.Any)
+ typecheck.check_option_type("foo", object(), typing.Any)
+ typecheck.check_option_type("foo", None, typing.Any)
+
+
+def test_check_command_type():
+ assert(typecheck.check_command_type("foo", str))
+ assert(typecheck.check_command_type(["foo"], typing.Sequence[str]))
+ assert(not typecheck.check_command_type(["foo", 1], typing.Sequence[str]))
+ assert(typecheck.check_command_type(None, None))
+ assert(not typecheck.check_command_type(["foo"], typing.Sequence[int]))
+ assert(not typecheck.check_command_type("foo", typing.Sequence[int]))
+ assert(typecheck.check_command_type([["foo", b"bar"]], command.Cuts))
+ assert(not typecheck.check_command_type(["foo", b"bar"], command.Cuts))
+ assert(not typecheck.check_command_type([["foo", 22]], command.Cuts))
+
+ # Python 3.5 only defines __parameters__
+ m = mock.Mock()
+ m.__str__ = lambda self: "typing.Sequence"
+ m.__parameters__ = (int,)
+
+ typecheck.check_command_type([10], m)
+
+ # Python 3.5 only defines __union_params__
+ m = mock.Mock()
+ m.__str__ = lambda self: "typing.Union"
+ m.__union_params__ = (int,)
+ assert not typecheck.check_command_type([22], m)
diff --git a/tox.ini b/tox.ini
index a1ed53f7..c5e2d5fc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,7 @@ toxworkdir={env:TOX_WORK_DIR:.tox}
deps =
{env:CI_DEPS:}
-rrequirements.txt
-passenv = CODECOV_TOKEN CI CI_* TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* SNAPSHOT_* OPENSSL_* RTOOL_*
+passenv = CODECOV_TOKEN CI CI_* TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* SNAPSHOT_* OPENSSL RTOOL_*
setenv = HOME = {envtmpdir}
commands =
mitmdump --version
@@ -27,17 +27,9 @@ commands =
flake8 --jobs 8 mitmproxy pathod examples test release
python3 test/filename_matching.py
rstcheck README.rst
- mypy --ignore-missing-imports --follow-imports=skip \
- mitmproxy/addons/ \
- mitmproxy/addonmanager.py \
- mitmproxy/optmanager.py \
- mitmproxy/proxy/protocol/ \
- mitmproxy/log.py \
- mitmproxy/tools/dump.py \
- mitmproxy/tools/web/ \
- mitmproxy/contentviews/
- mypy --ignore-missing-imports \
- mitmproxy/master.py
+ mypy --ignore-missing-imports ./mitmproxy
+ mypy --ignore-missing-imports ./pathod
+ mypy --ignore-missing-imports --follow-imports=skip ./examples/simple/
[testenv:individual_coverage]
deps =
diff --git a/web/package.json b/web/package.json
index 601d7077..94b0ee60 100644
--- a/web/package.json
+++ b/web/package.json
@@ -2,29 +2,39 @@
"name": "mitmproxy",
"private": true,
"scripts": {
- "test": "jest",
+ "test": "jest --coverage",
"build": "gulp prod",
"start": "gulp"
},
"jest": {
"testRegex": "__tests__/.*Spec.js$",
- "testPathDirs": [
+ "roots": [
"<rootDir>/src/js"
],
"unmockedModulePathPatterns": [
"react"
+ ],
+ "coverageDirectory": "./coverage",
+ "coveragePathIgnorePatterns": [
+ "<rootDir>/src/js/filt/filt.js"
+ ],
+ "collectCoverageFrom": [
+ "src/js/**/*.{js,jsx}"
]
},
"dependencies": {
"bootstrap": "^3.3.7",
"classnames": "^2.2.5",
"lodash": "^4.17.4",
+ "prop-types": "^15.5.0",
"react": "^15.4.2",
"react-codemirror": "^0.3.0",
"react-dom": "^15.4.2",
"react-redux": "^5.0.2",
+ "react-test-renderer": "^15.5.4",
"redux": "^3.6.0",
"redux-logger": "^2.8.1",
+ "redux-mock-store": "^1.2.3",
"redux-thunk": "^2.2.0",
"shallowequal": "^0.2.2"
},
diff --git a/web/src/css/header.less b/web/src/css/header.less
index aa9abc76..55fc59d0 100644
--- a/web/src/css/header.less
+++ b/web/src/css/header.less
@@ -1,5 +1,7 @@
@import (reference) '../../node_modules/bootstrap/less/variables.less';
@import (reference) '../../node_modules/bootstrap/less/mixins/grid.less';
+@import (reference) "../../node_modules/bootstrap/less/mixins/labels.less";
+@import (reference) "../../node_modules/bootstrap/less/labels.less";
@menu-height: 85px;
@@ -7,7 +9,7 @@ header {
padding-top: 6px;
background-color: white;
@separator-color: lighten(grey, 15%);
- menu {
+ > div {
display: block;
margin: 0;
padding: 0;
@@ -45,7 +47,6 @@ header {
}
}
-
.menu-entry {
text-align: left;
height: (@menu-height - @menu-legend-height)/3;
@@ -63,7 +64,6 @@ header {
}
}
-
.menu-legend {
height: @menu-legend-height;
text-align: center;
@@ -130,3 +130,27 @@ header {
}
}
}
+
+.connection-indicator {
+ .label();
+ float: right;
+ margin: 5px;
+ opacity: 1;
+ transition: all 1s linear;
+
+ &.init, &.fetching {
+ background-color: @label-info-bg;
+ }
+ &.established {
+ background-color: @label-success-bg;
+ opacity: 0;
+ }
+ &.error {
+ background-color: @label-danger-bg;
+ transition: all 0.2s linear;
+ }
+ &.offline {
+ background-color: @label-warning-bg;
+ opacity: 1;
+ }
+}
diff --git a/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js
new file mode 100644
index 00000000..f3373c02
--- /dev/null
+++ b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js
@@ -0,0 +1,108 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import * as Columns from '../../../components/FlowTable/FlowColumns'
+import { TFlow } from '../../ducks/tutils'
+
+describe('FlowColumns Components', () => {
+
+ let tflow = TFlow()
+ it('should render TLSColumn', () => {
+ let tlsColumn = renderer.create(<Columns.TLSColumn flow={tflow}/>),
+ tree = tlsColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render IconColumn', () => {
+ let iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>),
+ tree = iconColumn.toJSON()
+ // plain
+ expect(tree).toMatchSnapshot()
+ // not modified
+ tflow.response.status_code = 304
+ iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // redirect
+ tflow.response.status_code = 302
+ iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // image
+ let imageFlow = TFlow()
+ imageFlow.response.headers = [['Content-Type', 'image/jpeg']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={imageFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // javascript
+ let jsFlow = TFlow()
+ jsFlow.response.headers = [['Content-Type', 'application/x-javascript']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={jsFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // css
+ let cssFlow = TFlow()
+ cssFlow.response.headers = [['Content-Type', 'text/css']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={cssFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // html
+ let htmlFlow = TFlow()
+ htmlFlow.response.headers = [['Content-Type', 'text/html']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={htmlFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // default
+ let fooFlow = TFlow()
+ fooFlow.response.headers = [['Content-Type', 'foo']]
+ iconColumn = renderer.create(<Columns.IconColumn flow={fooFlow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ // no response
+ tflow.response = null
+ iconColumn = renderer.create(<Columns.IconColumn flow={tflow}/>)
+ tree = iconColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render pathColumn', () => {
+ let pathColumn = renderer.create(<Columns.PathColumn flow={tflow}/>),
+ tree = pathColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ tflow.error.msg = 'Connection killed'
+ tflow.intercepted = true
+ pathColumn = renderer.create(<Columns.PathColumn flow={tflow}/>)
+ tree = pathColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render MethodColumn', () => {
+ let methodColumn =renderer.create(<Columns.MethodColumn flow={tflow}/>),
+ tree = methodColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render StatusColumn', () => {
+ let statusColumn = renderer.create(<Columns.StatusColumn flow={tflow}/>),
+ tree = statusColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render SizeColumn', () => {
+ tflow = TFlow()
+ let sizeColumn = renderer.create(<Columns.SizeColumn flow={tflow}/>),
+ tree = sizeColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should render TimeColumn', () => {
+ let timeColumn = renderer.create(<Columns.TimeColumn flow={tflow}/>),
+ tree = timeColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ tflow.response = null
+ timeColumn = renderer.create(<Columns.TimeColumn flow={tflow}/>),
+ tree = timeColumn.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap
new file mode 100644
index 00000000..d6946507
--- /dev/null
+++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.js.snap
@@ -0,0 +1,160 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FlowColumns Components should render IconColumn 1`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-plain"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 2`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-not-modified"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 3`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-redirect"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 4`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-image"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 5`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-js"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 6`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-css"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 7`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-document"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 8`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-plain"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render IconColumn 9`] = `
+<td
+ className="col-icon"
+>
+ <div
+ className="resource-icon resource-icon-plain"
+ />
+</td>
+`;
+
+exports[`FlowColumns Components should render MethodColumn 1`] = `
+<td
+ className="col-method"
+>
+ GET
+</td>
+`;
+
+exports[`FlowColumns Components should render SizeColumn 1`] = `
+<td
+ className="col-size"
+>
+ 14b
+</td>
+`;
+
+exports[`FlowColumns Components should render StatusColumn 1`] = `
+<td
+ className="col-status"
+/>
+`;
+
+exports[`FlowColumns Components should render TLSColumn 1`] = `
+<td
+ className="col-tls col-tls-http"
+/>
+`;
+
+exports[`FlowColumns Components should render TimeColumn 1`] = `
+<td
+ className="col-time"
+>
+ 415381h
+</td>
+`;
+
+exports[`FlowColumns Components should render TimeColumn 2`] = `
+<td
+ className="col-time"
+>
+ ...
+</td>
+`;
+
+exports[`FlowColumns Components should render pathColumn 1`] = `
+<td
+ className="col-path"
+>
+ <i
+ className="fa fa-fw fa-exclamation pull-right"
+ />
+ http://address:22/path
+</td>
+`;
+
+exports[`FlowColumns Components should render pathColumn 2`] = `
+<td
+ className="col-path"
+>
+ <i
+ className="fa fa-fw fa-pause pull-right"
+ />
+ <i
+ className="fa fa-fw fa-times pull-right"
+ />
+ http://address:22/path
+</td>
+`;
diff --git a/web/src/js/__tests__/components/ValueEditor/ValidateEditorSpec.js b/web/src/js/__tests__/components/ValueEditor/ValidateEditorSpec.js
new file mode 100644
index 00000000..32dabe59
--- /dev/null
+++ b/web/src/js/__tests__/components/ValueEditor/ValidateEditorSpec.js
@@ -0,0 +1,47 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import TestUtils from 'react-dom/test-utils'
+import ValidateEditor from '../../../components/ValueEditor/ValidateEditor'
+
+describe('ValidateEditor Component', () => {
+ let validateFn = jest.fn( content => content.length == 3),
+ doneFn = jest.fn()
+
+ it('should render correctly', () => {
+ let validateEditor = renderer.create(
+ <ValidateEditor content="foo" onDone={doneFn} isValid={validateFn}/>
+ ),
+ tree = validateEditor.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ let validateEditor = TestUtils.renderIntoDocument(
+ <ValidateEditor content="foo" onDone={doneFn} isValid={validateFn}/>
+ )
+ it('should handle componentWillReceiveProps', () => {
+ let mockProps = {
+ isValid: s => s.length == 3,
+ content: "bar"
+ }
+ validateEditor.componentWillReceiveProps(mockProps)
+ expect(validateEditor.state.valid).toBeTruthy()
+ validateEditor.componentWillReceiveProps({...mockProps, content: "bars"})
+ expect(validateEditor.state.valid).toBeFalsy()
+
+ })
+
+ it('should handle input', () => {
+ validateEditor.onInput("foo bar")
+ expect(validateFn).toBeCalledWith("foo bar")
+ })
+
+ it('should handle done', () => {
+ // invalid
+ validateEditor.editor.reset = jest.fn()
+ validateEditor.onDone("foo bar")
+ expect(validateEditor.editor.reset).toBeCalled()
+ // valid
+ validateEditor.onDone("bar")
+ expect(doneFn).toBeCalledWith("bar")
+ })
+})
diff --git a/web/src/js/__tests__/components/ValueEditor/ValueEditorSpec.js b/web/src/js/__tests__/components/ValueEditor/ValueEditorSpec.js
new file mode 100644
index 00000000..f94a6acc
--- /dev/null
+++ b/web/src/js/__tests__/components/ValueEditor/ValueEditorSpec.js
@@ -0,0 +1,155 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import TestUtils from 'react-dom/test-utils'
+import ValueEditor from '../../../components/ValueEditor/ValueEditor'
+import { Key } from '../../../utils'
+
+describe('ValueEditor Component', () => {
+
+ let mockFn = jest.fn()
+ it ('should render correctly', () => {
+ let valueEditor = renderer.create(
+ <ValueEditor content="foo" onDone={mockFn}/>
+ ),
+ tree = valueEditor.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ let valueEditor = TestUtils.renderIntoDocument(
+ <ValueEditor content="<script>foo</script>" onDone={mockFn}/>
+ )
+ it('should handle this.blur', () => {
+ valueEditor.input.blur = jest.fn()
+ valueEditor.blur()
+ expect(valueEditor.input.blur).toHaveBeenCalled()
+ })
+
+ it('should handle reset', () => {
+ valueEditor.reset()
+ expect(valueEditor.input.innerHTML).toEqual(
+ "&lt;script&gt;foo&lt;/script&gt;"
+ )
+ })
+
+ it('should handle paste', () => {
+ let mockEvent = {
+ preventDefault: jest.fn(),
+ clipboardData: { getData: (t) => "foo content"}
+ }
+ document.execCommand = jest.fn()
+ valueEditor.onPaste(mockEvent)
+ expect(document.execCommand).toBeCalledWith('insertHTML', false, "foo content")
+ })
+
+ it('should handle mouseDown', () => {
+ window.addEventListener = jest.fn()
+ valueEditor.onMouseDown({})
+ expect(valueEditor._mouseDown).toBeTruthy()
+ expect(window.addEventListener).toBeCalledWith('mouseup', valueEditor.onMouseUp)
+ })
+
+ it('should handle mouseUp', () => {
+ window.removeEventListener = jest.fn()
+ valueEditor.onMouseUp()
+ expect(window.removeEventListener).toBeCalledWith('mouseup', valueEditor.onMouseUp)
+ })
+
+ it('should handle focus', () => {
+ let mockEvent = { clientX: 1, clientY: 2 },
+ mockSelection = {
+ rangeCount: 1,
+ getRangeAt: jest.fn( (index) => {return { selectNodeContents: jest.fn() }}),
+ removeAllRanges: jest.fn(),
+ addRange: jest.fn()
+ },
+ clearState = (v) => {
+ v._mouseDown = false
+ v._ignore_events = false
+ v.state.editable = false
+ }
+ window.getSelection = () => mockSelection
+
+ // return undefined when mouse down
+ valueEditor.onMouseDown()
+ expect(valueEditor.onFocus(mockEvent)).toEqual(undefined)
+ valueEditor.onMouseUp()
+
+ // sel.rangeCount > 0
+ valueEditor.onFocus(mockEvent)
+ expect(mockSelection.getRangeAt).toBeCalledWith(0)
+ expect(valueEditor.state.editable).toBeTruthy()
+ expect(mockSelection.removeAllRanges).toBeCalled()
+ expect(mockSelection.addRange).toBeCalled()
+ clearState(valueEditor)
+
+ // document.caretPositionFromPoint
+ mockSelection.rangeCount = 0
+ let mockRange = { setStart: jest.fn(), selectNodeContents: jest.fn() }
+
+ document.caretPositionFromPoint = jest.fn((x, y) => {
+ return { offsetNode: 0, offset: x + y}
+ })
+ document.createRange = jest.fn(() => mockRange)
+ valueEditor.onFocus(mockEvent)
+ expect(mockRange.setStart).toBeCalledWith(0, 3)
+ clearState(valueEditor)
+ document.caretPositionFromPoint = null
+
+ //document.caretRangeFromPoint
+ document.caretRangeFromPoint = jest.fn(() => mockRange)
+ valueEditor.onFocus(mockEvent)
+ expect(document.caretRangeFromPoint).toBeCalledWith(1, 2)
+ clearState(valueEditor)
+ document.caretRangeFromPoint = null
+
+ //else
+ valueEditor.onFocus(mockEvent)
+ expect(mockRange.selectNodeContents).toBeCalledWith(valueEditor.input)
+ clearState(valueEditor)
+ })
+
+ it('should handle click', () => {
+ valueEditor.onMouseUp = jest.fn()
+ valueEditor.onFocus = jest.fn()
+ valueEditor.onClick('foo')
+ expect(valueEditor.onMouseUp).toBeCalled()
+ expect(valueEditor.onFocus).toBeCalledWith('foo')
+ })
+
+ it('should handle blur', () => {
+ // return undefined
+ valueEditor._ignore_events = true
+ expect(valueEditor.onBlur({})).toEqual(undefined)
+ // else
+ valueEditor._ignore_events = false
+ valueEditor.onBlur({})
+ expect(valueEditor.state.editable).toBeFalsy()
+ expect(valueEditor.props.onDone).toBeCalledWith(valueEditor.input.textContent)
+ })
+
+ it('should handle key down', () => {
+ let mockKeyEvent = (keyCode, shiftKey=false) => {
+ return {
+ keyCode: keyCode,
+ shiftKey: shiftKey,
+ stopPropagation: jest.fn(),
+ preventDefault: jest.fn()
+ }
+ }
+ valueEditor.reset = jest.fn()
+ valueEditor.blur = jest.fn()
+ valueEditor.onKeyDown(mockKeyEvent(Key.ESC))
+ expect(valueEditor.reset).toBeCalled()
+ expect(valueEditor.blur).toBeCalled()
+ valueEditor.blur.mockReset()
+
+ valueEditor.onKeyDown(mockKeyEvent(Key.ENTER))
+ expect(valueEditor.blur).toBeCalled()
+
+ valueEditor.onKeyDown(mockKeyEvent(Key.SPACE))
+ })
+
+ it('should handle input', () => {
+ valueEditor.onInput()
+ })
+})
diff --git a/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValidateEditorSpec.js.snap b/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValidateEditorSpec.js.snap
new file mode 100644
index 00000000..96b9ce19
--- /dev/null
+++ b/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValidateEditorSpec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ValidateEditor Component should render correctly 1`] = `
+<div
+ className="inline-input editable has-success"
+ contentEditable={undefined}
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "foo",
+ }
+ }
+ onBlur={[Function]}
+ onClick={[Function]}
+ onFocus={[Function]}
+ onInput={[Function]}
+ onKeyDown={[Function]}
+ onMouseDown={[Function]}
+ onPaste={[Function]}
+ tabIndex={0}
+/>
+`;
diff --git a/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValueEditorSpec.js.snap b/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValueEditorSpec.js.snap
new file mode 100644
index 00000000..91e8ee84
--- /dev/null
+++ b/web/src/js/__tests__/components/ValueEditor/__snapshots__/ValueEditorSpec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ValueEditor Component should render correctly 1`] = `
+<div
+ className="inline-input editable"
+ contentEditable={undefined}
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "foo",
+ }
+ }
+ onBlur={[Function]}
+ onClick={[Function]}
+ onFocus={[Function]}
+ onInput={[Function]}
+ onKeyDown={[Function]}
+ onMouseDown={[Function]}
+ onPaste={[Function]}
+ tabIndex={0}
+/>
+`;
diff --git a/web/src/js/__tests__/components/common/ButtonSpec.js b/web/src/js/__tests__/components/common/ButtonSpec.js
new file mode 100644
index 00000000..ea05ee6e
--- /dev/null
+++ b/web/src/js/__tests__/components/common/ButtonSpec.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import Button from '../../../components/common/Button'
+
+describe('Button Component', () => {
+
+ it('should render correctly', () => {
+ let button = renderer.create(
+ <Button className="classname" onClick={() => "onclick"} title="title" icon="icon">
+ <a>foo</a>
+ </Button>
+ ),
+ tree = button.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should be able to be disabled', () => {
+ let button = renderer.create(
+ <Button className="classname" onClick={() => "onclick"} disabled="true" children="children">
+ <a>foo</a>
+ </Button>
+ ),
+ tree = button.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/DocsLinkSpec.js b/web/src/js/__tests__/components/common/DocsLinkSpec.js
new file mode 100644
index 00000000..effed1b7
--- /dev/null
+++ b/web/src/js/__tests__/components/common/DocsLinkSpec.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import DocsLink from '../../../components/common/DocsLink'
+
+describe('DocsLink Component', () => {
+ it('should be able to be rendered with children nodes', () => {
+ let docsLink = renderer.create(<DocsLink children="foo" resource="bar"></DocsLink>),
+ tree = docsLink.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should be able to be rendered without children nodes', () => {
+ let docsLink = renderer.create(<DocsLink resource="bar"></DocsLink>),
+ tree = docsLink.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/DropdownSpec.js b/web/src/js/__tests__/components/common/DropdownSpec.js
new file mode 100644
index 00000000..c8c57ea6
--- /dev/null
+++ b/web/src/js/__tests__/components/common/DropdownSpec.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import Dropdown, { Divider } from '../../../components/common/Dropdown'
+
+describe('Dropdown Component', () => {
+ let dropup = renderer.create(<Dropdown dropup btnClass="foo">
+ <a href="#">1</a>
+ <Divider/>
+ <a href="#">2</a>
+ </Dropdown>),
+ dropdown = renderer.create(<Dropdown btnClass="foo">
+ <a href="#">1</a>
+ <a href="#">2</a>
+ </Dropdown>)
+
+ it('should render correctly', () => {
+ let tree = dropup.toJSON()
+ expect(tree).toMatchSnapshot()
+
+ tree = dropdown.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle open/close action', () => {
+ document.body.addEventListener('click', ()=>{})
+ let tree = dropup.toJSON(),
+ e = { preventDefault: jest.fn() }
+ tree.children[0].props.onClick(e)
+ expect(tree).toMatchSnapshot()
+
+ // click action when the state is open
+ tree.children[0].props.onClick(e)
+
+ // close
+ document.body.click()
+ expect(tree).toMatchSnapshot()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/FileChooserSpec.js b/web/src/js/__tests__/components/common/FileChooserSpec.js
new file mode 100644
index 00000000..7d031a38
--- /dev/null
+++ b/web/src/js/__tests__/components/common/FileChooserSpec.js
@@ -0,0 +1,38 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import FileChooser from '../../../components/common/FileChooser'
+
+describe('FileChooser Component', () => {
+ let openFileFunc = jest.fn(),
+ createNodeMock = () => { return { click: jest.fn() } },
+ fileChooser = renderer.create(
+ <FileChooser className="foo" title="bar" onOpenFile={ openFileFunc }/>
+ , { createNodeMock })
+ //[test refs with react-test-renderer](https://github.com/facebook/react/issues/7371)
+
+ it('should render correctly', () => {
+ let tree = fileChooser.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle click action', () => {
+ let tree = fileChooser.toJSON(),
+ mockEvent = {
+ preventDefault: jest.fn(),
+ target: {
+ files: [ "foo", "bar" ]
+ }
+ }
+ tree.children[1].props.onChange(mockEvent)
+ expect(openFileFunc).toBeCalledWith("foo")
+ tree.props.onClick()
+ // without files
+ mockEvent = {
+ ...mockEvent,
+ target: { files: [ ]}
+ }
+ openFileFunc.mockClear()
+ tree.children[1].props.onChange(mockEvent)
+ expect(openFileFunc).not.toBeCalled()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/SplitterSpec.js b/web/src/js/__tests__/components/common/SplitterSpec.js
new file mode 100644
index 00000000..9ec48350
--- /dev/null
+++ b/web/src/js/__tests__/components/common/SplitterSpec.js
@@ -0,0 +1,84 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+import renderer from 'react-test-renderer'
+import Splitter from '../../../components/common/Splitter'
+import TestUtils from 'react-dom/test-utils';
+
+describe('Splitter Component', () => {
+
+ it('should render correctly', () => {
+ let splitter = renderer.create(<Splitter></Splitter>),
+ tree = splitter.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ let splitter = TestUtils.renderIntoDocument(<Splitter></Splitter>),
+ dom = ReactDOM.findDOMNode(splitter),
+ previousElementSibling = {
+ offsetHeight: 0,
+ offsetWidth: 0,
+ style: {flex: ''}
+ },
+ nextElementSibling = {
+ style: {flex: ''}
+ }
+
+ it('should handle mouseDown ', () => {
+ window.addEventListener = jest.fn()
+ splitter.onMouseDown({ pageX: 1, pageY: 2})
+ expect(splitter.state.startX).toEqual(1)
+ expect(splitter.state.startY).toEqual(2)
+ expect(window.addEventListener).toBeCalledWith('mousemove', splitter.onMouseMove)
+ expect(window.addEventListener).toBeCalledWith('mouseup', splitter.onMouseUp)
+ expect(window.addEventListener).toBeCalledWith('dragend', splitter.onDragEnd)
+ })
+
+ it('should handle dragEnd', () => {
+ window.removeEventListener = jest.fn()
+ splitter.onDragEnd()
+ expect(dom.style.transform).toEqual('')
+ expect(window.removeEventListener).toBeCalledWith('dragend', splitter.onDragEnd)
+ expect(window.removeEventListener).toBeCalledWith('mouseup', splitter.onMouseUp)
+ expect(window.removeEventListener).toBeCalledWith('mousemove', splitter.onMouseMove)
+ })
+
+ it('should handle mouseUp', () => {
+
+ Object.defineProperty(dom, 'previousElementSibling', { value: previousElementSibling })
+ Object.defineProperty(dom, 'nextElementSibling', { value: nextElementSibling })
+ splitter.onMouseUp({ pageX: 3, pageY: 4 })
+ expect(splitter.state.applied).toBeTruthy()
+ expect(nextElementSibling.style.flex).toEqual('1 1 auto')
+ expect(previousElementSibling.style.flex).toEqual('0 0 2px')
+ })
+
+ it('should handle mouseMove', () => {
+ splitter.onMouseMove({pageX: 10, pageY: 10})
+ expect(dom.style.transform).toEqual("translate(9px, 0px)")
+
+ let splitterY = TestUtils.renderIntoDocument(<Splitter axis="y"></Splitter>)
+ splitterY.onMouseMove({pageX: 10, pageY: 10})
+ expect(ReactDOM.findDOMNode(splitterY).style.transform).toEqual("translate(0px, 10px)")
+ })
+
+ it('should handle resize', () => {
+ window.setTimeout = jest.fn((event, time) => event())
+ splitter.onResize()
+ expect(window.setTimeout).toHaveBeenCalled()
+ })
+
+ it('should handle componentWillUnmount', () => {
+ splitter.componentWillUnmount()
+ expect(previousElementSibling.style.flex).toEqual('')
+ expect(nextElementSibling.style.flex).toEqual('')
+ expect(splitter.state.applied).toBeTruthy()
+ })
+
+ it('should handle reset', () => {
+ splitter.reset(false)
+ expect(splitter.state.applied).toBeFalsy()
+
+ expect(splitter.reset(true)).toEqual(undefined)
+ })
+
+})
diff --git a/web/src/js/__tests__/components/common/ToggleButtonSpec.js b/web/src/js/__tests__/components/common/ToggleButtonSpec.js
new file mode 100644
index 00000000..2188da82
--- /dev/null
+++ b/web/src/js/__tests__/components/common/ToggleButtonSpec.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import ToggleButton from '../../../components/common/ToggleButton'
+
+describe('ToggleButton Component', () => {
+ let mockFunc = jest.fn()
+
+ it('should render correctly', () => {
+ let checkedButton = renderer.create(
+ <ToggleButton checked={true} onToggle={mockFunc} text="foo">
+ text
+ </ToggleButton>),
+ tree = checkedButton.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle click action', () => {
+ let uncheckButton = renderer.create(
+ <ToggleButton checked={false} onToggle={mockFunc} text="foo">
+ text
+ </ToggleButton>),
+ tree = uncheckButton.toJSON()
+ tree.props.onClick()
+ expect(mockFunc).toBeCalled()
+ })
+})
diff --git a/web/src/js/__tests__/components/common/ToggleInputButtonSpec.js b/web/src/js/__tests__/components/common/ToggleInputButtonSpec.js
new file mode 100644
index 00000000..39e555cd
--- /dev/null
+++ b/web/src/js/__tests__/components/common/ToggleInputButtonSpec.js
@@ -0,0 +1,43 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import ToggleInputButton from '../../../components/common/ToggleInputButton'
+import { Key } from '../../../utils'
+
+describe('ToggleInputButton Component', () => {
+ let mockFunc = jest.fn(),
+ toggleInputButton = undefined,
+ tree = undefined
+
+ it('should render correctly', () => {
+ toggleInputButton = renderer.create(
+ <ToggleInputButton checked={true} name="foo" onToggleChanged={mockFunc}
+ placeholder="bar">text</ToggleInputButton>)
+ tree = toggleInputButton.toJSON()
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle keydown and click action', () => {
+ toggleInputButton = renderer.create(
+ <ToggleInputButton checked={false} name="foo" onToggleChanged={mockFunc}
+ placeholder="bar" txt="txt">text</ToggleInputButton>)
+ tree = toggleInputButton.toJSON()
+ let mockEvent = {
+ keyCode: Key.ENTER,
+ stopPropagation: jest.fn()
+ }
+
+ tree.children[1].props.onKeyDown(mockEvent)
+ expect(mockFunc).toBeCalledWith("txt")
+
+ tree.children[0].props.onClick()
+ expect(mockFunc).toBeCalledWith("txt")
+ })
+
+ it('should update state onChange', () => {
+ // trigger onChange
+ tree.children[1].props.onChange({ target: { value: "foo" }})
+ // update the tree
+ tree = toggleInputButton.toJSON()
+ expect(tree.children[1].props.value).toEqual("foo")
+ })
+})
diff --git a/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap
new file mode 100644
index 00000000..1d403b2d
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/ButtonSpec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Button Component should be able to be disabled 1`] = `
+<div
+ className="classname btn btn-default"
+ disabled="true"
+ onClick={false}
+ title={undefined}
+>
+ <a>
+ foo
+ </a>
+</div>
+`;
+
+exports[`Button Component should render correctly 1`] = `
+<div
+ className="classname btn btn-default"
+ disabled={undefined}
+ onClick={[Function]}
+ title="title"
+>
+ <i
+ className="fa fa-fw icon"
+ />
+ <a>
+ foo
+ </a>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.js.snap
new file mode 100644
index 00000000..d91b77f7
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/DocsLinkSpec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DocsLink Component should be able to be rendered with children nodes 1`] = `
+<a
+ href="http://docs.mitmproxy.org/en/stable/bar"
+ target="_blank"
+>
+ foo
+</a>
+`;
+
+exports[`DocsLink Component should be able to be rendered without children nodes 1`] = `
+<a
+ href="http://docs.mitmproxy.org/en/stable/bar"
+ target="_blank"
+>
+ <i
+ className="fa fa-question-circle"
+ />
+</a>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/DropdownSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/DropdownSpec.js.snap
new file mode 100644
index 00000000..57d4968d
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/DropdownSpec.js.snap
@@ -0,0 +1,162 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Dropdown Component should handle open/close action 1`] = `
+<div
+ className="dropup"
+>
+ <a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ />
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ >
+ 1
+ </a>
+
+ </li>
+ <li>
+
+ <hr
+ className="divider"
+ />
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ >
+ 2
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`Dropdown Component should handle open/close action 2`] = `
+<div
+ className="dropup"
+>
+ <a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ />
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ >
+ 1
+ </a>
+
+ </li>
+ <li>
+
+ <hr
+ className="divider"
+ />
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ >
+ 2
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`Dropdown Component should render correctly 1`] = `
+<div
+ className="dropup"
+>
+ <a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ />
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ >
+ 1
+ </a>
+
+ </li>
+ <li>
+
+ <hr
+ className="divider"
+ />
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ >
+ 2
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
+
+exports[`Dropdown Component should render correctly 2`] = `
+<div
+ className="dropdown"
+>
+ <a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ />
+ <ul
+ className="dropdown-menu"
+ role="menu"
+ >
+ <li>
+
+ <a
+ href="#"
+ >
+ 1
+ </a>
+
+ </li>
+ <li>
+
+ <a
+ href="#"
+ >
+ 2
+ </a>
+
+ </li>
+ </ul>
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/FileChooserSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/FileChooserSpec.js.snap
new file mode 100644
index 00000000..5f0b3cf3
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/FileChooserSpec.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FileChooser Component should render correctly 1`] = `
+<a
+ className="foo"
+ href="#"
+ onClick={[Function]}
+ title="bar"
+>
+ <i
+ className="fa fa-fw undefined"
+ />
+ <input
+ className="hidden"
+ onChange={[Function]}
+ type="file"
+ />
+</a>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/SplitterSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/SplitterSpec.js.snap
new file mode 100644
index 00000000..dd70ed7a
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/SplitterSpec.js.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Splitter Component should render correctly 1`] = `
+<div
+ className="splitter splitter-x"
+>
+ <div
+ draggable="true"
+ onMouseDown={[Function]}
+ />
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/ToggleButtonSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/ToggleButtonSpec.js.snap
new file mode 100644
index 00000000..f468d39f
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/ToggleButtonSpec.js.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ToggleButton Component should render correctly 1`] = `
+<div
+ className="btn btn-toggle btn-primary"
+ onClick={[Function]}
+>
+ <i
+ className="fa fa-fw fa-check-square-o"
+ />
+  
+ foo
+</div>
+`;
diff --git a/web/src/js/__tests__/components/common/__snapshots__/ToggleInputButtonSpec.js.snap b/web/src/js/__tests__/components/common/__snapshots__/ToggleInputButtonSpec.js.snap
new file mode 100644
index 00000000..b8d80177
--- /dev/null
+++ b/web/src/js/__tests__/components/common/__snapshots__/ToggleInputButtonSpec.js.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ToggleInputButton Component should render correctly 1`] = `
+<div
+ className="input-group toggle-input-btn"
+>
+ <span
+ className="input-group-btn"
+ onClick={[Function]}
+ >
+ <div
+ className="btn btn-primary"
+ >
+ <span
+ className="fa fa-check-square-o"
+ />
+  
+ foo
+ </div>
+ </span>
+ <input
+ className="form-control"
+ disabled={true}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="bar"
+ type="text"
+ value=""
+ />
+</div>
+`;
diff --git a/web/src/js/__tests__/components/helpers/AutoScrollSpec.js b/web/src/js/__tests__/components/helpers/AutoScrollSpec.js
new file mode 100644
index 00000000..18a3d669
--- /dev/null
+++ b/web/src/js/__tests__/components/helpers/AutoScrollSpec.js
@@ -0,0 +1,41 @@
+import React from "react"
+import ReactDOM from "react-dom"
+import AutoScroll from '../../../components/helpers/AutoScroll'
+import { calcVScroll } from '../../../components/helpers/VirtualScroll'
+import TestUtils from 'react-dom/test-utils'
+
+describe('Autoscroll', () => {
+ let mockFn = jest.fn()
+ class tComponent extends React.Component {
+ constructor(props, context){
+ super(props, context)
+ this.state = { vScroll: calcVScroll() }
+ }
+
+ componentWillUpdate() {
+ mockFn("foo")
+ }
+
+ componentDidUpdate() {
+ mockFn("bar")
+ }
+
+ render() {
+ return (<p>foo</p>)
+ }
+ }
+
+ it('should update component', () => {
+ let Foo = AutoScroll(tComponent),
+ autoScroll = TestUtils.renderIntoDocument(<Foo></Foo>),
+ viewport = ReactDOM.findDOMNode(autoScroll)
+ viewport.scrollTop = 10
+ Object.defineProperty(viewport, "scrollHeight", { value: 10, writable: true })
+ autoScroll.componentWillUpdate()
+ expect(mockFn).toBeCalledWith("foo")
+
+ Object.defineProperty(viewport, "scrollHeight", { value: 0, writable: true })
+ autoScroll.componentDidUpdate()
+ expect(mockFn).toBeCalledWith("bar")
+ })
+})
diff --git a/web/src/js/__tests__/components/helpers/VirtualScrollSpec.js b/web/src/js/__tests__/components/helpers/VirtualScrollSpec.js
new file mode 100644
index 00000000..8081e90d
--- /dev/null
+++ b/web/src/js/__tests__/components/helpers/VirtualScrollSpec.js
@@ -0,0 +1,21 @@
+import { calcVScroll } from '../../../components/helpers/VirtualScroll'
+
+describe('VirtualScroll', () => {
+
+ it('should return default state without options', () => {
+ expect(calcVScroll()).toEqual({start: 0, end: 0, paddingTop: 0, paddingBottom: 0})
+ })
+
+ it('should calculate position without itemHeights', () => {
+ expect(calcVScroll({itemCount: 0, rowHeight: 32, viewportHeight: 400, viewportTop: 0})).toEqual({
+ start: 0, end: 0, paddingTop: 0, paddingBottom: 0
+ })
+ })
+
+ it('should calculate position with itemHeights', () => {
+ expect(calcVScroll({itemCount: 5, itemHeights: [100, 100, 100, 100, 100],
+ viewportHeight: 300, viewportTop: 0})).toEqual({
+ start: 0, end: 4, paddingTop: 0, paddingBottom: 100
+ })
+ })
+})
diff --git a/web/src/js/__tests__/ducks/_tflow.js b/web/src/js/__tests__/ducks/_tflow.js
new file mode 100644
index 00000000..f6a382bd
--- /dev/null
+++ b/web/src/js/__tests__/ducks/_tflow.js
@@ -0,0 +1,97 @@
+export default function(){
+ return {
+ "client_conn": {
+ "address": [
+ "address",
+ 22
+ ],
+ "alpn_proto_negotiated": "http/1.1",
+ "cipher_name": "cipher",
+ "clientcert": null,
+ "id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
+ "sni": "address",
+ "ssl_established": false,
+ "timestamp_end": 3.0,
+ "timestamp_ssl_setup": 2.0,
+ "timestamp_start": 1.0,
+ "tls_version": "TLSv1.2"
+ },
+ "error": {
+ "msg": "error",
+ "timestamp": 1495370312.4814785
+ },
+ "id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
+ "intercepted": false,
+ "marked": false,
+ "modified": false,
+ "request": {
+ "contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
+ "contentLength": 7,
+ "headers": [
+ [
+ "header",
+ "qvalue"
+ ],
+ [
+ "content-length",
+ "7"
+ ]
+ ],
+ "host": "address",
+ "http_version": "HTTP/1.1",
+ "is_replay": false,
+ "method": "GET",
+ "path": "/path",
+ "port": 22,
+ "pretty_host": "address",
+ "scheme": "http",
+ "timestamp_end": null,
+ "timestamp_start": null
+ },
+ "response": {
+ "contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d",
+ "contentLength": 7,
+ "headers": [
+ [
+ "header-response",
+ "svalue"
+ ],
+ [
+ "content-length",
+ "7"
+ ]
+ ],
+ "http_version": "HTTP/1.1",
+ "is_replay": false,
+ "reason": "OK",
+ "status_code": 200,
+ "timestamp_end": 1495370312.4814625,
+ "timestamp_start": 1495370312.481462
+ },
+ "server_conn": {
+ "address": [
+ "address",
+ 22
+ ],
+ "alpn_proto_negotiated": null,
+ "id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
+ "ip_address": [
+ "192.168.0.1",
+ 22
+ ],
+ "sni": "address",
+ "source_address": [
+ "address",
+ 22
+ ],
+ "ssl_established": false,
+ "timestamp_end": 4.0,
+ "timestamp_ssl_setup": 3.0,
+ "timestamp_start": 1.0,
+ "timestamp_tcp_setup": 2.0,
+ "tls_version": "TLSv1.2",
+ "via": null
+ },
+ "type": "http"
+}
+} \ No newline at end of file
diff --git a/web/src/js/__tests__/ducks/connectionSpec.js b/web/src/js/__tests__/ducks/connectionSpec.js
new file mode 100644
index 00000000..d087e867
--- /dev/null
+++ b/web/src/js/__tests__/ducks/connectionSpec.js
@@ -0,0 +1,41 @@
+import reduceConnection from "../../ducks/connection"
+import * as ConnectionActions from "../../ducks/connection"
+import { ConnectionState } from "../../ducks/connection"
+
+describe('connection reducer', () => {
+ it('should return initial state', () => {
+ expect(reduceConnection(undefined, {})).toEqual({
+ state: ConnectionState.INIT,
+ message: null,
+ })
+ })
+
+ it('should handle start fetch', () => {
+ expect(reduceConnection(undefined, ConnectionActions.startFetching())).toEqual({
+ state: ConnectionState.FETCHING,
+ message: undefined,
+ })
+ })
+
+ it('should handle connection established', () => {
+ expect(reduceConnection(undefined, ConnectionActions.connectionEstablished())).toEqual({
+ state: ConnectionState.ESTABLISHED,
+ message: undefined,
+ })
+ })
+
+ it('should handle connection error', () => {
+ expect(reduceConnection(undefined, ConnectionActions.connectionError("no internet"))).toEqual({
+ state: ConnectionState.ERROR,
+ message: "no internet",
+ })
+ })
+
+ it('should handle offline mode', () => {
+ expect(reduceConnection(undefined, ConnectionActions.setOffline())).toEqual({
+ state: ConnectionState.OFFLINE,
+ message: undefined,
+ })
+ })
+
+})
diff --git a/web/src/js/__tests__/ducks/eventLogSpec.js b/web/src/js/__tests__/ducks/eventLogSpec.js
new file mode 100644
index 00000000..1c993734
--- /dev/null
+++ b/web/src/js/__tests__/ducks/eventLogSpec.js
@@ -0,0 +1,38 @@
+import reduceEventLog, * as eventLogActions from '../../ducks/eventLog'
+import reduceStore from '../../ducks/utils/store'
+
+describe('event log reducer', () => {
+ it('should return initial state', () => {
+ expect(reduceEventLog(undefined, {})).toEqual({
+ visible: false,
+ filters: { debug: false, info: true, web: true, warn: true, error: true },
+ ...reduceStore(undefined, {}),
+ })
+ })
+
+ it('should be possible to toggle filter', () => {
+ let state = reduceEventLog(undefined, eventLogActions.add('foo'))
+ expect(reduceEventLog(state, eventLogActions.toggleFilter('info'))).toEqual({
+ visible: false,
+ filters: { ...state.filters, info: false},
+ ...reduceStore(state, {})
+ })
+ })
+
+ it('should be possible to toggle visibility', () => {
+ let state = reduceEventLog(undefined, {})
+ expect(reduceEventLog(state, eventLogActions.toggleVisibility())).toEqual({
+ visible: true,
+ filters: {...state.filters},
+ ...reduceStore(undefined, {})
+ })
+ })
+
+ it('should be possible to add message', () => {
+ let state = reduceEventLog(undefined, eventLogActions.add('foo'))
+ expect(state.visible).toBeFalsy()
+ expect(state.filters).toEqual({
+ debug: false, info: true, web: true, warn: true, error: true
+ })
+ })
+})
diff --git a/web/src/js/__tests__/ducks/flowsSpec.js b/web/src/js/__tests__/ducks/flowsSpec.js
index acfa3083..5bd866f2 100644
--- a/web/src/js/__tests__/ducks/flowsSpec.js
+++ b/web/src/js/__tests__/ducks/flowsSpec.js
@@ -1,31 +1,224 @@
-jest.unmock('../../ducks/flows');
+jest.mock('../../utils')
-import reduceFlows, * as flowActions from '../../ducks/flows'
-import * as storeActions from '../../ducks/utils/store'
+import reduceFlows from "../../ducks/flows"
+import * as flowActions from "../../ducks/flows"
+import reduceStore from "../../ducks/utils/store"
+import { fetchApi } from "../../utils"
+import { createStore } from "./tutils"
-
-describe('select flow', () => {
-
- let state = reduceFlows(undefined, {})
+describe('flow reducer', () => {
+ let state = undefined
for (let i of [1, 2, 3, 4]) {
- state = reduceFlows(state, storeActions.add({ id: i }))
+ state = reduceFlows(state, { type: flowActions.ADD, data: { id: i }, cmd: 'add' })
}
- it('should be possible to select a single flow', () => {
- expect(reduceFlows(state, flowActions.select(2))).toEqual(
- {
- ...state,
- selected: [2],
- }
- )
- })
-
- it('should be possible to deselect a flow', () => {
- expect(reduceFlows({ ...state, selected: [1] }, flowActions.select())).toEqual(
- {
- ...state,
- selected: [],
- }
- )
+ it('should return initial state', () => {
+ expect(reduceFlows(undefined, {})).toEqual({
+ highlight: null,
+ filter: null,
+ sort: { column: null, desc: false },
+ selected: [],
+ ...reduceStore(undefined, {})
+ })
+ })
+
+ describe('selections', () => {
+ it('should be possible to select a single flow', () => {
+ expect(reduceFlows(state, flowActions.select(2))).toEqual(
+ {
+ ...state,
+ selected: [2],
+ }
+ )
+ })
+
+ it('should be possible to deselect a flow', () => {
+ expect(reduceFlows({ ...state, selected: [1] }, flowActions.select())).toEqual(
+ {
+ ...state,
+ selected: [],
+ }
+ )
+ })
+
+ it('should be possible to select relative', () => {
+ // haven't selected any flow
+ expect(
+ flowActions.selectRelative(state, 1)
+ ).toEqual(
+ flowActions.select(4)
+ )
+
+ // already selected some flows
+ expect(
+ flowActions.selectRelative({ ...state, selected: [2] }, 1)
+ ).toEqual(
+ flowActions.select(3)
+ )
+ })
+
+ it('should update state.selected on remove', () => {
+ let next
+ next = reduceFlows({ ...state, selected: [2] }, {
+ type: flowActions.REMOVE,
+ data: 2,
+ cmd: 'remove'
+ })
+ expect(next.selected).toEqual([3])
+
+ //last row
+ next = reduceFlows({ ...state, selected: [4] }, {
+ type: flowActions.REMOVE,
+ data: 4,
+ cmd: 'remove'
+ })
+ expect(next.selected).toEqual([3])
+
+ //multiple selection
+ next = reduceFlows({ ...state, selected: [2, 3, 4] }, {
+ type: flowActions.REMOVE,
+ data: 3,
+ cmd: 'remove'
+ })
+ expect(next.selected).toEqual([2, 4])
+ })
+ })
+
+ it('should be possible to set filter', () => {
+ let filt = "~u 123"
+ expect(reduceFlows(undefined, flowActions.setFilter(filt)).filter).toEqual(filt)
+ })
+
+ it('should be possible to set highlight', () => {
+ let key = "foo"
+ expect(reduceFlows(undefined, flowActions.setHighlight(key)).highlight).toEqual(key)
+ })
+
+ it('should be possible to set sort', () => {
+ let sort = { column: "TLSColumn", desc: 1 }
+ expect(reduceFlows(undefined, flowActions.setSort(sort.column, sort.desc)).sort).toEqual(sort)
+ })
+
+})
+
+describe('flows actions', () => {
+
+ let store = createStore({ reduceFlows })
+
+ let tflow = { id: 1 }
+ it('should handle resume action', () => {
+ store.dispatch(flowActions.resume(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/resume', { method: 'POST' })
+ })
+
+ it('should handle resumeAll action', () => {
+ store.dispatch(flowActions.resumeAll())
+ expect(fetchApi).toBeCalledWith('/flows/resume', { method: 'POST' })
+ })
+
+ it('should handle kill action', () => {
+ store.dispatch(flowActions.kill(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/kill', { method: 'POST' })
+
+ })
+
+ it('should handle killAll action', () => {
+ store.dispatch(flowActions.killAll())
+ expect(fetchApi).toBeCalledWith('/flows/kill', { method: 'POST' })
+ })
+
+ it('should handle remove action', () => {
+ store.dispatch(flowActions.remove(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1', { method: 'DELETE' })
+ })
+
+ it('should handle duplicate action', () => {
+ store.dispatch(flowActions.duplicate(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/duplicate', { method: 'POST' })
+ })
+
+ it('should handle replay action', () => {
+ store.dispatch(flowActions.replay(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/replay', { method: 'POST' })
+ })
+
+ it('should handle revert action', () => {
+ store.dispatch(flowActions.revert(tflow))
+ expect(fetchApi).toBeCalledWith('/flows/1/revert', { method: 'POST' })
+ })
+
+ it('should handle update action', () => {
+ store.dispatch(flowActions.update(tflow, 'foo'))
+ expect(fetchApi.put).toBeCalledWith('/flows/1', 'foo')
+ })
+
+ it('should handle uploadContent action', () => {
+ let body = new FormData(),
+ file = new window.Blob(['foo'], { type: 'plain/text' })
+ body.append('file', file)
+ store.dispatch(flowActions.uploadContent(tflow, 'foo', 'foo'))
+ expect(fetchApi).toBeCalledWith('/flows/1/foo/content', { method: 'POST', body})
+ })
+
+ it('should handle clear action', () => {
+ store.dispatch(flowActions.clear())
+ expect(fetchApi).toBeCalledWith('/clear', { method: 'POST'} )
+ })
+
+ it('should handle download action', () => {
+ let state = reduceFlows(undefined, {})
+ expect(reduceFlows(state, flowActions.download())).toEqual(state)
+ })
+
+ it('should handle upload action', () => {
+ let body = new FormData()
+ body.append('file', 'foo')
+ store.dispatch(flowActions.upload('foo'))
+ expect(fetchApi).toBeCalledWith('/flows/dump', { method: 'POST', body })
+ })
+})
+
+describe('makeSort', () => {
+ it('should be possible to sort by TLSColumn', () => {
+ let sort = flowActions.makeSort({ column: 'TLSColumn', desc: true }),
+ a = { request: { scheme: 'http' } },
+ b = { request: { scheme: 'https' } }
+ expect(sort(a, b)).toEqual(1)
+ })
+
+ it('should be possible to sort by PathColumn', () => {
+ let sort = flowActions.makeSort({ column: 'PathColumn', desc: true }),
+ a = { request: {} },
+ b = { request: {} }
+ expect(sort(a, b)).toEqual(0)
+
+ })
+
+ it('should be possible to sort by MethodColumn', () => {
+ let sort = flowActions.makeSort({ column: 'MethodColumn', desc: true }),
+ a = { request: { method: 'GET' } },
+ b = { request: { method: 'POST' } }
+ expect(sort(b, a)).toEqual(-1)
+ })
+
+ it('should be possible to sort by StatusColumn', () => {
+ let sort = flowActions.makeSort({ column: 'StatusColumn', desc: false }),
+ a = { response: { status_code: 200 } },
+ b = { response: { status_code: 404 } }
+ expect(sort(a, b)).toEqual(-1)
+ })
+
+ it('should be possible to sort by TimeColumn', () => {
+ let sort = flowActions.makeSort({ column: 'TimeColumn', desc: false }),
+ a = { response: { timestamp_end: 9 }, request: { timestamp_start: 8 } },
+ b = { response: { timestamp_end: 10 }, request: { timestamp_start: 8 } }
+ expect(sort(b, a)).toEqual(1)
+ })
+
+ it('should be possible to sort by SizeColumn', () => {
+ let sort = flowActions.makeSort({ column: 'SizeColumn', desc: true }),
+ a = { request: { contentLength: 1 }, response: { contentLength: 1 } },
+ b = { request: { contentLength: 1 } }
+ expect(sort(a, b)).toEqual(-1)
})
})
diff --git a/web/src/js/__tests__/ducks/indexSpec.js b/web/src/js/__tests__/ducks/indexSpec.js
new file mode 100644
index 00000000..c5c4d525
--- /dev/null
+++ b/web/src/js/__tests__/ducks/indexSpec.js
@@ -0,0 +1,12 @@
+import reduceState from '../../ducks/index'
+
+describe('reduceState in js/ducks/index.js', () => {
+ it('should combine flow and header', () => {
+ let state = reduceState(undefined, {})
+ expect(state.hasOwnProperty('eventLog')).toBeTruthy()
+ expect(state.hasOwnProperty('flows')).toBeTruthy()
+ expect(state.hasOwnProperty('settings')).toBeTruthy()
+ expect(state.hasOwnProperty('connection')).toBeTruthy()
+ expect(state.hasOwnProperty('ui')).toBeTruthy()
+ })
+})
diff --git a/web/src/js/__tests__/ducks/settingsSpec.js b/web/src/js/__tests__/ducks/settingsSpec.js
new file mode 100644
index 00000000..46d56ec7
--- /dev/null
+++ b/web/src/js/__tests__/ducks/settingsSpec.js
@@ -0,0 +1,25 @@
+jest.mock('../../utils')
+
+import reduceSettings, * as SettingsActions from '../../ducks/settings'
+
+describe('setting reducer', () => {
+ it('should return initial state', () => {
+ expect(reduceSettings(undefined, {})).toEqual({})
+ })
+
+ it('should handle receive action', () => {
+ let action = { type: SettingsActions.RECEIVE, data: 'foo' }
+ expect(reduceSettings(undefined, action)).toEqual('foo')
+ })
+
+ it('should handle update action', () => {
+ let action = {type: SettingsActions.UPDATE, data: {id: 1} }
+ expect(reduceSettings(undefined, action)).toEqual({id: 1})
+ })
+})
+
+describe('setting actions', () => {
+ it('should be possible to update setting', () => {
+ expect(reduceSettings(undefined, SettingsActions.update())).toEqual({})
+ })
+})
diff --git a/web/src/js/__tests__/ducks/tutils.js b/web/src/js/__tests__/ducks/tutils.js
index 90a21b78..211b61e3 100644
--- a/web/src/js/__tests__/ducks/tutils.js
+++ b/web/src/js/__tests__/ducks/tutils.js
@@ -1,6 +1,3 @@
-jest.unmock('redux')
-jest.unmock('redux-thunk')
-
import { combineReducers, applyMiddleware, createStore as createReduxStore } from 'redux'
import thunk from 'redux-thunk'
@@ -10,3 +7,5 @@ export function createStore(parts) {
applyMiddleware(...[thunk])
)
}
+
+export { default as TFlow } from './_tflow'
diff --git a/web/src/js/__tests__/ducks/ui/flowSpec.js b/web/src/js/__tests__/ducks/ui/flowSpec.js
index e994624d..cd6ffa2f 100644
--- a/web/src/js/__tests__/ducks/ui/flowSpec.js
+++ b/web/src/js/__tests__/ducks/ui/flowSpec.js
@@ -1,7 +1,3 @@
-jest.unmock('../../../ducks/ui/flow')
-jest.unmock('../../../ducks/flows')
-jest.unmock('lodash')
-
import _ from 'lodash'
import reducer, {
startEdit,
diff --git a/web/src/js/__tests__/ducks/ui/headerSpec.js b/web/src/js/__tests__/ducks/ui/headerSpec.js
index 8968e636..98822fd8 100644
--- a/web/src/js/__tests__/ducks/ui/headerSpec.js
+++ b/web/src/js/__tests__/ducks/ui/headerSpec.js
@@ -1,6 +1,3 @@
-jest.unmock('../../../ducks/ui/header')
-jest.unmock('../../../ducks/flows')
-
import reducer, { setActiveMenu } from '../../../ducks/ui/header'
import * as flowActions from '../../../ducks/flows'
diff --git a/web/src/js/__tests__/ducks/ui/indexSpec.js b/web/src/js/__tests__/ducks/ui/indexSpec.js
new file mode 100644
index 00000000..3c136bff
--- /dev/null
+++ b/web/src/js/__tests__/ducks/ui/indexSpec.js
@@ -0,0 +1,9 @@
+import reduceUI from '../../../ducks/ui/index'
+
+describe('reduceUI in js/ducks/ui/index.js', () => {
+ it('should combine flow and header', () => {
+ let state = reduceUI(undefined, {})
+ expect(state.hasOwnProperty('flow')).toBeTruthy()
+ expect(state.hasOwnProperty('header')).toBeTruthy()
+ })
+})
diff --git a/web/src/js/__tests__/ducks/ui/keyboardSpec.js b/web/src/js/__tests__/ducks/ui/keyboardSpec.js
new file mode 100644
index 00000000..500733cb
--- /dev/null
+++ b/web/src/js/__tests__/ducks/ui/keyboardSpec.js
@@ -0,0 +1,157 @@
+jest.mock('../../../utils')
+
+import { Key } from '../../../utils'
+import { onKeyDown } from '../../../ducks/ui/keyboard'
+import reduceFlows from '../../../ducks/flows'
+import reduceUI from '../../../ducks/ui/index'
+import * as flowsActions from '../../../ducks/flows'
+import * as UIActions from '../../../ducks/ui/flow'
+import configureStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import { fetchApi } from '../../../utils'
+
+const mockStore = configureStore([ thunk ])
+console.debug = jest.fn()
+
+describe('onKeyDown', () => {
+ let flows = undefined
+ for( let i=1; i <= 12; i++ ) {
+ flows = reduceFlows(flows, {type: flowsActions.ADD, data: {id: i, request: true, response: true}, cmd: 'add'})
+ }
+ let store = mockStore({ flows, ui: reduceUI(undefined, {}) })
+ let createKeyEvent = (keyCode, shiftKey = undefined, ctrlKey = undefined) => {
+ return onKeyDown({ keyCode, shiftKey, ctrlKey, preventDefault: jest.fn() })
+ }
+
+ afterEach(() => {
+ store.clearActions()
+ fetchApi.mockClear()
+ });
+
+ it('should handle cursor up', () => {
+ store.getState().flows = reduceFlows(flows, flowsActions.select(2))
+ store.dispatch(createKeyEvent(Key.K))
+ expect(store.getActions()).toEqual([{ flowIds: [1], type: flowsActions.SELECT }])
+
+ store.clearActions()
+ store.dispatch(createKeyEvent(Key.UP))
+ expect(store.getActions()).toEqual([{ flowIds: [1], type: flowsActions.SELECT }])
+ })
+
+ it('should handle cursor down', () => {
+ store.dispatch(createKeyEvent(Key.J))
+ expect(store.getActions()).toEqual([{ flowIds: [3], type: flowsActions.SELECT }])
+
+ store.clearActions()
+ store.dispatch(createKeyEvent(Key.DOWN))
+ expect(store.getActions()).toEqual([{ flowIds: [3], type: flowsActions.SELECT }])
+ })
+
+ it('should handle page down', () => {
+ store.dispatch(createKeyEvent(Key.SPACE))
+ expect(store.getActions()).toEqual([{ flowIds: [12], type: flowsActions.SELECT }])
+
+ store.getState().flows = reduceFlows(flows, flowsActions.select(1))
+ store.clearActions()
+ store.dispatch(createKeyEvent(Key.PAGE_DOWN))
+ expect(store.getActions()).toEqual([{ flowIds: [11], type: flowsActions.SELECT }])
+ })
+
+ it('should handle page up', () => {
+ store.getState().flows = reduceFlows(flows, flowsActions.select(11))
+ store.dispatch(createKeyEvent(Key.PAGE_UP))
+ expect(store.getActions()).toEqual([{ flowIds: [1], type: flowsActions.SELECT }])
+ })
+
+ it('should handle select first', () => {
+ store.dispatch(createKeyEvent(Key.HOME))
+ expect(store.getActions()).toEqual([{ flowIds: [1], type: flowsActions.SELECT }])
+ })
+
+ it('should handle select last', () => {
+ store.getState().flows = reduceFlows(flows, flowsActions.select(1))
+ store.dispatch(createKeyEvent(Key.END))
+ expect(store.getActions()).toEqual([{ flowIds: [12], type: flowsActions.SELECT }])
+ })
+
+ it('should handle deselect', () => {
+ store.dispatch(createKeyEvent(Key.ESC))
+ expect(store.getActions()).toEqual([{ flowIds: [], type: flowsActions.SELECT }])
+ })
+
+ it('should handle switch to left tab', () => {
+ store.dispatch(createKeyEvent(Key.LEFT))
+ expect(store.getActions()).toEqual([{ tab: 'details', type: UIActions.SET_TAB }])
+ })
+
+ it('should handle switch to right tab', () => {
+ store.dispatch(createKeyEvent(Key.TAB))
+ expect(store.getActions()).toEqual([{ tab: 'response', type: UIActions.SET_TAB }])
+
+ store.clearActions()
+ store.dispatch(createKeyEvent(Key.RIGHT))
+ expect(store.getActions()).toEqual([{ tab: 'response', type: UIActions.SET_TAB }])
+ })
+
+ it('should handle delete action', () => {
+ store.dispatch(createKeyEvent(Key.D))
+ expect(fetchApi).toBeCalledWith('/flows/1', { method: 'DELETE' })
+
+ })
+
+ it('should handle duplicate action', () => {
+ store.dispatch(createKeyEvent(Key.D, true))
+ expect(fetchApi).toBeCalledWith('/flows/1/duplicate', { method: 'POST' })
+ })
+
+ it('should handle resume action', () => {
+ // resume all
+ store.dispatch(createKeyEvent(Key.A, true))
+ expect(fetchApi).toBeCalledWith('/flows/resume', { method: 'POST' })
+ // resume
+ store.getState().flows.byId[store.getState().flows.selected[0]].intercepted = true
+ store.dispatch(createKeyEvent(Key.A))
+ expect(fetchApi).toBeCalledWith('/flows/1/resume', { method: 'POST' })
+ })
+
+ it('should handle replay action', () => {
+ store.dispatch(createKeyEvent(Key.R))
+ expect(fetchApi).toBeCalledWith('/flows/1/replay', { method: 'POST' })
+ })
+
+ it('should handle revert action', () => {
+ store.getState().flows.byId[store.getState().flows.selected[0]].modified = true
+ store.dispatch(createKeyEvent(Key.V))
+ expect(fetchApi).toBeCalledWith('/flows/1/revert', { method: 'POST' })
+ })
+
+ it('should handle kill action', () => {
+ // kill all
+ store.dispatch(createKeyEvent(Key.X, true))
+ expect(fetchApi).toBeCalledWith('/flows/kill', { method: 'POST' })
+ // kill
+ store.dispatch(createKeyEvent(Key.X))
+ expect(fetchApi).toBeCalledWith('/flows/1/kill', { method: 'POST' })
+ })
+
+ it('should handle clear action', () => {
+ store.dispatch(createKeyEvent(Key.Z))
+ expect(fetchApi).toBeCalledWith('/clear', { method: 'POST' })
+ })
+
+ it('should stop on some action with no flow is selected', () => {
+ store.getState().flows = reduceFlows(undefined, {})
+ store.dispatch(createKeyEvent(Key.LEFT))
+ store.dispatch(createKeyEvent(Key.TAB))
+ store.dispatch(createKeyEvent(Key.RIGHT))
+ store.dispatch(createKeyEvent(Key.D))
+ expect(fetchApi).not.toBeCalled()
+ })
+
+ it('should do nothing when Ctrl and undefined key is pressed ', () => {
+ store.dispatch(createKeyEvent(Key.BACKSPACE, false, true))
+ store.dispatch(createKeyEvent(0))
+ expect(fetchApi).not.toBeCalled()
+ })
+
+})
diff --git a/web/src/js/__tests__/ducks/utils/storeSpec.js b/web/src/js/__tests__/ducks/utils/storeSpec.js
index e4742490..11a8fe23 100644
--- a/web/src/js/__tests__/ducks/utils/storeSpec.js
+++ b/web/src/js/__tests__/ducks/utils/storeSpec.js
@@ -1,5 +1,3 @@
-jest.unmock('../../../ducks/utils/store')
-
import reduceStore, * as storeActions from '../../../ducks/utils/store'
describe('store reducer', () => {
diff --git a/web/src/js/__tests__/flow/utilsSpec.js b/web/src/js/__tests__/flow/utilsSpec.js
new file mode 100644
index 00000000..2d8f0456
--- /dev/null
+++ b/web/src/js/__tests__/flow/utilsSpec.js
@@ -0,0 +1,69 @@
+import * as utils from '../../flow/utils'
+
+describe('MessageUtils', () => {
+ it('should be possible to get first header', () => {
+ let msg = { headers: [["foo", "bar"]]}
+ expect(utils.MessageUtils.get_first_header(msg, "foo")).toEqual("bar")
+ expect(utils.MessageUtils.get_first_header(msg, "123")).toEqual(undefined)
+ })
+
+ it('should be possible to get Content-Type', () => {
+ let type = "text/html",
+ msg = { headers: [["Content-Type", type]]}
+ expect(utils.MessageUtils.getContentType(msg)).toEqual(type)
+ })
+
+ it('should be possible to match header', () => {
+ let h1 = ["foo", "bar"],
+ msg = {headers : [h1]}
+ expect(utils.MessageUtils.match_header(msg, /foo/i)).toEqual(h1)
+ expect(utils.MessageUtils.match_header(msg, /123/i)).toBeFalsy()
+ })
+
+ it('should be possible to get content URL', () => {
+ // request
+ let msg = "foo", view = "bar",
+ flow = { request: msg, id: 1}
+ expect(utils.MessageUtils.getContentURL(flow, msg, view)).toEqual(
+ "/flows/1/request/content/bar"
+ )
+ expect(utils.MessageUtils.getContentURL(flow, msg, '')).toEqual(
+ "/flows/1/request/content"
+ )
+ // response
+ flow = {response: msg, id: 2}
+ expect(utils.MessageUtils.getContentURL(flow, msg, view)).toEqual(
+ "/flows/2/response/content/bar"
+ )
+ })
+})
+
+describe('RequestUtils', () => {
+ it('should be possible prettify url', () => {
+ let request = {port: 4444, scheme: "http", pretty_host: "foo", path: "/bar"}
+ expect(utils.RequestUtils.pretty_url(request)).toEqual(
+ "http://foo:4444/bar"
+ )
+ })
+})
+
+describe('parseUrl', () => {
+ it('should be possible to parse url', () => {
+ let url = "http://foo:4444/bar"
+ expect(utils.parseUrl(url)).toEqual({
+ port: 4444,
+ scheme: 'http',
+ host: 'foo',
+ path: '/bar'
+ })
+
+ expect(utils.parseUrl("foo:foo")).toBeFalsy()
+ })
+})
+
+describe('isValidHttpVersion', () => {
+ it('should be possible to validate http version', () => {
+ expect(utils.isValidHttpVersion("HTTP/1.1")).toBeTruthy()
+ expect(utils.isValidHttpVersion("HTTP//1")).toBeFalsy()
+ })
+})
diff --git a/web/src/js/__tests__/urlStateSpec.js b/web/src/js/__tests__/urlStateSpec.js
new file mode 100644
index 00000000..c57c0a00
--- /dev/null
+++ b/web/src/js/__tests__/urlStateSpec.js
@@ -0,0 +1,100 @@
+import initialize from '../urlState'
+import { updateStoreFromUrl, updateUrlFromStore } from '../urlState'
+
+import reduceFlows from '../ducks/flows'
+import reduceUI from '../ducks/ui/index'
+import reduceEventLog from '../ducks/eventLog'
+import * as flowsActions from '../ducks/flows'
+
+import configureStore from 'redux-mock-store'
+
+const mockStore = configureStore()
+history.replaceState = jest.fn()
+
+describe('updateStoreFromUrl', () => {
+
+ it('should handle search query', () => {
+ window.location.hash = "#/flows?s=foo"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ filter: "foo", type: "FLOWS_SET_FILTER" }])
+ })
+
+ it('should handle highlight query', () => {
+ window.location.hash = "#/flows?h=foo"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ highlight: "foo", type: "FLOWS_SET_HIGHLIGHT" }])
+ })
+
+ it('should handle show event log', () => {
+ window.location.hash = "#/flows?e=true"
+ let initialState = { eventLog: reduceEventLog(undefined, {}) },
+ store = mockStore(initialState)
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([{ type: "EVENTS_TOGGLE_VISIBILITY" }])
+ })
+
+ it('should handle unimplemented query argument', () => {
+ window.location.hash = "#/flows?foo=bar"
+ console.error = jest.fn()
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(console.error).toBeCalledWith("unimplemented query arg: foo=bar")
+ })
+
+ it('should select flow and tab', () => {
+ window.location.hash = "#/flows/123/request"
+ let store = mockStore()
+ updateStoreFromUrl(store)
+ expect(store.getActions()).toEqual([
+ {
+ flowIds: ["123"],
+ type: "FLOWS_SELECT"
+ },
+ {
+ tab: "request",
+ type: "UI_FLOWVIEW_SET_TAB"
+ }
+ ])
+ })
+})
+
+describe('updateUrlFromStore', () => {
+ let initialState = {
+ flows: reduceFlows(undefined, {}),
+ ui: reduceUI(undefined, {}),
+ eventLog: reduceEventLog(undefined, {})
+ }
+
+ it('should update initial url', () => {
+ let store = mockStore(initialState)
+ updateUrlFromStore(store)
+ expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows')
+ })
+
+ it('should update url', () => {
+ let flows = reduceFlows(undefined, flowsActions.select(123)),
+ state = {
+ ...initialState,
+ flows: reduceFlows(flows, flowsActions.setFilter('~u foo'))
+ },
+ store = mockStore(state)
+ updateUrlFromStore(store)
+ expect(history.replaceState).toBeCalledWith(undefined, '', '/#/flows/123/request?s=~u foo')
+ })
+})
+
+describe('initialize', () => {
+ let initialState = {
+ flows: reduceFlows(undefined, {}),
+ ui: reduceUI(undefined, {}),
+ eventLog: reduceEventLog(undefined, {})
+ }
+
+ it('should handle initial state', () => {
+ let store = mockStore(initialState)
+ initialize(store)
+ store.dispatch({ type: "foo" })
+ })
+})
diff --git a/web/src/js/__tests__/utilsSpec.js b/web/src/js/__tests__/utilsSpec.js
new file mode 100644
index 00000000..9a1a0750
--- /dev/null
+++ b/web/src/js/__tests__/utilsSpec.js
@@ -0,0 +1,95 @@
+import * as utils from '../utils'
+
+global.fetch = jest.fn()
+
+describe('formatSize', () => {
+ it('should return 0 when 0 byte', () => {
+ expect(utils.formatSize(0)).toEqual('0')
+ })
+
+ it('should return formatted size', () => {
+ expect(utils.formatSize(27104011)).toEqual("25.8mb")
+ expect(utils.formatSize(1023)).toEqual("1023b")
+ })
+})
+
+describe('formatTimeDelta', () => {
+ it('should return formatted time', () => {
+ expect(utils.formatTimeDelta(3600100)).toEqual("1h")
+ })
+})
+
+describe('formatTimeSTamp', () => {
+ it('should return formatted time', () => {
+ expect(utils.formatTimeStamp(1483228800)).toEqual("2017-01-01 00:00:00.000")
+ })
+})
+
+describe('reverseString', () => {
+ it('should return reversed string', () => {
+ let str1 = "abc", str2="xyz"
+ expect(utils.reverseString(str1) > utils.reverseString(str2)).toBeTruthy()
+ })
+})
+
+describe('fetchApi', () => {
+ it('should handle fetch operation', () => {
+ utils.fetchApi('http://foo/bar', {method: "POST"})
+ expect(fetch.mock.calls[0][0]).toEqual(
+ "http://foo/bar?_xsrf=undefined"
+ )
+ fetch.mockClear()
+
+ utils.fetchApi('http://foo?bar=1', {method: "POST"})
+ expect(fetch.mock.calls[0][0]).toEqual(
+ "http://foo?bar=1&_xsrf=undefined"
+ )
+
+ })
+
+ it('should be possible to do put request', () => {
+ fetch.mockClear()
+ utils.fetchApi.put("http://foo", [1, 2, 3], {})
+ expect(fetch.mock.calls[0]).toEqual(
+ [
+ "http://foo?_xsrf=undefined",
+ {
+ body: "[1,2,3]",
+ credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ method: "PUT"
+ },
+ ]
+ )
+ })
+})
+
+describe('getDiff', () => {
+ it('should return json object including only the changed keys value pairs', () => {
+ let obj1 = {a: 1, b:{ foo: 1} , c: [3]},
+ obj2 = {a: 1, b:{ foo: 2} , c: [4]}
+ expect(utils.getDiff(obj1, obj2)).toEqual({ b: {foo: 2}, c:[4]})
+ })
+})
+
+describe('pure', () => {
+ let tFunc = function({ className }) {
+ return (<p className={ className }>foo</p>)
+ },
+ puredFunc = utils.pure(tFunc),
+ f = new puredFunc('bar')
+
+ it('should display function name', () => {
+ expect(utils.pure(tFunc).displayName).toEqual('tFunc')
+ })
+
+ it('should suggest when should component update', () => {
+ expect(f.shouldComponentUpdate('foo')).toBeTruthy()
+ expect(f.shouldComponentUpdate('bar')).toBeFalsy()
+ })
+
+ it('should render properties', () => {
+ expect(f.render()).toEqual(tFunc('bar'))
+ })
+
+})
diff --git a/web/src/js/backends/websocket.js b/web/src/js/backends/websocket.js
index 44b260c9..01094ac4 100644
--- a/web/src/js/backends/websocket.js
+++ b/web/src/js/backends/websocket.js
@@ -4,6 +4,7 @@
* An alternative backend may use the REST API only to host static instances.
*/
import { fetchApi } from "../utils"
+import * as connectionActions from "../ducks/connection"
const CMD_RESET = 'reset'
@@ -17,7 +18,7 @@ export default class WebsocketBackend {
connect() {
this.socket = new WebSocket(location.origin.replace('http', 'ws') + '/updates')
this.socket.addEventListener('open', () => this.onOpen())
- this.socket.addEventListener('close', () => this.onClose())
+ this.socket.addEventListener('close', event => this.onClose(event))
this.socket.addEventListener('message', msg => this.onMessage(JSON.parse(msg.data)))
this.socket.addEventListener('error', error => this.onError(error))
}
@@ -26,6 +27,7 @@ export default class WebsocketBackend {
this.fetchData("settings")
this.fetchData("flows")
this.fetchData("events")
+ this.store.dispatch(connectionActions.startFetching())
}
fetchData(resource) {
@@ -59,15 +61,22 @@ export default class WebsocketBackend {
let queue = this.activeFetches[resource]
delete this.activeFetches[resource]
queue.forEach(msg => this.onMessage(msg))
+
+ if(Object.keys(this.activeFetches).length === 0) {
+ // We have fetched the last resource
+ this.store.dispatch(connectionActions.connectionEstablished())
+ }
}
- onClose() {
- // FIXME
- console.error("onClose", arguments)
+ onClose(closeEvent) {
+ this.store.dispatch(connectionActions.connectionError(
+ `Connection closed at ${new Date().toUTCString()} with error code ${closeEvent.code}.`
+ ))
+ console.error("websocket connection closed", closeEvent)
}
onError() {
// FIXME
- console.error("onError", arguments)
+ console.error("websocket connection errored", arguments)
}
}
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx
index 398438ab..a79bf9e5 100644
--- a/web/src/js/components/ContentView.jsx
+++ b/web/src/js/components/ContentView.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import * as ContentViews from './ContentView/ContentViews'
import * as MetaViews from './ContentView/MetaViews'
@@ -11,8 +12,8 @@ ContentView.propTypes = {
// It may seem a bit weird at the first glance:
// Every view takes the flow and the message as props, e.g.
// <Auto flow={flow} message={flow.request}/>
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
+ flow: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
}
ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2)
diff --git a/web/src/js/components/ContentView/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx
index 8afc128f..f5961447 100644
--- a/web/src/js/components/ContentView/CodeEditor.jsx
+++ b/web/src/js/components/ContentView/CodeEditor.jsx
@@ -1,4 +1,5 @@
-import React, {PropTypes} from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import Codemirror from 'react-codemirror';
diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx
index e7a6f379..4cafde28 100644
--- a/web/src/js/components/ContentView/ContentLoader.jsx
+++ b/web/src/js/components/ContentView/ContentLoader.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { MessageUtils } from '../../flow/utils.js'
export default View => class extends React.Component {
diff --git a/web/src/js/components/ContentView/ContentViewOptions.jsx b/web/src/js/components/ContentView/ContentViewOptions.jsx
index 1ec9013e..e3cc39cd 100644
--- a/web/src/js/components/ContentView/ContentViewOptions.jsx
+++ b/web/src/js/components/ContentView/ContentViewOptions.jsx
@@ -1,12 +1,13 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ViewSelector from './ViewSelector'
import UploadContentButton from './UploadContentButton'
import DownloadContentButton from './DownloadContentButton'
ContentViewOptions.propTypes = {
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
+ flow: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
}
function ContentViewOptions({ flow, message, uploadContent, readonly, contentViewDescription }) {
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx
index db239195..136188d4 100644
--- a/web/src/js/components/ContentView/ContentViews.jsx
+++ b/web/src/js/components/ContentView/ContentViews.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes, Component } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { setContentViewDescription, setContent } from '../../ducks/ui/flow'
import ContentLoader from './ContentLoader'
@@ -21,7 +22,7 @@ function ViewImage({ flow, message }) {
}
Edit.propTypes = {
- content: React.PropTypes.string.isRequired,
+ content: PropTypes.string.isRequired,
}
function Edit({ content, onChange }) {
diff --git a/web/src/js/components/ContentView/DownloadContentButton.jsx b/web/src/js/components/ContentView/DownloadContentButton.jsx
index 3f11f909..447db211 100644
--- a/web/src/js/components/ContentView/DownloadContentButton.jsx
+++ b/web/src/js/components/ContentView/DownloadContentButton.jsx
@@ -1,5 +1,5 @@
import { MessageUtils } from "../../flow/utils"
-import { PropTypes } from 'react'
+import PropTypes from 'prop-types'
DownloadContentButton.propTypes = {
flow: PropTypes.object.isRequired,
diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx
index fd68991e..fd627ad9 100644
--- a/web/src/js/components/ContentView/ShowFullContentButton.jsx
+++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { render } from 'react-dom';
import Button from '../common/Button';
diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx
index de349af4..0021593f 100644
--- a/web/src/js/components/ContentView/UploadContentButton.jsx
+++ b/web/src/js/components/ContentView/UploadContentButton.jsx
@@ -1,4 +1,4 @@
-import { PropTypes } from 'react'
+import PropTypes from 'prop-types'
import FileChooser from '../common/FileChooser'
UploadContentButton.propTypes = {
@@ -6,7 +6,7 @@ UploadContentButton.propTypes = {
}
export default function UploadContentButton({ uploadContent }) {
-
+
return (
<FileChooser
icon="fa-upload"
diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx
index 43a53995..4c99d5ed 100644
--- a/web/src/js/components/ContentView/ViewSelector.jsx
+++ b/web/src/js/components/ContentView/ViewSelector.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes, Component } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { setContentView } from '../../ducks/ui/flow';
import Dropdown from '../common/Dropdown'
diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx
index 1a449511..a83cdb28 100644
--- a/web/src/js/components/EventLog.jsx
+++ b/web/src/js/components/EventLog.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { toggleFilter, toggleVisibility } from '../ducks/eventLog'
import ToggleButton from './common/ToggleButton'
@@ -55,7 +56,7 @@ class EventLog extends Component {
<div onMouseDown={this.onDragStart}>
Eventlog
<div className="pull-right">
- {['debug', 'info', 'web'].map(type => (
+ {['debug', 'info', 'web', 'warn', 'error'].map(type => (
<ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => toggleFilter(type)}/>
))}
<i onClick={close} className="fa fa-close"></i>
diff --git a/web/src/js/components/EventLog/EventList.jsx b/web/src/js/components/EventLog/EventList.jsx
index d0b036e7..a77b7e36 100644
--- a/web/src/js/components/EventLog/EventList.jsx
+++ b/web/src/js/components/EventLog/EventList.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import shallowEqual from 'shallowequal'
import AutoScroll from '../helpers/AutoScroll'
@@ -83,7 +84,12 @@ class EventLogList extends Component {
}
function LogIcon({ event }) {
- const icon = { web: 'html5', debug: 'bug' }[event.level] || 'info'
+ const icon = {
+ web: 'html5',
+ debug: 'bug',
+ warn: 'exclamation-triangle',
+ error: 'ban'
+ }[event.level] || 'info'
return <i className={`fa fa-fw fa-${icon}`}></i>
}
diff --git a/web/src/js/components/FlowTable.jsx b/web/src/js/components/FlowTable.jsx
index eddeed62..24c1f3a1 100644
--- a/web/src/js/components/FlowTable.jsx
+++ b/web/src/js/components/FlowTable.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import shallowEqual from 'shallowequal'
import AutoScroll from './helpers/AutoScroll'
diff --git a/web/src/js/components/FlowTable/FlowRow.jsx b/web/src/js/components/FlowTable/FlowRow.jsx
index 7961d502..71a30e39 100644
--- a/web/src/js/components/FlowTable/FlowRow.jsx
+++ b/web/src/js/components/FlowTable/FlowRow.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import classnames from 'classnames'
import columns from './FlowColumns'
import { pure } from '../../utils'
diff --git a/web/src/js/components/FlowTable/FlowTableHead.jsx b/web/src/js/components/FlowTable/FlowTableHead.jsx
index b201285f..59ad73e2 100644
--- a/web/src/js/components/FlowTable/FlowTableHead.jsx
+++ b/web/src/js/components/FlowTable/FlowTableHead.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
import columns from './FlowColumns'
@@ -7,8 +8,8 @@ import { setSort } from '../../ducks/flows'
FlowTableHead.propTypes = {
setSort: PropTypes.func.isRequired,
- sortDesc: React.PropTypes.bool.isRequired,
- sortColumn: React.PropTypes.string,
+ sortDesc: PropTypes.bool.isRequired,
+ sortColumn: PropTypes.string,
}
function FlowTableHead({ sortColumn, sortDesc, setSort }) {
diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx
index 2e181383..92e11465 100644
--- a/web/src/js/components/FlowView/Headers.jsx
+++ b/web/src/js/components/FlowView/Headers.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import ValueEditor from '../ValueEditor/ValueEditor'
import { Key } from '../../utils'
diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx
index 93c52660..4a31faf4 100644
--- a/web/src/js/components/FlowView/Messages.jsx
+++ b/web/src/js/components/FlowView/Messages.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
diff --git a/web/src/js/components/FlowView/Nav.jsx b/web/src/js/components/FlowView/Nav.jsx
index 37c073ce..af5a879e 100644
--- a/web/src/js/components/FlowView/Nav.jsx
+++ b/web/src/js/components/FlowView/Nav.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
diff --git a/web/src/js/components/FlowView/ToggleEdit.jsx b/web/src/js/components/FlowView/ToggleEdit.jsx
index 6a691a3d..b47b45db 100644
--- a/web/src/js/components/FlowView/ToggleEdit.jsx
+++ b/web/src/js/components/FlowView/ToggleEdit.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes, Component } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { startEdit, stopEdit } from '../../ducks/ui/flow'
diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx
index 58dd0dcb..08d15496 100644
--- a/web/src/js/components/Footer.jsx
+++ b/web/src/js/components/Footer.jsx
@@ -1,14 +1,15 @@
import React from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { formatSize } from '../utils.js'
Footer.propTypes = {
- settings: React.PropTypes.object.isRequired,
+ settings: PropTypes.object.isRequired,
}
function Footer({ settings }) {
let {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, websocket, anticache, anticomp,
- stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, version} = settings;
+ stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, version, server} = settings;
return (
<footer>
{mode && mode != "regular" && (
@@ -48,9 +49,11 @@ function Footer({ settings }) {
<span className="label label-success">stream: {formatSize(stream_large_bodies)}</span>
)}
<div className="pull-right">
- <span className="label label-primary" title="HTTP Proxy Server Address">
- {listen_host || "*"}:{listen_port}
- </span>
+ {server && (
+ <span className="label label-primary" title="HTTP Proxy Server Address">
+ {listen_host||"*"}:{listen_port}
+ </span>
+ )}
<span className="label label-info" title="Mitmproxy Version">
v{version}
</span>
diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx
index c15c951f..ebe7453c 100644
--- a/web/src/js/components/Header.jsx
+++ b/web/src/js/components/Header.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
import MainMenu from './Header/MainMenu'
@@ -6,6 +7,7 @@ import OptionMenu from './Header/OptionMenu'
import FileMenu from './Header/FileMenu'
import FlowMenu from './Header/FlowMenu'
import {setActiveMenu} from '../ducks/ui/header'
+import ConnectionIndicator from "./Header/ConnectionIndicator"
class Header extends Component {
static entries = [MainMenu, OptionMenu]
@@ -38,10 +40,11 @@ class Header extends Component {
{Entry.title}
</a>
))}
+ <ConnectionIndicator/>
</nav>
- <menu>
+ <div>
<Active/>
- </menu>
+ </div>
</header>
)
}
diff --git a/web/src/js/components/Header/ConnectionIndicator.jsx b/web/src/js/components/Header/ConnectionIndicator.jsx
new file mode 100644
index 00000000..1ee42e25
--- /dev/null
+++ b/web/src/js/components/Header/ConnectionIndicator.jsx
@@ -0,0 +1,30 @@
+import React from "react"
+import PropTypes from "prop-types"
+import { connect } from "react-redux"
+import { ConnectionState } from "../../ducks/connection"
+
+
+ConnectionIndicator.propTypes = {
+ state: PropTypes.symbol.isRequired,
+ message: PropTypes.string,
+
+}
+function ConnectionIndicator({ state, message }) {
+ switch (state) {
+ case ConnectionState.INIT:
+ return <span className="connection-indicator init">connecting…</span>;
+ case ConnectionState.FETCHING:
+ return <span className="connection-indicator fetching">fetching data…</span>;
+ case ConnectionState.ESTABLISHED:
+ return <span className="connection-indicator established">connected</span>;
+ case ConnectionState.ERROR:
+ return <span className="connection-indicator error"
+ title={message}>connection lost</span>;
+ case ConnectionState.OFFLINE:
+ return <span className="connection-indicator offline">offline</span>;
+ }
+}
+
+export default connect(
+ state => state.connection,
+)(ConnectionIndicator)
diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx
index ec32c857..1975d1cb 100644
--- a/web/src/js/components/Header/FileMenu.jsx
+++ b/web/src/js/components/Header/FileMenu.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import FileChooser from '../common/FileChooser'
import Dropdown, {Divider} from '../common/Dropdown'
diff --git a/web/src/js/components/Header/FilterInput.jsx b/web/src/js/components/Header/FilterInput.jsx
index 12479c10..44496d5b 100644
--- a/web/src/js/components/Header/FilterInput.jsx
+++ b/web/src/js/components/Header/FilterInput.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes, Component } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import classnames from 'classnames'
import { Key } from '../../utils.js'
diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx
index a404fdb7..fb61baf1 100644
--- a/web/src/js/components/Header/FlowMenu.jsx
+++ b/web/src/js/components/Header/FlowMenu.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react"
+import React from "react"
+import PropTypes from 'prop-types'
import { connect } from "react-redux"
import Button from "../common/Button"
import { MessageUtils } from "../../flow/utils.js"
diff --git a/web/src/js/components/Header/MainMenu.jsx b/web/src/js/components/Header/MainMenu.jsx
index 6a4e12bf..465649d7 100644
--- a/web/src/js/components/Header/MainMenu.jsx
+++ b/web/src/js/components/Header/MainMenu.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from "react"
+import React, { Component } from "react"
+import PropTypes from 'prop-types'
import { connect } from "react-redux"
import FilterInput from "./FilterInput"
import { update as updateSettings } from "../../ducks/settings"
diff --git a/web/src/js/components/Header/MenuToggle.jsx b/web/src/js/components/Header/MenuToggle.jsx
index 91f093c6..220a2b79 100644
--- a/web/src/js/components/Header/MenuToggle.jsx
+++ b/web/src/js/components/Header/MenuToggle.jsx
@@ -1,4 +1,4 @@
-import { PropTypes } from "react"
+import PropTypes from 'prop-types'
import { connect } from "react-redux"
import { update as updateSettings } from "../../ducks/settings"
import { toggleVisibility } from "../../ducks/eventLog"
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
index d6a8dfc2..b33d578d 100644
--- a/web/src/js/components/Header/OptionMenu.jsx
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react"
+import React from "react"
+import PropTypes from 'prop-types'
import { connect } from "react-redux"
import { SettingsToggle, EventlogToggle } from "./MenuToggle"
import DocsLink from "../common/DocsLink"
diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx
index 5c9a2d30..e2bedc88 100644
--- a/web/src/js/components/MainView.jsx
+++ b/web/src/js/components/MainView.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import Splitter from './common/Splitter'
import FlowTable from './FlowTable'
diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx
index 1c20b1a9..77b07027 100755
--- a/web/src/js/components/Prompt.jsx
+++ b/web/src/js/components/Prompt.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import _ from 'lodash'
diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx
index 18976de0..af5b3caa 100644
--- a/web/src/js/components/ProxyApp.jsx
+++ b/web/src/js/components/ProxyApp.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { onKeyDown } from '../ducks/ui/keyboard'
diff --git a/web/src/js/components/ValueEditor/ValidateEditor.jsx b/web/src/js/components/ValueEditor/ValidateEditor.jsx
index 7415c1b8..27b8ca48 100755
--- a/web/src/js/components/ValueEditor/ValidateEditor.jsx
+++ b/web/src/js/components/ValueEditor/ValidateEditor.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import ValueEditor from './ValueEditor'
import classnames from 'classnames'
diff --git a/web/src/js/components/ValueEditor/ValueEditor.jsx b/web/src/js/components/ValueEditor/ValueEditor.jsx
index 852f82c4..9301c181 100644
--- a/web/src/js/components/ValueEditor/ValueEditor.jsx
+++ b/web/src/js/components/ValueEditor/ValueEditor.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import _ from "lodash"
import classnames from 'classnames'
diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx
index f05a68d0..e02ae010 100644
--- a/web/src/js/components/common/Button.jsx
+++ b/web/src/js/components/common/Button.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from "react"
+import React from "react"
+import PropTypes from 'prop-types'
import classnames from "classnames"
Button.propTypes = {
diff --git a/web/src/js/components/common/DocsLink.jsx b/web/src/js/components/common/DocsLink.jsx
index 182811a3..70974133 100644
--- a/web/src/js/components/common/DocsLink.jsx
+++ b/web/src/js/components/common/DocsLink.jsx
@@ -1,4 +1,5 @@
-import { PropTypes } from 'react'
+import React from "react"
+import PropTypes from "prop-types"
DocsLink.propTypes = {
resource: PropTypes.string.isRequired,
diff --git a/web/src/js/components/common/Dropdown.jsx b/web/src/js/components/common/Dropdown.jsx
index cc95a6dc..991e127e 100644
--- a/web/src/js/components/common/Dropdown.jsx
+++ b/web/src/js/components/common/Dropdown.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import classnames from 'classnames'
export const Divider = () => <hr className="divider"/>
diff --git a/web/src/js/components/common/FileChooser.jsx b/web/src/js/components/common/FileChooser.jsx
index d59d2d6d..0b14a87e 100644
--- a/web/src/js/components/common/FileChooser.jsx
+++ b/web/src/js/components/common/FileChooser.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
FileChooser.propTypes = {
icon: PropTypes.string,
@@ -20,7 +21,7 @@ export default function FileChooser({ icon, text, className, title, onOpenFile }
ref={ref => fileInput = ref}
className="hidden"
type="file"
- onChange={e => { e.preventDefault(); if(e.target.files.length > 0) onOpenFile(e.target.files[0]); fileInput = "";}}
+ onChange={e => { e.preventDefault(); if(e.target.files.length > 0) onOpenFile(e.target.files[0]); fileInput.value="";}}
/>
</a>
)
diff --git a/web/src/js/components/common/ToggleButton.jsx b/web/src/js/components/common/ToggleButton.jsx
index 6027728b..925d3c39 100644
--- a/web/src/js/components/common/ToggleButton.jsx
+++ b/web/src/js/components/common/ToggleButton.jsx
@@ -1,4 +1,5 @@
-import React, { PropTypes } from 'react'
+import React from 'react'
+import PropTypes from 'prop-types'
ToggleButton.propTypes = {
checked: PropTypes.bool.isRequired,
diff --git a/web/src/js/components/common/ToggleInputButton.jsx b/web/src/js/components/common/ToggleInputButton.jsx
index 5fa24c10..2607fb66 100644
--- a/web/src/js/components/common/ToggleInputButton.jsx
+++ b/web/src/js/components/common/ToggleInputButton.jsx
@@ -1,4 +1,5 @@
-import React, { Component, PropTypes } from 'react'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
import classnames from 'classnames'
import { Key } from '../../utils'
diff --git a/web/src/js/ducks/connection.js b/web/src/js/ducks/connection.js
new file mode 100644
index 00000000..ffa2c309
--- /dev/null
+++ b/web/src/js/ducks/connection.js
@@ -0,0 +1,44 @@
+export const ConnectionState = {
+ INIT: Symbol("init"),
+ FETCHING: Symbol("fetching"), // WebSocket is established, but still startFetching resources.
+ ESTABLISHED: Symbol("established"),
+ ERROR: Symbol("error"),
+ OFFLINE: Symbol("offline"), // indicates that there is no live (websocket) backend.
+}
+
+const defaultState = {
+ state: ConnectionState.INIT,
+ message: null,
+}
+
+export default function reducer(state = defaultState, action) {
+ switch (action.type) {
+
+ case ConnectionState.ESTABLISHED:
+ case ConnectionState.FETCHING:
+ case ConnectionState.ERROR:
+ case ConnectionState.OFFLINE:
+ return {
+ state: action.type,
+ message: action.message
+ }
+
+ default:
+ return state
+ }
+}
+
+export function startFetching() {
+ return { type: ConnectionState.FETCHING }
+}
+
+export function connectionEstablished() {
+ return { type: ConnectionState.ESTABLISHED }
+}
+
+export function connectionError(message) {
+ return { type: ConnectionState.ERROR, message }
+}
+export function setOffline() {
+ return { type: ConnectionState.OFFLINE }
+}
diff --git a/web/src/js/ducks/eventLog.js b/web/src/js/ducks/eventLog.js
index 73eaf2e8..8f9ec34d 100644
--- a/web/src/js/ducks/eventLog.js
+++ b/web/src/js/ducks/eventLog.js
@@ -8,7 +8,7 @@ export const TOGGLE_FILTER = 'EVENTS_TOGGLE_FILTER'
const defaultState = {
visible: false,
- filters: { debug: false, info: true, web: true },
+ filters: { debug: false, info: true, web: true, warn: true, error: true },
...reduceStore(undefined, {}),
}
diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js
index 92408891..523ec396 100644
--- a/web/src/js/ducks/flows.js
+++ b/web/src/js/ducks/flows.js
@@ -1,5 +1,6 @@
import { fetchApi } from "../utils"
-import reduceStore, * as storeActions from "./utils/store"
+import reduceStore from "./utils/store"
+import * as storeActions from "./utils/store"
import Filt from "../filt/filt"
import { RequestUtils } from "../flow/utils"
@@ -29,8 +30,6 @@ export default function reduce(state = defaultState, action) {
case UPDATE:
case REMOVE:
case RECEIVE:
- // FIXME: Update state.selected on REMOVE:
- // The selected flow may have been removed, we need to select the next one in the view.
let storeAction = storeActions[action.cmd](
action.data,
makeFilter(state.filter),
@@ -152,22 +151,20 @@ export function setSort(column, desc) {
return { type: SET_SORT, sort: { column, desc } }
}
-export function selectRelative(shift) {
- return (dispatch, getState) => {
- let currentSelectionIndex = getState().flows.viewIndex[getState().flows.selected[0]]
- let minIndex = 0
- let maxIndex = getState().flows.view.length - 1
- let newIndex
- if (currentSelectionIndex === undefined) {
- newIndex = (shift < 0) ? minIndex : maxIndex
- } else {
- newIndex = currentSelectionIndex + shift
- newIndex = window.Math.max(newIndex, minIndex)
- newIndex = window.Math.min(newIndex, maxIndex)
- }
- let flow = getState().flows.view[newIndex]
- dispatch(select(flow ? flow.id : undefined))
+export function selectRelative(flows, shift) {
+ let currentSelectionIndex = flows.viewIndex[flows.selected[0]]
+ let minIndex = 0
+ let maxIndex = flows.view.length - 1
+ let newIndex
+ if (currentSelectionIndex === undefined) {
+ newIndex = (shift < 0) ? minIndex : maxIndex
+ } else {
+ newIndex = currentSelectionIndex + shift
+ newIndex = window.Math.max(newIndex, minIndex)
+ newIndex = window.Math.min(newIndex, maxIndex)
}
+ let flow = flows.view[newIndex]
+ return select(flow ? flow.id : undefined)
}
@@ -212,7 +209,7 @@ export function uploadContent(flow, file, type) {
const body = new FormData()
file = new window.Blob([file], { type: 'plain/text' })
body.append('file', file)
- return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, { method: 'post', body })
+ return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, { method: 'POST', body })
}
@@ -228,7 +225,7 @@ export function download() {
export function upload(file) {
const body = new FormData()
body.append('file', file)
- return dispatch => fetchApi('/flows/dump', { method: 'post', body })
+ return dispatch => fetchApi('/flows/dump', { method: 'POST', body })
}
diff --git a/web/src/js/ducks/index.js b/web/src/js/ducks/index.js
index 753075fa..0f2426ec 100644
--- a/web/src/js/ducks/index.js
+++ b/web/src/js/ducks/index.js
@@ -1,12 +1,14 @@
-import { combineReducers } from 'redux'
-import eventLog from './eventLog'
-import flows from './flows'
-import settings from './settings'
-import ui from './ui/index'
+import { combineReducers } from "redux"
+import eventLog from "./eventLog"
+import flows from "./flows"
+import settings from "./settings"
+import ui from "./ui/index"
+import connection from "./connection"
export default combineReducers({
eventLog,
flows,
settings,
+ connection,
ui,
})
diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js
index ba604ea2..51ad4184 100644
--- a/web/src/js/ducks/ui/flow.js
+++ b/web/src/js/ducks/ui/flow.js
@@ -26,7 +26,7 @@ const defaultState = {
}
export default function reducer(state = defaultState, action) {
- let wasInEditMode = !!(state.modifiedFlow)
+ let wasInEditMode = state.modifiedFlow
let content = action.content || state.content
let isFullContentShown = content && content.length <= state.maxContentLines
@@ -89,14 +89,14 @@ export default function reducer(state = defaultState, action) {
...state,
tab: action.tab ? action.tab : 'request',
displayLarge: false,
- showFullContent: state.contentView == 'Edit'
+ showFullContent: state.contentView === 'Edit'
}
case SET_CONTENT_VIEW:
return {
...state,
contentView: action.contentView,
- showFullContent: action.contentView == 'Edit'
+ showFullContent: action.contentView === 'Edit'
}
case SET_CONTENT:
diff --git a/web/src/js/ducks/ui/header.js b/web/src/js/ducks/ui/header.js
index 6581149e..274d82aa 100644
--- a/web/src/js/ducks/ui/header.js
+++ b/web/src/js/ducks/ui/header.js
@@ -30,7 +30,7 @@ export default function reducer(state = defaultState, action) {
// Deselect
if (action.flowIds.length === 0 && state.isFlowSelected) {
let activeMenu = state.activeMenu
- if (activeMenu == 'Flow') {
+ if (activeMenu === 'Flow') {
activeMenu = 'Start'
}
return {
diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js
index 30fd76e1..0e3491fa 100644
--- a/web/src/js/ducks/ui/keyboard.js
+++ b/web/src/js/ducks/ui/keyboard.js
@@ -9,39 +9,40 @@ export function onKeyDown(e) {
return () => {
}
}
- var key = e.keyCode
- var shiftKey = e.shiftKey
+ let key = e.keyCode,
+ shiftKey = e.shiftKey
e.preventDefault()
return (dispatch, getState) => {
- const flow = getState().flows.byId[getState().flows.selected[0]]
+ const flows = getState().flows,
+ flow = flows.byId[getState().flows.selected[0]]
switch (key) {
case Key.K:
case Key.UP:
- dispatch(flowsActions.selectRelative(-1))
+ dispatch(flowsActions.selectRelative(flows, -1))
break
case Key.J:
case Key.DOWN:
- dispatch(flowsActions.selectRelative(+1))
+ dispatch(flowsActions.selectRelative(flows, +1))
break
case Key.SPACE:
case Key.PAGE_DOWN:
- dispatch(flowsActions.selectRelative(+10))
+ dispatch(flowsActions.selectRelative(flows, +10))
break
case Key.PAGE_UP:
- dispatch(flowsActions.selectRelative(-10))
+ dispatch(flowsActions.selectRelative(flows, -10))
break
case Key.END:
- dispatch(flowsActions.selectRelative(+1e10))
+ dispatch(flowsActions.selectRelative(flows, +1e10))
break
case Key.HOME:
- dispatch(flowsActions.selectRelative(-1e10))
+ dispatch(flowsActions.selectRelative(flows, -1e10))
break
case Key.ESC:
diff --git a/web/src/js/filt/filt.js b/web/src/js/filt/filt.js
index 2252f957..26058649 100644
--- a/web/src/js/filt/filt.js
+++ b/web/src/js/filt/filt.js
@@ -1953,7 +1953,7 @@ module.exports = (function() {
function domain(regex){
regex = new RegExp(regex, "i");
function domainFilter(flow){
- return flow.request && regex.test(flow.request.host);
+ return flow.request && (regex.test(flow.request.host) || regex.test(flow.request.pretty_host));
}
domainFilter.desc = "domain matches " + regex;
return domainFilter;
diff --git a/web/src/js/urlState.js b/web/src/js/urlState.js
index ca9187b2..7802bdb8 100644
--- a/web/src/js/urlState.js
+++ b/web/src/js/urlState.js
@@ -15,7 +15,7 @@ const Query = {
SHOW_EVENTLOG: "e"
};
-function updateStoreFromUrl(store) {
+export function updateStoreFromUrl(store) {
const [path, query] = window.location.hash.substr(1).split("?", 2)
const path_components = path.substr(1).split("/")
@@ -50,7 +50,7 @@ function updateStoreFromUrl(store) {
}
}
-function updateUrlFromStore(store) {
+export function updateUrlFromStore(store) {
const state = store.getState()
let query = {
[Query.SEARCH]: state.flows.filter,
diff --git a/web/yarn.lock b/web/yarn.lock
index 6bdc7907..602b4916 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -1279,14 +1279,14 @@ content-type@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
-convert-source-map@1.X, convert-source-map@^1.2.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.4.0.tgz#e3dad195bf61bfe13a7a3c73e9876ec14a0268f3"
-
-convert-source-map@^1.1.0, convert-source-map@~1.1.0:
+convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@~1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
+convert-source-map@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.4.0.tgz#e3dad195bf61bfe13a7a3c73e9876ec14a0268f3"
+
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@@ -1860,7 +1860,7 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"
-fbjs@^0.8.1, fbjs@^0.8.4:
+fbjs@^0.8.4, fbjs@^0.8.9:
version "0.8.9"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14"
dependencies:
@@ -2461,14 +2461,10 @@ https-browserify@~0.0.0:
version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
-iconv-lite@0.4.13:
+iconv-lite@0.4.13, iconv-lite@~0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
-iconv-lite@~0.4.13:
- version "0.4.15"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
-
ieee754@^1.1.4:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
@@ -4068,6 +4064,12 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
+prop-types@^15.5.0, prop-types@~15.5.7:
+ version "15.5.8"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394"
+ dependencies:
+ fbjs "^0.8.9"
+
prr@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
@@ -4154,12 +4156,13 @@ react-codemirror@^0.3.0:
lodash.debounce "^4.0.8"
react-dom@^15.4.2:
- version "15.4.2"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f"
+ version "15.5.4"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da"
dependencies:
- fbjs "^0.8.1"
+ fbjs "^0.8.9"
loose-envify "^1.1.0"
object-assign "^4.1.0"
+ prop-types "~15.5.7"
react-redux@^5.0.2:
version "5.0.2"
@@ -4171,6 +4174,13 @@ react-redux@^5.0.2:
lodash-es "^4.2.0"
loose-envify "^1.1.0"
+react-test-renderer@^15.5.4:
+ version "15.5.4"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.5.4.tgz#d4ebb23f613d685ea8f5390109c2d20fbf7c83bc"
+ dependencies:
+ fbjs "^0.8.9"
+ object-assign "^4.1.0"
+
react@^15.4.2:
version "15.4.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef"
@@ -4289,6 +4299,10 @@ redux-logger@^2.8.1:
dependencies:
deep-diff "0.3.4"
+redux-mock-store@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.2.3.tgz#1b3ad299da91cb41ba30d68e3b6f024475fb9e1b"
+
redux-thunk@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"