Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[web] [WIP] Mitmweb options editor content #2423

Merged
merged 8 commits into from Jul 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 7 additions & 7 deletions mitmproxy/optmanager.py
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good change. 👍



def parse(text):
Expand Down
10 changes: 10 additions & 0 deletions mitmproxy/tools/web/master.py
Expand Up @@ -5,6 +5,7 @@
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
Expand All @@ -29,6 +30,7 @@ def __init__(self, options, server, with_termlog=True):
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(
Expand Down Expand Up @@ -86,6 +88,14 @@ def _sig_events_refresh(self, event_store):
)

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",
Expand Down
1 change: 1 addition & 0 deletions test/mitmproxy/test_optmanager.py
Expand Up @@ -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):
Expand Down
28 changes: 24 additions & 4 deletions test/mitmproxy/tools/web/test_app.py
Expand Up @@ -255,8 +255,8 @@ def test_settings_update(self):

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
Expand All @@ -275,12 +275,32 @@ def test_websocket(self):
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.
Expand Down
1 change: 1 addition & 0 deletions web/src/css/app.less
Expand Up @@ -19,3 +19,4 @@ html {
@import (less) "footer.less";
@import (less) "codemirror.less";
@import (less) "contentview.less";
@import (less) "modal.less";
10 changes: 10 additions & 0 deletions 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;
}
Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions web/src/js/__tests__/ducks/tutils.js
Expand Up @@ -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},
Expand Down
1 change: 1 addition & 0 deletions web/src/js/backends/websocket.js
Expand Up @@ -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())
}

Expand Down
119 changes: 119 additions & 0 deletions web/src/js/components/Modal/OptionMaster.jsx
@@ -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>
)
}