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)
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: [ { 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 = []
for k in sorted(opts.keys()):
options_dict = {}
keys = keys if keys else opts.keys()
for k in sorted(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):

View File

@ -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",

View File

@ -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):

View File

@ -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
@ -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.

View File

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

View File

@ -1,3 +1,13 @@
.modal-visible {
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
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
className="modal-footer"

View File

@ -42,6 +42,36 @@ export function TStore(){
anticache: true,
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: {
selected: ["d91165be-ca1f-4612-88a9-c0f8696f3e29"],
byId: {"d91165be-ca1f-4612-88a9-c0f8696f3e29": tflow},

View File

@ -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())
}

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 { 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 {
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 (
<div>
@ -26,7 +28,19 @@ class PureOptionModal extends Component {
</div>
<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 className="modal-footer">
@ -39,7 +53,10 @@ class PureOptionModal extends Component {
export default connect(
state => ({
options: state.options
}),
{ hideModal: modalAction.hideModal }
{
hideModal: modalAction.hideModal,
updateOptions: updateOptions,
}
)(PureOptionModal)

View File

@ -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,
})