mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2025-01-30 14:58:38 +00:00
Merge pull request #2423 from MatthewShao/mitmweb-options
[web] [WIP] Mitmweb options editor content
This commit is contained in:
commit
062a58f848
@ -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):
|
||||||
|
@ -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",
|
||||||
|
@ -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):
|
||||||
|
@ -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.
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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},
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
119
web/src/js/components/Modal/OptionMaster.jsx
Normal file
119
web/src/js/components/Modal/OptionMaster.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user