From 663e6026fbbf0ce1c1e96d2dd5b46dcda9bb3e06 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Fri, 21 Jul 2017 22:58:20 +0800 Subject: [web] Add default value suggester in option editor. --- web/src/js/components/Modal/OptionModal.jsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index 5741ee8c..2a95f43b 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -2,6 +2,7 @@ import React, { Component } from "react" import { connect } from "react-redux" import * as modalAction from "../../ducks/ui/modal" import Option from "./Option" +import _ from "lodash" function PureOptionHelp({help}){ return
{help}
; @@ -18,6 +19,31 @@ const OptionError = connect((state, {name}) => ({ error: state.ui.optionsEditor[name] && state.ui.optionsEditor[name].error }))(PureOptionError); +function PureOptionDefault({value, defaultVal}){ + if( value === defaultVal ) { + return null + } else { + if (typeof(defaultVal) === 'boolean') { + defaultVal = defaultVal ? 'true' : 'false' + } else if (Array.isArray(defaultVal)){ + if (_.isEmpty(_.compact(value)) && // filter the empty string in array + _.isEmpty(defaultVal)){ + return null + } + defaultVal = '[ ]' + } else if (defaultVal === ''){ + defaultVal = '\"\"' + } else if (defaultVal === null){ + defaultVal = 'null' + } + return
Default: {defaultVal}
+ } +} +const OptionDefault = connect((state, {name}) => ({ + value: state.options[name].value, + defaultVal: state.options[name].default +}))(PureOptionDefault) + class PureOptionModal extends Component { constructor(props, context) { @@ -53,6 +79,7 @@ class PureOptionModal extends Component {
) -- cgit v1.2.3 From a0d14caa897c64c4e41605a1a514ebef5b1a9faf Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Fri, 21 Jul 2017 23:00:10 +0800 Subject: [web] Update tests. --- .../Modal/__snapshots__/ModalSpec.js.snap | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) 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 bfd855bd..0b904c07 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap @@ -136,6 +136,17 @@ exports[`Modal Component should render correctly 2`] = ` +
+ Default: + + + a + + + +
+
+ Default: + + + 0 + + + +
+
+ Default: + + + null + + + +
-- cgit v1.2.3 From 93cd1562def9e5c760b1b1f6a552440a83bae383 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Sat, 22 Jul 2017 21:16:16 +0800 Subject: [web] OptionModal component coverge ++. --- .../__tests__/components/Modal/OptionModalSpec.js | 54 +++++++++++++++++++ .../Modal/__snapshots__/OptionModalSpec.js.snap | 61 ++++++++++++++++++++++ web/src/js/components/Modal/OptionModal.jsx | 2 +- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 web/src/js/__tests__/components/Modal/OptionModalSpec.js create mode 100644 web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap diff --git a/web/src/js/__tests__/components/Modal/OptionModalSpec.js b/web/src/js/__tests__/components/Modal/OptionModalSpec.js new file mode 100644 index 00000000..dd4e70a2 --- /dev/null +++ b/web/src/js/__tests__/components/Modal/OptionModalSpec.js @@ -0,0 +1,54 @@ +import React from 'react' +import renderer from 'react-test-renderer' +import { PureOptionDefault } from '../../../components/Modal/OptionModal' + +describe('PureOptionDefault Component', () => { + + it('should return null when the value is default', () => { + let pureOptionDefault = renderer.create( + + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should handle boolean type', () => { + let pureOptionDefault = renderer.create( + + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should handle array', () => { + let a = [""], b = [], c = ['c'], + pureOptionDefault = renderer.create( + + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + + pureOptionDefault = renderer.create( + + ) + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should handle string', () => { + let pureOptionDefault = renderer.create( + + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should handle null value', () => { + let pureOptionDefault = renderer.create( + + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + +}) diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap new file mode 100644 index 00000000..68f1c9fc --- /dev/null +++ b/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PureOptionDefault Component should handle array 1`] = `null`; + +exports[`PureOptionDefault Component should handle array 2`] = ` +
+ Default: + + + [ ] + + + +
+`; + +exports[`PureOptionDefault Component should handle boolean type 1`] = ` +
+ Default: + + + false + + + +
+`; + +exports[`PureOptionDefault Component should handle null value 1`] = ` +
+ Default: + + + null + + + +
+`; + +exports[`PureOptionDefault Component should handle string 1`] = ` +
+ Default: + + + "" + + + +
+`; + +exports[`PureOptionDefault Component should return null when the value is default 1`] = `null`; diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index 2a95f43b..35ba3a2e 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -19,7 +19,7 @@ const OptionError = connect((state, {name}) => ({ error: state.ui.optionsEditor[name] && state.ui.optionsEditor[name].error }))(PureOptionError); -function PureOptionDefault({value, defaultVal}){ +export function PureOptionDefault({value, defaultVal}){ if( value === defaultVal ) { return null } else { -- cgit v1.2.3 From 6c4dbcc7a7774f7acbad0f1362a94525316d51ff Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Mon, 24 Jul 2017 20:30:14 +0800 Subject: [web] Update OptManager.save() to suit the need of mitmweb. --- mitmproxy/optmanager.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index e1d74b8e..83082153 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -512,13 +512,15 @@ def serialize(opts, text, defaults=False): return ruamel.yaml.round_trip_dump(data) -def save(opts, path, defaults=False): +def save(opts, path=None, defaults=False): """ - Save to path. If the destination file exists, modify it in-place. + If the path is given, save to path, otherwise return the serialized data. + + If the destination file exists, modify it in-place. Raises OptionsError if the existing data is corrupt. """ - if os.path.exists(path) and os.path.isfile(path): + if path and os.path.exists(path) and os.path.isfile(path): with open(path, "rt", encoding="utf8") as f: try: data = f.read() @@ -529,5 +531,9 @@ def save(opts, path, defaults=False): else: data = "" data = serialize(opts, data, defaults) - with open(path, "wt", encoding="utf8") as f: - f.write(data) + + if path: + with open(path, "wt", encoding="utf8") as f: + f.write(data) + else: + return data -- cgit v1.2.3 From 38d926d159221d7e4a4bd2c895a040f351f5264f Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Mon, 24 Jul 2017 20:32:46 +0800 Subject: [web] Add /options/dump API to backend. --- mitmproxy/tools/web/app.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index c6fb2ef6..36cb1ca1 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -451,6 +451,22 @@ class Options(RequestHandler): raise APIError(400, "{}".format(err)) +class DumpOptions(RequestHandler): + def get(self): + self.set_header("Content-Disposition", "attachment; filename=options") + self.set_header("Content-Type", "application/octet-stream") + + data = optmanager.save(self.master.options) + self.write(data) + + def post(self): + try: + data = optmanager.parse(self.filecontents) + self.master.options.update(**data) + except Exception as err: + raise APIError(400, "{}".format(err)) + + class Application(tornado.web.Application): def __init__(self, master, debug): self.master = master @@ -475,7 +491,8 @@ class Application(tornado.web.Application): FlowContentView), (r"/settings", Settings), (r"/clear", ClearAll), - (r"/options", Options) + (r"/options", Options), + (r"/options/dump", DumpOptions) ] settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), -- cgit v1.2.3 From 8c3e988a8c95bae7235d5de4ba8e85e77988c395 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Mon, 24 Jul 2017 20:34:15 +0800 Subject: [web] Add Download/Load button to OptionEditor. --- web/src/js/components/Modal/OptionModal.jsx | 17 ++++++++++++++++- web/src/js/ducks/options.js | 12 ++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index 35ba3a2e..6595717d 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -1,7 +1,9 @@ import React, { Component } from "react" import { connect } from "react-redux" import * as modalAction from "../../ducks/ui/modal" +import * as optionAction from "../../ducks/options" import Option from "./Option" +import FileChooser from '../../components/common/FileChooser' import _ from "lodash" function PureOptionHelp({help}){ @@ -52,7 +54,7 @@ class PureOptionModal extends Component { } render() { - const { hideModal, options } = this.props + const { hideModal, options, download, upload } = this.props const { title } = this.state return (
@@ -88,6 +90,17 @@ class PureOptionModal extends Component {
+ + + {upload(file); alert('Option configuration loaded!')}} + text="Load" + className="btn btn-primary" + />
) @@ -100,5 +113,7 @@ export default connect( }), { hideModal: modalAction.hideModal, + download: optionAction.download, + upload: optionAction.upload, } )(PureOptionModal) diff --git a/web/src/js/ducks/options.js b/web/src/js/ducks/options.js index 06144a3c..b22030a3 100644 --- a/web/src/js/ducks/options.js +++ b/web/src/js/ducks/options.js @@ -5,6 +5,7 @@ import _ from "lodash" export const RECEIVE = 'OPTIONS_RECEIVE' export const UPDATE = 'OPTIONS_UPDATE' export const REQUEST_UPDATE = 'REQUEST_UPDATE' +export const SAVE = 'OPTION_SAVE' const defaultState = {} @@ -44,3 +45,14 @@ export function update(option, value) { sendUpdate(option, value, dispatch); } } + +export function download() { + window.location = '/options/dump' + return { type: SAVE } +} + +export function upload(file) { + const body = new FormData() + body.append('file', file) + return dispatch => fetchApi('/options/dump', { method: 'POST', body }) +} -- cgit v1.2.3 From 5c45a90ce961d740e59644f4de0f8ffe97a5706c Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Mon, 24 Jul 2017 20:35:43 +0800 Subject: [web] Update tests. --- test/mitmproxy/test_optmanager.py | 4 +++ test/mitmproxy/tools/web/test_app.py | 13 +++++++++- .../Modal/__snapshots__/ModalSpec.js.snap | 29 +++++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index 7b4ffb8b..3b38162f 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -285,6 +285,10 @@ def test_saving(tmpdir): optmanager.load_paths(o, dst) assert o.three == "foo" + o2 = TD2() + o2.three = "foo" + assert(optmanager.save(o2, None, defaults=False)) == "three: foo\n" + with open(dst, 'a') as f: f.write("foobar: '123'") assert optmanager.load_paths(o, dst) == {"foobar": "123"} diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index bb439b34..bb0b1907 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -263,6 +263,18 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): assert self.put_json("/options", {"wtf": True}).code == 400 assert self.put_json("/options", {"anticache": "foo"}).code == 400 + def test_option_dump(self): + resp = self.fetch("/options/dump") + assert b"http2: false" in resp.body + + assert self.fetch("/options/dump", method="POST", body=b"http2: true").code == 200 + j = json(self.fetch("/options")) + assert j["http2"]["value"] + + resp = self.fetch("/options/dump", method="POST", body=b"wtf: true") + assert resp.code == 400 + assert resp.body == b"'Unknown options: wtf'" + def test_err(self): with mock.patch("mitmproxy.tools.web.app.IndexHandler.get") as f: f.side_effect = RuntimeError @@ -279,7 +291,6 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): 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 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 0b904c07..92dad72b 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap @@ -246,7 +246,34 @@ exports[`Modal Component should render correctly 2`] = `
+ > + + + + Load + + +
-- cgit v1.2.3 From 9d16cc7079984661c9268a36c4e45bdcb5c7ea19 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Wed, 26 Jul 2017 15:06:42 +0800 Subject: [web] Update POST /options/dump API in backend. --- mitmproxy/tools/web/app.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index 36cb1ca1..f9f44162 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -18,8 +18,10 @@ from mitmproxy import io from mitmproxy import log from mitmproxy import version from mitmproxy import optmanager +from mitmproxy import options import mitmproxy.tools.web.master # noqa +CONFIG_PATH = os.path.expanduser(os.path.join(options.CA_DIR, 'config.yaml')) def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: """ @@ -452,17 +454,9 @@ class Options(RequestHandler): class DumpOptions(RequestHandler): - def get(self): - self.set_header("Content-Disposition", "attachment; filename=options") - self.set_header("Content-Type", "application/octet-stream") - - data = optmanager.save(self.master.options) - self.write(data) - def post(self): try: - data = optmanager.parse(self.filecontents) - self.master.options.update(**data) + optmanager.save(self.master.options, CONFIG_PATH) except Exception as err: raise APIError(400, "{}".format(err)) -- cgit v1.2.3 From c7e41b32abba017210ef5c2dc3961554a9b6b9b3 Mon Sep 17 00:00:00 2001 From: Matthew Shao Date: Wed, 26 Jul 2017 15:09:11 +0800 Subject: [web] Update options auto dump for frontend. --- web/src/js/components/Modal/Option.jsx | 2 +- web/src/js/components/Modal/OptionModal.jsx | 18 +++--------------- web/src/js/ducks/options.js | 12 ++---------- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx index 58b863d1..38e2f239 100644 --- a/web/src/js/components/Modal/Option.jsx +++ b/web/src/js/components/Modal/Option.jsx @@ -74,7 +74,7 @@ export function ChoicesOption({ value, onChange, choices, ...props }) { return ( - - + /> diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap index 514e0eb5..257bddce 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap @@ -18,7 +18,7 @@ exports[`BooleanOption Component should render correctly 1`] = ` exports[`ChoiceOption Component should render correctly 1`] = `