Merge pull request #2423 from MatthewShao/mitmweb-options

[web] [WIP] Mitmweb options editor content
This commit is contained in:
Maximilian Hils 2017-07-05 16:25:38 +02:00 committed by GitHub
commit 062a58f848
12 changed files with 305 additions and 17 deletions

View File

@ -409,26 +409,26 @@ def dump_defaults(opts):
return ruamel.yaml.round_trip_dump(s) 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. 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()): keys = keys if keys else opts.keys()
for k in sorted(keys):
o = opts._options[k] o = opts._options[k]
t = typecheck.typespec_to_str(o.typespec) t = typecheck.typespec_to_str(o.typespec)
option = { option = {
'name': k,
'type': t, 'type': t,
'default': o.default, 'default': o.default,
'value': o.current(), 'value': o.current(),
'help': o.help, 'help': o.help,
'choices': o.choices 'choices': o.choices
} }
options_list.append(option) options_dict[k] = option
return options_list return options_dict
def parse(text): def parse(text):

View File

@ -5,6 +5,7 @@ import tornado.ioloop
from mitmproxy import addons from mitmproxy import addons
from mitmproxy import log from mitmproxy import log
from mitmproxy import master from mitmproxy import master
from mitmproxy import optmanager
from mitmproxy.addons import eventstore from mitmproxy.addons import eventstore
from mitmproxy.addons import intercept from mitmproxy.addons import intercept
from mitmproxy.addons import readfile from mitmproxy.addons import readfile
@ -29,6 +30,7 @@ class WebMaster(master.Master):
self.events.sig_refresh.connect(self._sig_events_refresh) self.events.sig_refresh.connect(self._sig_events_refresh)
self.options.changed.connect(self._sig_options_update) 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(*addons.default_addons())
self.addons.add( self.addons.add(
@ -86,6 +88,14 @@ class WebMaster(master.Master):
) )
def _sig_options_update(self, options, updated): 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( app.ClientConnection.broadcast(
resource="settings", resource="settings",
cmd="update", cmd="update",

View File

@ -341,6 +341,7 @@ def test_dump_defaults():
def test_dump_dicts(): def test_dump_dicts():
o = options.Options() o = options.Options()
assert optmanager.dump_dicts(o) assert optmanager.dump_dicts(o)
assert optmanager.dump_dicts(o, ['http2', 'anticomp'])
class TTypes(optmanager.OptManager): class TTypes(optmanager.OptManager):

View File

@ -255,8 +255,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
def test_options(self): def test_options(self):
j = json(self.fetch("/options")) j = json(self.fetch("/options"))
assert type(j) == list assert type(j) == dict
assert type(j[0]) == dict assert type(j['anticache']) == dict
def test_option_update(self): def test_option_update(self):
assert self.put_json("/options", {"anticache": True}).code == 200 assert self.put_json("/options", {"anticache": True}).code == 200
@ -275,12 +275,32 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
ws_client = yield websocket.websocket_connect(ws_url) ws_client = yield websocket.websocket_connect(ws_url)
self.master.options.anticomp = True self.master.options.anticomp = True
response = yield ws_client.read_message() r1 = yield ws_client.read_message()
assert _json.loads(response) == { 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", "resource": "settings",
"cmd": "update", "cmd": "update",
"data": {"anticomp": True}, "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() ws_client.close()
# trigger on_close by opening a second connection. # trigger on_close by opening a second connection.

View File

@ -19,3 +19,4 @@ html {
@import (less) "footer.less"; @import (less) "footer.less";
@import (less) "codemirror.less"; @import (less) "codemirror.less";
@import (less) "contentview.less"; @import (less) "contentview.less";
@import (less) "modal.less";

View File

@ -1,3 +1,13 @@
.modal-visible { .modal-visible {
display: block; display: block;
} }
.modal-dialog {
overflow-y: initial !important;
}
.modal-body {
max-height: calc(100vh - 20px);
overflow-y: auto;
}

View File

@ -46,7 +46,84 @@ exports[`Modal Component should render correctly 2`] = `
<div <div
className="modal-body" className="modal-body"
> >
... <div
className="menu-entry"
>
<label>
booleanOption
<input
checked={false}
onChange={[Function]}
title="foo"
type="checkbox"
/>
</label>
</div>
<div
className="menu-entry"
>
<label
htmlFor=""
>
choiceOption
<select
name="choiceOption"
onChange={[Function]}
selected="b"
title="foo"
>
<option
value="a"
>
a
</option>
<option
value="b"
>
b
</option>
<option
value="c"
>
c
</option>
</select>
</label>
</div>
<div
className="menu-entry"
>
<label>
intOption
<input
onChange={[Function]}
onKeyDown={[Function]}
title="foo"
type="number"
value={1}
/>
</label>
</div>
<div
className="menu-entry"
>
<label>
strOption
<input
onChange={[Function]}
onKeyDown={[Function]}
title="foo"
type="text"
value="str content"
/>
</label>
</div>
</div> </div>
<div <div
className="modal-footer" className="modal-footer"

View File

@ -42,6 +42,36 @@ export function TStore(){
anticache: true, anticache: true,
anticomp: false anticomp: false
}, },
options: {
booleanOption: {
choices: null,
default: false,
help: "foo",
type: "bool",
value: false
},
strOption: {
choices: null,
default: null,
help: "foo",
type: "str",
value: "str content"
},
intOption: {
choices: null,
default: 0,
help: "foo",
type: "int",
value: 1
},
choiceOption: {
choices: ['a', 'b', 'c'],
default: 'a',
help: "foo",
type: "str",
value: "b"
},
},
flows: { flows: {
selected: ["d91165be-ca1f-4612-88a9-c0f8696f3e29"], selected: ["d91165be-ca1f-4612-88a9-c0f8696f3e29"],
byId: {"d91165be-ca1f-4612-88a9-c0f8696f3e29": tflow}, byId: {"d91165be-ca1f-4612-88a9-c0f8696f3e29": tflow},

View File

@ -27,6 +27,7 @@ export default class WebsocketBackend {
this.fetchData("settings") this.fetchData("settings")
this.fetchData("flows") this.fetchData("flows")
this.fetchData("events") this.fetchData("events")
this.fetchData("options")
this.store.dispatch(connectionActions.startFetching()) this.store.dispatch(connectionActions.startFetching())
} }

View File

@ -0,0 +1,119 @@
import PropTypes from 'prop-types'
PureBooleanOption.PropTypes = {
value: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
}
function PureBooleanOption({ value, onChange, name, help}) {
return (
<label>
{ name }
<input type="checkbox"
checked={value}
onChange={onChange}
title={help}
/>
</label>
)
}
PureStringOption.PropTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
function PureStringOption( { value, onChange, name, help }) {
let onKeyDown = (e) => {e.stopPropagation()}
return (
<label>
{ name }
<input type="text"
value={value}
onChange={onChange}
title={help}
onKeyDown={onKeyDown}
/>
</label>
)
}
PureNumberOption.PropTypes = {
value: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
}
function PureNumberOption( {value, onChange, name, help }) {
let onKeyDown = (e) => {e.stopPropagation()}
return (
<label>
{ name }
<input type="number"
value={value}
onChange={onChange}
title={help}
onKeyDown={onKeyDown}
/>
</label>
)
}
PureChoicesOption.PropTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
function PureChoicesOption( { value, onChange, name, help, choices }) {
return (
<label htmlFor="">
{ name }
<select name={name} onChange={onChange} title={help} selected={value}>
{ choices.map((choice, index) => (
<option key={index} value={choice}> {choice} </option>
))}
</select>
</label>
)
}
const OptionTypes = {
bool: PureBooleanOption,
str: PureStringOption,
int: PureNumberOption,
"optional str": PureStringOption,
"sequence of str": PureStringOption,
}
export default function OptionMaster({option, name, updateOptions, ...props}) {
let WrappedComponent = null
if (option.choices) {
WrappedComponent = PureChoicesOption
} else {
WrappedComponent = OptionTypes[option.type]
}
let onChange = (e) => {
switch (option.type) {
case 'bool' :
updateOptions({[name]: !option.value})
break
case 'int':
updateOptions({[name]: parseInt(e.target.value)})
break
default:
updateOptions({[name]: e.target.value})
}
}
return (
<div className="menu-entry">
<WrappedComponent
children={props.children}
value={option.value}
onChange={onChange}
name={name}
help={option.help}
choices={option.choices}
/>
</div>
)
}

View File

@ -1,16 +1,18 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import * as modalAction from '../../ducks/ui/modal' import * as modalAction from '../../ducks/ui/modal'
import { update as updateOptions } from '../../ducks/options'
import Option from './OptionMaster'
class PureOptionModal extends Component { class PureOptionModal extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
this.state = { title: 'Options', } this.state = { title: 'Options' }
} }
render() { render() {
const { hideModal } = this.props const { hideModal, options } = this.props
const { title } = this.state const { title } = this.state
return ( return (
<div> <div>
@ -26,7 +28,19 @@ class PureOptionModal extends Component {
</div> </div>
<div className="modal-body"> <div className="modal-body">
... {
Object.keys(options).sort()
.map((key, index) => {
let option = options[key];
return (
<Option
key={index}
name={key}
updateOptions={updateOptions}
option={option}
/>)
})
}
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
@ -39,7 +53,10 @@ class PureOptionModal extends Component {
export default connect( export default connect(
state => ({ state => ({
options: state.options
}), }),
{ hideModal: modalAction.hideModal } {
hideModal: modalAction.hideModal,
updateOptions: updateOptions,
}
)(PureOptionModal) )(PureOptionModal)

View File

@ -4,6 +4,7 @@ import flows from "./flows"
import settings from "./settings" import settings from "./settings"
import ui from "./ui/index" import ui from "./ui/index"
import connection from "./connection" import connection from "./connection"
import options from './options'
export default combineReducers({ export default combineReducers({
eventLog, eventLog,
@ -11,4 +12,5 @@ export default combineReducers({
settings, settings,
connection, connection,
ui, ui,
options,
}) })