From d7bbfca167e56510eb4b9cf634f17a1cf0159a6a Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Sun, 2 Jul 2017 12:17:16 +0800 Subject: [PATCH 1/8] [web] Change the response format of GET /options. --- mitmproxy/optmanager.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index 68c2f9753..5692d7512 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -413,22 +413,21 @@ def dump_dicts(opts): """ Dumps the options into a list of dict object. - Return: A list like: [ { name: "anticache", type: "bool", default: false, value: true, help: "help text"} ] + Return: A list like: { "anticache": { type: "bool", default: false, value: true, help: "help text"} } """ - options_list = [] + options_dict = {} for k in sorted(opts.keys()): o = opts._options[k] t = typecheck.typespec_to_str(o.typespec) option = { - 'name': k, 'type': t, 'default': o.default, 'value': o.current(), 'help': o.help, 'choices': o.choices } - options_list.append(option) - return options_list + options_dict[k] = option + return options_dict def parse(text): From 2e6f56c4e7de1d375eb91e188c3697383b31638f Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Sun, 2 Jul 2017 12:19:32 +0800 Subject: [PATCH 2/8] [web] Try toggle options in option modal. --- web/src/js/backends/websocket.js | 1 + web/src/js/components/Modal/OptionModal.jsx | 6 ++- web/src/js/components/Modal/OptionTypes.jsx | 48 +++++++++++++++++++++ web/src/js/ducks/index.js | 2 + 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 web/src/js/components/Modal/OptionTypes.jsx diff --git a/web/src/js/backends/websocket.js b/web/src/js/backends/websocket.js index 01094ac45..d7e13bb2a 100644 --- a/web/src/js/backends/websocket.js +++ b/web/src/js/backends/websocket.js @@ -27,6 +27,7 @@ export default class WebsocketBackend { this.fetchData("settings") this.fetchData("flows") this.fetchData("events") + this.fetchData("options") this.store.dispatch(connectionActions.startFetching()) } diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index 500495c42..c9a56d099 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -1,6 +1,8 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import * as modalAction from '../../ducks/ui/modal' +import { SettingsToggle } from '../Header/MenuToggle' +import { OptionsToggle } from './OptionTypes' class PureOptionModal extends Component { @@ -26,7 +28,9 @@ class PureOptionModal extends Component {
- ... + HTTP/2.0 + Anticache + Anticomp
diff --git a/web/src/js/components/Modal/OptionTypes.jsx b/web/src/js/components/Modal/OptionTypes.jsx new file mode 100644 index 000000000..8a9325e13 --- /dev/null +++ b/web/src/js/components/Modal/OptionTypes.jsx @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { update as updateOptions } from '../../ducks/options' + +MenuToggle.propTypes = { + value: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + children: PropTypes.node.isRequired, +} + +export function MenuToggle({ value, onChange, children }) { + return ( +
+ +
+ ) +} + +OptionsToggle.propTypes = { + option: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +} + +export function OptionsToggle({ option, children, options, updateOptions }) { + return ( + {console.log(options[option]); + updateOptions({ [option]: !(options[option].value)}) }} + > + {children} + + ) +} + +OptionsToggle = connect( + state => ({ + options: state.options, + }), + { + updateOptions, + } +)(OptionsToggle) diff --git a/web/src/js/ducks/index.js b/web/src/js/ducks/index.js index 0f2426ecf..be2f28850 100644 --- a/web/src/js/ducks/index.js +++ b/web/src/js/ducks/index.js @@ -4,6 +4,7 @@ import flows from "./flows" import settings from "./settings" import ui from "./ui/index" import connection from "./connection" +import options from './options' export default combineReducers({ eventLog, @@ -11,4 +12,5 @@ export default combineReducers({ settings, connection, ui, + options, }) From aa01a62df7ce015ba4bf3c5ba9f3074616530496 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Wed, 5 Jul 2017 08:40:21 +0800 Subject: [PATCH 3/8] [web] Add keys argument for dump_dict in optmanager. --- mitmproxy/optmanager.py | 5 +- test/mitmproxy/test_optmanager.py | 1 + test/mitmproxy/tools/web/test_app.py | 4 +- web/src/js/components/Modal/OptionMaster.jsx | 138 +++++++++++++++++++ web/src/js/components/Modal/OptionTypes.jsx | 48 ------- 5 files changed, 144 insertions(+), 52 deletions(-) create mode 100644 web/src/js/components/Modal/OptionMaster.jsx delete mode 100644 web/src/js/components/Modal/OptionTypes.jsx diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index 5692d7512..e1d74b8e0 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -409,14 +409,15 @@ def dump_defaults(opts): return ruamel.yaml.round_trip_dump(s) -def dump_dicts(opts): +def dump_dicts(opts, keys: typing.List[str]=None): """ Dumps the options into a list of dict object. Return: A list like: { "anticache": { type: "bool", default: false, value: true, help: "help text"} } """ options_dict = {} - for k in sorted(opts.keys()): + keys = keys if keys else opts.keys() + for k in sorted(keys): o = opts._options[k] t = typecheck.typespec_to_str(o.typespec) option = { diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index 0c4006838..7b4ffb8b0 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -341,6 +341,7 @@ def test_dump_defaults(): def test_dump_dicts(): o = options.Options() assert optmanager.dump_dicts(o) + assert optmanager.dump_dicts(o, ['http2', 'anticomp']) class TTypes(optmanager.OptManager): diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index e6d563e7f..044b45951 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -255,8 +255,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): def test_options(self): j = json(self.fetch("/options")) - assert type(j) == list - assert type(j[0]) == dict + assert type(j) == dict + assert type(j['anticache']) == dict def test_option_update(self): assert self.put_json("/options", {"anticache": True}).code == 200 diff --git a/web/src/js/components/Modal/OptionMaster.jsx b/web/src/js/components/Modal/OptionMaster.jsx new file mode 100644 index 000000000..e16319fb0 --- /dev/null +++ b/web/src/js/components/Modal/OptionMaster.jsx @@ -0,0 +1,138 @@ +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { update as updateOptions } from '../../ducks/options' + +PureBooleanOption.PropTypes = { + value: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, +} + +function PureBooleanOption({ value, onChange, name, help}) { + return ( +
+ +
+ ) +} + +PureStringOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} + +function PureStringOption( { value, onChange, name, help }) { + let onKeyDown = (e) => {e.stopPropagation()} + return ( +
+ +
+ ) +} + +PureNumberOption.PropTypes = { + value: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, +} + +function PureNumberOption( {value, onChange, name, help }) { + let onKeyDown = (e) => {e.stopPropagation()} + return ( +
+ +
+ ) +} + +PureChoicesOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} + +function PureChoicesOption( { value, onChange, name, help, choices }) { + return ( +
+ +
+ ) +} + +const OptionTypes = { + bool: PureBooleanOption, + str: PureStringOption, + int: PureNumberOption, + "optional str": PureStringOption, + "sequence of str": PureStringOption, +} + +Wrapper.displayName = 'OptionWrapper' + + +function Wrapper({option, options, updateOptions, ...props}) { + let optionObj = options[option], + WrappedComponent = null + if (optionObj.choices) { + WrappedComponent = PureChoicesOption + } else { + WrappedComponent = OptionTypes[optionObj.type] + } + + let onChange = (e) => { + switch (optionObj.type) { + case 'bool' : + updateOptions({[option]: !optionObj.value}) + break + case 'int': + updateOptions({[option]: parseInt(e.target.value)}) + break + default: + updateOptions({[option]: e.target.value}) + } + } + return +} + +export default connect( + state => ({ + options: state.options, + }), + { + updateOptions, + } +)(Wrapper) diff --git a/web/src/js/components/Modal/OptionTypes.jsx b/web/src/js/components/Modal/OptionTypes.jsx deleted file mode 100644 index 8a9325e13..000000000 --- a/web/src/js/components/Modal/OptionTypes.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types' -import { connect } from 'react-redux' -import { update as updateOptions } from '../../ducks/options' - -MenuToggle.propTypes = { - value: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - children: PropTypes.node.isRequired, -} - -export function MenuToggle({ value, onChange, children }) { - return ( -
- -
- ) -} - -OptionsToggle.propTypes = { - option: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, -} - -export function OptionsToggle({ option, children, options, updateOptions }) { - return ( - {console.log(options[option]); - updateOptions({ [option]: !(options[option].value)}) }} - > - {children} - - ) -} - -OptionsToggle = connect( - state => ({ - options: state.options, - }), - { - updateOptions, - } -)(OptionsToggle) From c1553c7602f43f2b566435b765c285596d03c2f5 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Wed, 5 Jul 2017 08:42:41 +0800 Subject: [PATCH 4/8] [web] Broadcast options update in backend. --- mitmproxy/tools/web/master.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mitmproxy/tools/web/master.py b/mitmproxy/tools/web/master.py index c09fe0a20..8c2433ec6 100644 --- a/mitmproxy/tools/web/master.py +++ b/mitmproxy/tools/web/master.py @@ -5,6 +5,7 @@ import tornado.ioloop from mitmproxy import addons from mitmproxy import log from mitmproxy import master +from mitmproxy import optmanager from mitmproxy.addons import eventstore from mitmproxy.addons import intercept from mitmproxy.addons import readfile @@ -29,6 +30,7 @@ class WebMaster(master.Master): self.events.sig_refresh.connect(self._sig_events_refresh) self.options.changed.connect(self._sig_options_update) + self.options.changed.connect(self._sig_settings_update) self.addons.add(*addons.default_addons()) self.addons.add( @@ -86,6 +88,14 @@ class WebMaster(master.Master): ) def _sig_options_update(self, options, updated): + options_dict = optmanager.dump_dicts(options, updated) + app.ClientConnection.broadcast( + resource="options", + cmd="update", + data=options_dict + ) + + def _sig_settings_update(self, options, updated): app.ClientConnection.broadcast( resource="settings", cmd="update", From d889892ba570297047eb4ada342ddf5584b0e43d Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Wed, 5 Jul 2017 08:45:31 +0800 Subject: [PATCH 5/8] [web] List all options in option modal. --- web/src/js/components/Modal/OptionModal.jsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index c9a56d099..582ac55fd 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -1,18 +1,17 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import * as modalAction from '../../ducks/ui/modal' -import { SettingsToggle } from '../Header/MenuToggle' -import { OptionsToggle } from './OptionTypes' +import Option from './OptionMaster' class PureOptionModal extends Component { constructor(props, context) { super(props, context) - this.state = { title: 'Options', } + this.state = { title: 'Options' } } render() { - const { hideModal } = this.props + const { hideModal, options } = this.props const { title } = this.state return (
@@ -28,9 +27,11 @@ class PureOptionModal extends Component {
- HTTP/2.0 - Anticache - Anticomp + { + Object.keys(options).sort().map((key) => ( +
@@ -43,7 +44,7 @@ class PureOptionModal extends Component { export default connect( state => ({ - + options: state.options }), { hideModal: modalAction.hideModal } )(PureOptionModal) From e8f3b740c944401a01d3c57e8d1c6051e92b8236 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Wed, 5 Jul 2017 08:46:16 +0800 Subject: [PATCH 6/8] [web] Update css for modal. --- web/src/css/app.less | 1 + web/src/css/modal.less | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/web/src/css/app.less b/web/src/css/app.less index 353e412a4..b9b5b310a 100644 --- a/web/src/css/app.less +++ b/web/src/css/app.less @@ -19,3 +19,4 @@ html { @import (less) "footer.less"; @import (less) "codemirror.less"; @import (less) "contentview.less"; +@import (less) "modal.less"; diff --git a/web/src/css/modal.less b/web/src/css/modal.less index b08e309ae..8d578f03d 100644 --- a/web/src/css/modal.less +++ b/web/src/css/modal.less @@ -1,3 +1,13 @@ .modal-visible { display: block; } + + +.modal-dialog { + overflow-y: initial !important; +} + +.modal-body { + max-height: calc(100vh - 20px); + overflow-y: auto; +} From 7516f706b41de0c10c2bf9b88278afb6e3edcd4a Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Wed, 5 Jul 2017 18:55:56 +0800 Subject: [PATCH 7/8] [web] Update Option Modal UI and its tests. --- .../Modal/__snapshots__/ModalSpec.js.snap | 79 ++++++++++++++++++- web/src/js/__tests__/ducks/tutils.js | 30 +++++++ web/src/js/components/Modal/OptionMaster.jsx | 65 ++++++--------- web/src/js/components/Modal/OptionModal.jsx | 20 ++++- 4 files changed, 147 insertions(+), 47 deletions(-) diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap index 4fe163d1d..af587ae42 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap @@ -46,7 +46,84 @@ exports[`Modal Component should render correctly 2`] = `
- ... +
+ +
+
+ +
+
+ +
+
+ +
-
) } @@ -30,7 +26,6 @@ PureStringOption.PropTypes = { function PureStringOption( { value, onChange, name, help }) { let onKeyDown = (e) => {e.stopPropagation()} return ( -
-
) } @@ -52,7 +46,6 @@ PureNumberOption.PropTypes = { function PureNumberOption( {value, onChange, name, help }) { let onKeyDown = (e) => {e.stopPropagation()} return ( -
-
) } @@ -73,16 +65,14 @@ PureChoicesOption.PropTypes = { function PureChoicesOption( { value, onChange, name, help, choices }) { return ( -
-
) } @@ -94,45 +84,36 @@ const OptionTypes = { "sequence of str": PureStringOption, } -Wrapper.displayName = 'OptionWrapper' - - -function Wrapper({option, options, updateOptions, ...props}) { - let optionObj = options[option], - WrappedComponent = null - if (optionObj.choices) { +export default function OptionMaster({option, name, updateOptions, ...props}) { + let WrappedComponent = null + if (option.choices) { WrappedComponent = PureChoicesOption } else { - WrappedComponent = OptionTypes[optionObj.type] + WrappedComponent = OptionTypes[option.type] } let onChange = (e) => { - switch (optionObj.type) { + switch (option.type) { case 'bool' : - updateOptions({[option]: !optionObj.value}) + updateOptions({[name]: !option.value}) break case 'int': - updateOptions({[option]: parseInt(e.target.value)}) + updateOptions({[name]: parseInt(e.target.value)}) break default: - updateOptions({[option]: e.target.value}) + updateOptions({[name]: e.target.value}) } } - return + return ( +
+ +
+ ) } - -export default connect( - state => ({ - options: state.options, - }), - { - updateOptions, - } -)(Wrapper) diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index 582ac55fd..ef3a224af 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -1,6 +1,7 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import * as modalAction from '../../ducks/ui/modal' +import { update as updateOptions } from '../../ducks/options' import Option from './OptionMaster' class PureOptionModal extends Component { @@ -28,9 +29,17 @@ class PureOptionModal extends Component {
{ - Object.keys(options).sort().map((key) => ( -
@@ -46,5 +55,8 @@ export default connect( state => ({ options: state.options }), - { hideModal: modalAction.hideModal } + { + hideModal: modalAction.hideModal, + updateOptions: updateOptions, + } )(PureOptionModal) From 37fea267c1d171fb661736b96db62943e7b49791 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Wed, 5 Jul 2017 18:57:19 +0800 Subject: [PATCH 8/8] Fix the test for websocket connection. --- test/mitmproxy/tools/web/test_app.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index 044b45951..bb439b340 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -275,12 +275,32 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): ws_client = yield websocket.websocket_connect(ws_url) self.master.options.anticomp = True - response = yield ws_client.read_message() - assert _json.loads(response) == { + r1 = yield ws_client.read_message() + r2 = yield ws_client.read_message() + j1 = _json.loads(r1) + j2 = _json.loads(r2) + print(j1) + response = dict() + response[j1['resource']] = j1 + response[j2['resource']] = j2 + assert response['settings'] == { "resource": "settings", "cmd": "update", "data": {"anticomp": True}, } + assert response['options'] == { + "resource": "options", + "cmd": "update", + "data": { + "anticomp": { + "value": True, + "choices": None, + "default": False, + "help": "Try to convince servers to send us un-compressed data.", + "type": "bool", + } + } + } ws_client.close() # trigger on_close by opening a second connection.