diff options
author | Maximilian Hils <git@maximilianhils.com> | 2016-12-11 22:52:17 +0100 |
---|---|---|
committer | Maximilian Hils <git@maximilianhils.com> | 2016-12-12 00:08:29 +0100 |
commit | d854e08653ccee12119266e2cc3f5d6c279341e5 (patch) | |
tree | a58d465ea62fdb9665389b39c284d689f2b94e78 | |
parent | d1c7b203f08d4b1e1ee3c7a50762a4f08843feef (diff) | |
download | mitmproxy-d854e08653ccee12119266e2cc3f5d6c279341e5.tar.gz mitmproxy-d854e08653ccee12119266e2cc3f5d6c279341e5.tar.bz2 mitmproxy-d854e08653ccee12119266e2cc3f5d6c279341e5.zip |
[web] various fixes
-rw-r--r-- | mitmproxy/addons/view.py | 3 | ||||
-rw-r--r-- | mitmproxy/flow.py | 4 | ||||
-rw-r--r-- | mitmproxy/tools/web/app.py | 26 | ||||
-rw-r--r-- | mitmproxy/tools/web/master.py | 2 | ||||
-rw-r--r-- | mitmproxy/tools/web/static/app.css | 86 | ||||
-rw-r--r-- | mitmproxy/tools/web/static/app.js | 775 | ||||
-rw-r--r-- | test/mitmproxy/test_web_app.py | 19 | ||||
-rw-r--r-- | web/src/css/flowtable.less | 5 | ||||
-rw-r--r-- | web/src/css/header.less | 1 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ShowFullContentButton.jsx | 4 | ||||
-rw-r--r-- | web/src/js/components/FlowTable/FlowColumns.jsx | 12 | ||||
-rw-r--r-- | web/src/js/components/Header.jsx | 4 | ||||
-rw-r--r-- | web/src/js/components/Header/FileMenu.jsx | 8 | ||||
-rw-r--r-- | web/src/js/components/Header/FlowMenu.jsx | 27 | ||||
-rw-r--r-- | web/src/js/components/Header/MenuToggle.jsx | 2 | ||||
-rw-r--r-- | web/src/js/components/common/Button.jsx | 2 | ||||
-rw-r--r-- | web/src/js/ducks/flows.js | 44 | ||||
-rw-r--r-- | web/src/js/ducks/ui/header.js | 6 | ||||
-rw-r--r-- | web/src/js/ducks/ui/keyboard.js | 57 | ||||
-rw-r--r-- | web/src/js/ducks/utils/store.js | 1 |
20 files changed, 677 insertions, 411 deletions
diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index b8b6093f..be761adf 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -327,6 +327,9 @@ class View(collections.Sequence): def resume(self, f): self.update(f) + def kill(self, f): + self.update(f) + class Focus: """ diff --git a/mitmproxy/flow.py b/mitmproxy/flow.py index b9c4935c..605802c6 100644 --- a/mitmproxy/flow.py +++ b/mitmproxy/flow.py @@ -169,6 +169,8 @@ class Flow(stateobject.StateObject): self.reply.take() self.reply.kill(force=True) self.reply.commit() + self.live = False + master.addons("kill", self) def intercept(self, master): """ @@ -190,4 +192,4 @@ class Flow(stateobject.StateObject): self.intercepted = False self.reply.ack() self.reply.commit() - master.addons("intercept", self) + master.addons("resume", self) diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index ed95f536..adbbe160 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -31,7 +31,8 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: "intercepted": flow.intercepted, "client_conn": flow.client_conn.get_state(), "server_conn": flow.server_conn.get_state(), - "type": flow.type + "type": flow.type, + "modified": flow.modified(), } if flow.error: f["error"] = flow.error.get_state() @@ -222,17 +223,30 @@ class ClearAll(RequestHandler): self.master.events.clear() -class AcceptFlows(RequestHandler): +class ResumeFlows(RequestHandler): def post(self): for f in self.view: f.resume(self.master) -class AcceptFlow(RequestHandler): +class KillFlows(RequestHandler): + def post(self): + for f in self.view: + if f.killable: + f.kill(self.master) + + +class ResumeFlow(RequestHandler): def post(self, flow_id): self.flow.resume(self.master) +class KillFlow(RequestHandler): + def post(self, flow_id): + if self.flow.killable: + self.flow.kill(self.master) + + class FlowHandler(RequestHandler): def delete(self, flow_id): if self.flow.killable: @@ -410,9 +424,11 @@ class Application(tornado.web.Application): (r"/events", Events), (r"/flows", Flows), (r"/flows/dump", DumpFlows), - (r"/flows/accept", AcceptFlows), + (r"/flows/resume", ResumeFlows), + (r"/flows/kill", KillFlows), (r"/flows/(?P<flow_id>[0-9a-f\-]+)", FlowHandler), - (r"/flows/(?P<flow_id>[0-9a-f\-]+)/accept", AcceptFlow), + (r"/flows/(?P<flow_id>[0-9a-f\-]+)/resume", ResumeFlow), + (r"/flows/(?P<flow_id>[0-9a-f\-]+)/kill", KillFlow), (r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow), (r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow), (r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow), diff --git a/mitmproxy/tools/web/master.py b/mitmproxy/tools/web/master.py index 22cc156f..db4855ff 100644 --- a/mitmproxy/tools/web/master.py +++ b/mitmproxy/tools/web/master.py @@ -68,7 +68,7 @@ class WebMaster(master.Master): app.ClientConnection.broadcast( resource="flows", cmd="remove", - data=dict(id=flow.id) + data=flow.id ) def _sig_view_refresh(self, view): diff --git a/mitmproxy/tools/web/static/app.css b/mitmproxy/tools/web/static/app.css index 6799d23e..b95155a7 100644 --- a/mitmproxy/tools/web/static/app.css +++ b/mitmproxy/tools/web/static/app.css @@ -130,28 +130,98 @@ body, margin: 1px 0 0px; } header { - padding-top: 0.5em; + padding-top: 6px; background-color: white; } -header .menu { - padding: 10px; +header menu { + display: block; + margin: 0; + padding: 0; border-bottom: solid #a6a6a6 1px; + height: 85px; + overflow: visible; +} +.menu-group { + margin: 0 3px; + display: inline-block; + height: 85px; + vertical-align: top; +} +.menu-content { + height: 69px; + text-align: center; +} +.menu-content > .btn { + height: 69px; + text-align: center; + margin: 0 1px; + padding: 12px 5px; + border: none; + border-radius: 0; +} +.menu-content > .btn i { + font-size: 20px; + display: block; + margin: 0 auto 5px; +} +.menu-entry { + text-align: left; + height: 23px; + line-height: 1; + padding: 0.5rem 1rem; } -.menu-row { +.menu-entry label { + font-size: 1.2rem; + font-weight: normal; + margin: 0; +} +.menu-entry input[type=checkbox] { + margin: 0 2px; + vertical-align: middle; +} +.menu-legend { + height: 16px; + text-align: center; + font-size: 12px; + padding: 0 5px; +} +.menu-group + .menu-group:before { + margin-left: -3px; + content: " "; + border-left: solid 1px #e6e6e6; + margin-top: 10px; + height: 65px; + position: absolute; +} +.menu-main { margin-left: -2px; margin-right: -3px; + padding: 2px 5px; } .filter-input { position: relative; min-height: 1px; padding-left: 2.5px; padding-right: 2.5px; - margin-bottom: 5px; + padding: 2.5px; } @media (min-width: 768px) { .filter-input { float: left; - width: 25%; + width: 41.66666667%; + } +} +@media (max-width: 767px) { + .filter-input { + padding: 2px 2.5px; + } + .filter-input > .form-control, + .filter-input > .input-group-addon, + .filter-input > .input-group-btn > .btn { + height: 23.5px; + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; } } .filter-input .popover { @@ -258,6 +328,10 @@ header .menu { .flow-table .col-path .fa-pause { color: #ff8000; } +.flow-table .col-path .fa-exclamation, +.flow-table .col-path .fa-times { + color: darkred; +} .flow-table .col-method { width: 60px; } diff --git a/mitmproxy/tools/web/static/app.js b/mitmproxy/tools/web/static/app.js index 7e3a7ea8..7160def8 100644 --- a/mitmproxy/tools/web/static/app.js +++ b/mitmproxy/tools/web/static/app.js @@ -936,9 +936,13 @@ function ShowFullContentButton(_ref) { return !showFullContent && _react2.default.createElement( 'div', null, - _react2.default.createElement(_Button2.default, { className: 'view-all-content-btn btn-xs', onClick: function onClick() { - return setShowFullContent(); - }, text: 'Show full content' }), + _react2.default.createElement( + _Button2.default, + { className: 'view-all-content-btn btn-xs', onClick: function onClick() { + return setShowFullContent(); + } }, + 'Show full content' + ), _react2.default.createElement( 'span', { className: 'pull-right' }, @@ -993,7 +997,7 @@ function UploadContentButton(_ref) { className: 'btn btn-default btn-xs' }); } -},{"../common/FileChooser":42,"react":"react"}],13:[function(require,module,exports){ +},{"../common/FileChooser":43,"react":"react"}],13:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -1082,7 +1086,7 @@ exports.default = (0, _reactRedux.connect)(function (state) { setContentView: _flow.setContentView })(ViewSelector); -},{"../../ducks/ui/flow":52,"../common/Dropdown":41,"./ContentViews":8,"react":"react","react-redux":"react-redux"}],14:[function(require,module,exports){ +},{"../../ducks/ui/flow":52,"../common/Dropdown":42,"./ContentViews":8,"react":"react","react-redux":"react-redux"}],14:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -1209,7 +1213,7 @@ exports.default = (0, _reactRedux.connect)(function (state) { toggleFilter: _eventLog.toggleFilter })(EventLog); -},{"../ducks/eventLog":48,"./EventLog/EventList":15,"./common/ToggleButton":44,"react":"react","react-redux":"react-redux"}],15:[function(require,module,exports){ +},{"../ducks/eventLog":48,"./EventLog/EventList":15,"./common/ToggleButton":45,"react":"react","react-redux":"react-redux"}],15:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -1630,11 +1634,21 @@ IconColumn.getIcon = function (flow) { function PathColumn(_ref3) { var flow = _ref3.flow; + + var err = void 0; + if (flow.error) { + if (flow.error.msg === "Connection killed") { + err = _react2.default.createElement('i', { className: 'fa fa-fw fa-times pull-right' }); + } else { + err = _react2.default.createElement('i', { className: 'fa fa-fw fa-exclamation pull-right' }); + } + } return _react2.default.createElement( 'td', { className: 'col-path' }, flow.request.is_replay && _react2.default.createElement('i', { className: 'fa fa-fw fa-repeat pull-right' }), flow.intercepted && _react2.default.createElement('i', { className: 'fa fa-fw fa-pause pull-right' }), + err, _utils.RequestUtils.pretty_url(flow.request) ); } @@ -1695,7 +1709,7 @@ function TimeColumn(_ref7) { return _react2.default.createElement( 'td', { className: 'col-time' }, - flow.response ? (0, _utils2.formatTimeDelta)(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)) : '...' + flow.response ? (0, _utils2.formatTimeDelta)(1000 * (flow.response.timestamp_end - flow.server_conn.timestamp_start)) : '...' ); } @@ -2901,13 +2915,13 @@ function ToggleEdit(_ref) { { className: 'edit-flow-container' }, isEdit ? _react2.default.createElement( 'a', - { className: 'edit-flow', onClick: function onClick() { + { className: 'edit-flow', title: 'Finish Edit', onClick: function onClick() { return stopEdit(flow, modifiedFlow); } }, _react2.default.createElement('i', { className: 'fa fa-check' }) ) : _react2.default.createElement( 'a', - { className: 'edit-flow', onClick: function onClick() { + { className: 'edit-flow', title: 'Edit Flow', onClick: function onClick() { return startEdit(flow); } }, _react2.default.createElement('i', { className: 'fa fa-pencil' }) @@ -3077,10 +3091,6 @@ var _MainMenu = require('./Header/MainMenu'); var _MainMenu2 = _interopRequireDefault(_MainMenu); -var _ViewMenu = require('./Header/ViewMenu'); - -var _ViewMenu2 = _interopRequireDefault(_ViewMenu); - var _OptionMenu = require('./Header/OptionMenu'); var _OptionMenu2 = _interopRequireDefault(_OptionMenu); @@ -3133,9 +3143,11 @@ var Header = function (_Component) { var entries = [].concat(_toConsumableArray(Header.entries)); if (selectedFlowId) entries.push(_FlowMenu2.default); + // Make sure to have a fallback in case FlowMenu is selected but we don't have any flows + // (e.g. because they are all deleted or not yet received) var Active = _.find(entries, function (e) { return e.title == activeMenu; - }); + }) || _MainMenu2.default; return _react2.default.createElement( 'header', @@ -3158,8 +3170,8 @@ var Header = function (_Component) { }) ), _react2.default.createElement( - 'div', - { className: 'menu' }, + 'menu', + null, _react2.default.createElement(Active, null) ) ); @@ -3169,7 +3181,7 @@ var Header = function (_Component) { return Header; }(_react.Component); -Header.entries = [_MainMenu2.default, _ViewMenu2.default, _OptionMenu2.default]; +Header.entries = [_MainMenu2.default, _OptionMenu2.default]; exports.default = (0, _reactRedux.connect)(function (state) { return { selectedFlowId: state.flows.selected[0], @@ -3179,7 +3191,7 @@ exports.default = (0, _reactRedux.connect)(function (state) { setActiveMenu: _header.setActiveMenu })(Header); -},{"../ducks/ui/header":53,"./Header/FileMenu":28,"./Header/FlowMenu":31,"./Header/MainMenu":32,"./Header/OptionMenu":33,"./Header/ViewMenu":34,"classnames":"classnames","react":"react","react-redux":"react-redux"}],28:[function(require,module,exports){ +},{"../ducks/ui/header":53,"./Header/FileMenu":28,"./Header/FlowMenu":31,"./Header/MainMenu":32,"./Header/OptionMenu":34,"classnames":"classnames","react":"react","react-redux":"react-redux"}],28:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -3233,11 +3245,11 @@ function FileMenu(_ref) { return FileMenu.onNewClick(e, clearFlows); } }, _react2.default.createElement('i', { className: 'fa fa-fw fa-file' }), - 'New' + ' New' ), _react2.default.createElement(_FileChooser2.default, { icon: 'fa-folder-open', - text: 'Open...', + text: ' Open...', onOpenFile: function onOpenFile(file) { return loadFlows(file); } @@ -3248,14 +3260,14 @@ function FileMenu(_ref) { e.preventDefault();saveFlows(); } }, _react2.default.createElement('i', { className: 'fa fa-fw fa-floppy-o' }), - 'Save...' + ' Save...' ), _react2.default.createElement(_Dropdown.Divider, null), _react2.default.createElement( 'a', { href: 'http://mitm.it/', target: '_blank' }, _react2.default.createElement('i', { className: 'fa fa-fw fa-external-link' }), - 'Install Certificates...' + ' Install Certificates...' ) ); } @@ -3266,7 +3278,7 @@ exports.default = (0, _reactRedux.connect)(null, { saveFlows: flowsActions.download })(FileMenu); -},{"../../ducks/flows":49,"../common/Dropdown":41,"../common/FileChooser":42,"react":"react","react-redux":"react-redux"}],29:[function(require,module,exports){ +},{"../../ducks/flows":49,"../common/Dropdown":42,"../common/FileChooser":43,"react":"react","react-redux":"react-redux"}],29:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -3361,7 +3373,7 @@ var FilterDocs = function (_Component) { { href: 'http://docs.mitmproxy.org/en/stable/features/filters.html', target: '_blank' }, _react2.default.createElement('i', { className: 'fa fa-external-link' }), - '  mitmproxy docs' + ' mitmproxy docs' ) ) ) @@ -3573,25 +3585,25 @@ var FilterInput = function (_Component) { exports.default = FilterInput; },{"../../filt/filt":57,"../../utils.js":60,"./FilterDocs":29,"classnames":"classnames","react":"react","react-dom":"react-dom"}],31:[function(require,module,exports){ -'use strict'; +"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var _react = require('react'); +var _react = require("react"); var _react2 = _interopRequireDefault(_react); -var _reactRedux = require('react-redux'); +var _reactRedux = require("react-redux"); -var _Button = require('../common/Button'); +var _Button = require("../common/Button"); var _Button2 = _interopRequireDefault(_Button); -var _utils = require('../../flow/utils.js'); +var _utils = require("../../flow/utils.js"); -var _flows = require('../../ducks/flows'); +var _flows = require("../../ducks/flows"); var flowsActions = _interopRequireWildcard(_flows); @@ -3602,8 +3614,9 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de FlowMenu.title = 'Flow'; FlowMenu.propTypes = { - flow: _react.PropTypes.object.isRequired, - acceptFlow: _react.PropTypes.func.isRequired, + flow: _react.PropTypes.object, + resumeFlow: _react.PropTypes.func.isRequired, + killFlow: _react.PropTypes.func.isRequired, replayFlow: _react.PropTypes.func.isRequired, duplicateFlow: _react.PropTypes.func.isRequired, removeFlow: _react.PropTypes.func.isRequired, @@ -3612,38 +3625,112 @@ FlowMenu.propTypes = { function FlowMenu(_ref) { var flow = _ref.flow; - var acceptFlow = _ref.acceptFlow; + var resumeFlow = _ref.resumeFlow; + var killFlow = _ref.killFlow; var replayFlow = _ref.replayFlow; var duplicateFlow = _ref.duplicateFlow; var removeFlow = _ref.removeFlow; var revertFlow = _ref.revertFlow; + if (!flow) return _react2.default.createElement("div", null); return _react2.default.createElement( - 'div', + "div", null, _react2.default.createElement( - 'div', - { className: 'menu-row' }, - _react2.default.createElement(_Button2.default, { disabled: !flow || !flow.intercepted, title: '[a]ccept intercepted flow', text: 'Accept', icon: 'fa-play', onClick: function onClick() { - return acceptFlow(flow); - } }), - _react2.default.createElement(_Button2.default, { title: '[r]eplay flow', text: 'Replay', icon: 'fa-repeat', onClick: function onClick() { - return replayFlow(flow); - } }), - _react2.default.createElement(_Button2.default, { title: '[D]uplicate flow', text: 'Duplicate', icon: 'fa-copy', onClick: function onClick() { - return duplicateFlow(flow); - } }), - _react2.default.createElement(_Button2.default, { title: '[d]elete flow', text: 'Delete', icon: 'fa-trash', onClick: function onClick() { - return removeFlow(flow); - } }), - _react2.default.createElement(_Button2.default, { disabled: !flow || !flow.modified, title: 'revert changes to flow [V]', text: 'Revert', icon: 'fa-history', onClick: function onClick() { - return revertFlow(flow); - } }), - _react2.default.createElement(_Button2.default, { title: 'download', text: 'Download', icon: 'fa-download', onClick: function onClick() { - return window.location = _utils.MessageUtils.getContentURL(flow, flow.response); - } }) + "div", + { className: "menu-group" }, + _react2.default.createElement( + "div", + { className: "menu-content" }, + _react2.default.createElement( + _Button2.default, + { title: "[r]eplay flow", icon: "fa-repeat text-primary", + onClick: function onClick() { + return replayFlow(flow); + } }, + "Replay" + ), + _react2.default.createElement( + _Button2.default, + { title: "[D]uplicate flow", icon: "fa-copy text-info", + onClick: function onClick() { + return duplicateFlow(flow); + } }, + "Duplicate" + ), + _react2.default.createElement( + _Button2.default, + { disabled: !flow || !flow.modified, title: "revert changes to flow [V]", + icon: "fa-history text-warning", onClick: function onClick() { + return revertFlow(flow); + } }, + "Revert" + ), + _react2.default.createElement( + _Button2.default, + { title: "[d]elete flow", icon: "fa-trash text-danger", + onClick: function onClick() { + return removeFlow(flow); + } }, + "Delete" + ) + ), + _react2.default.createElement( + "div", + { className: "menu-legend" }, + "Flow Modification" + ) ), - _react2.default.createElement('div', { className: 'clearfix' }) + _react2.default.createElement( + "div", + { className: "menu-group" }, + _react2.default.createElement( + "div", + { className: "menu-content" }, + _react2.default.createElement( + _Button2.default, + { title: "download", icon: "fa-download", + onClick: function onClick() { + return window.location = _utils.MessageUtils.getContentURL(flow, flow.response); + } }, + "Download" + ) + ), + _react2.default.createElement( + "div", + { className: "menu-legend" }, + "Export" + ) + ), + _react2.default.createElement( + "div", + { className: "menu-group" }, + _react2.default.createElement( + "div", + { className: "menu-content" }, + _react2.default.createElement( + _Button2.default, + { disabled: !flow || !flow.intercepted, title: "[a]ccept intercepted flow", + icon: "fa-play text-success", onClick: function onClick() { + return resumeFlow(flow); + } }, + "Resume" + ), + _react2.default.createElement( + _Button2.default, + { disabled: !flow || !flow.intercepted, title: "kill intercepted flow [x]", + icon: "fa-times text-danger", onClick: function onClick() { + return killFlow(flow); + } }, + "Abort" + ) + ), + _react2.default.createElement( + "div", + { className: "menu-legend" }, + "Interception" + ) + ) ); } @@ -3652,7 +3739,8 @@ exports.default = (0, _reactRedux.connect)(function (state) { flow: state.flows.byId[state.flows.selected[0]] }; }, { - acceptFlow: flowsActions.accept, + resumeFlow: flowsActions.resume, + killFlow: flowsActions.kill, replayFlow: flowsActions.replay, duplicateFlow: flowsActions.duplicate, removeFlow: flowsActions.remove, @@ -3660,26 +3748,26 @@ exports.default = (0, _reactRedux.connect)(function (state) { })(FlowMenu); },{"../../ducks/flows":49,"../../flow/utils.js":58,"../common/Button":40,"react":"react","react-redux":"react-redux"}],32:[function(require,module,exports){ -'use strict'; +"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = MainMenu; -var _react = require('react'); +var _react = require("react"); var _react2 = _interopRequireDefault(_react); -var _reactRedux = require('react-redux'); +var _reactRedux = require("react-redux"); -var _FilterInput = require('./FilterInput'); +var _FilterInput = require("./FilterInput"); var _FilterInput2 = _interopRequireDefault(_FilterInput); -var _settings = require('../../ducks/settings'); +var _settings = require("../../ducks/settings"); -var _flows = require('../../ducks/flows'); +var _flows = require("../../ducks/flows"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } @@ -3687,16 +3775,11 @@ MainMenu.title = "Start"; function MainMenu() { return _react2.default.createElement( - 'div', - null, - _react2.default.createElement( - 'div', - { className: 'menu-row' }, - _react2.default.createElement(FlowFilterInput, null), - _react2.default.createElement(HighlightInput, null), - _react2.default.createElement(InterceptInput, null) - ), - _react2.default.createElement('div', { className: 'clearfix' }) + "div", + { className: "menu-main" }, + _react2.default.createElement(FlowFilterInput, null), + _react2.default.createElement(HighlightInput, null), + _react2.default.createElement(InterceptInput, null) ); } @@ -3730,178 +3813,208 @@ var HighlightInput = (0, _reactRedux.connect)(function (state) { }, { onChange: _flows.setHighlight })(_FilterInput2.default); },{"../../ducks/flows":49,"../../ducks/settings":51,"./FilterInput":30,"react":"react","react-redux":"react-redux"}],33:[function(require,module,exports){ -'use strict'; +"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.MenuToggle = MenuToggle; +exports.SettingsToggle = SettingsToggle; +exports.EventlogToggle = EventlogToggle; -var _react = require('react'); - -var _react2 = _interopRequireDefault(_react); - -var _reactRedux = require('react-redux'); +var _react = require("react"); -var _ToggleButton = require('../common/ToggleButton'); +var _reactRedux = require("react-redux"); -var _ToggleButton2 = _interopRequireDefault(_ToggleButton); +var _settings = require("../../ducks/settings"); -var _ToggleInputButton = require('../common/ToggleInputButton'); +var _eventLog = require("../../ducks/eventLog"); -var _ToggleInputButton2 = _interopRequireDefault(_ToggleInputButton); +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -var _settings = require('../../ducks/settings'); +MenuToggle.propTypes = { + value: _react.PropTypes.bool.isRequired, + onChange: _react.PropTypes.func.isRequired, + children: _react.PropTypes.node.isRequired +}; -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } +function MenuToggle(_ref) { + var value = _ref.value; + var onChange = _ref.onChange; + var children = _ref.children; -OptionMenu.title = 'Options'; + return React.createElement( + "div", + { className: "menu-entry" }, + React.createElement( + "label", + null, + React.createElement("input", { type: "checkbox", + checked: value, + onChange: onChange }), + children + ) + ); +} -OptionMenu.propTypes = { - settings: _react.PropTypes.object.isRequired, - updateSettings: _react.PropTypes.func.isRequired +SettingsToggle.propTypes = { + setting: _react.PropTypes.string.isRequired, + children: _react.PropTypes.node.isRequired }; -function OptionMenu(_ref) { - var settings = _ref.settings; - var updateSettings = _ref.updateSettings; +function SettingsToggle(_ref2) { + var setting = _ref2.setting; + var children = _ref2.children; + var settings = _ref2.settings; + var updateSettings = _ref2.updateSettings; - return _react2.default.createElement( - 'div', - null, - _react2.default.createElement( - 'div', - { className: 'menu-row' }, - _react2.default.createElement(_ToggleButton2.default, { text: 'showhost', - checked: settings.showhost, - onToggle: function onToggle() { - return updateSettings({ showhost: !settings.showhost }); - } - }), - _react2.default.createElement(_ToggleButton2.default, { text: 'no_upstream_cert', - checked: settings.no_upstream_cert, - onToggle: function onToggle() { - return updateSettings({ no_upstream_cert: !settings.no_upstream_cert }); - } - }), - _react2.default.createElement(_ToggleButton2.default, { text: 'rawtcp', - checked: settings.rawtcp, - onToggle: function onToggle() { - return updateSettings({ rawtcp: !settings.rawtcp }); - } - }), - _react2.default.createElement(_ToggleButton2.default, { text: 'http2', - checked: settings.http2, - onToggle: function onToggle() { - return updateSettings({ http2: !settings.http2 }); - } - }), - _react2.default.createElement(_ToggleButton2.default, { text: 'websocket', - checked: settings.websocket, - onToggle: function onToggle() { - return updateSettings({ websocket: !settings.websocket }); - } - }), - _react2.default.createElement(_ToggleButton2.default, { text: 'anticache', - checked: settings.anticache, - onToggle: function onToggle() { - return updateSettings({ anticache: !settings.anticache }); - } - }), - _react2.default.createElement(_ToggleButton2.default, { text: 'anticomp', - checked: settings.anticomp, - onToggle: function onToggle() { - return updateSettings({ anticomp: !settings.anticomp }); - } - }), - _react2.default.createElement(_ToggleInputButton2.default, { name: 'stickyauth', placeholder: 'Sticky auth filter', - checked: !!settings.stickyauth, - txt: settings.stickyauth, - onToggleChanged: function onToggleChanged(txt) { - return updateSettings({ stickyauth: !settings.stickyauth ? txt : null }); - } - }), - _react2.default.createElement(_ToggleInputButton2.default, { name: 'stickycookie', placeholder: 'Sticky cookie filter', - checked: !!settings.stickycookie, - txt: settings.stickycookie, - onToggleChanged: function onToggleChanged(txt) { - return updateSettings({ stickycookie: !settings.stickycookie ? txt : null }); - } - }), - _react2.default.createElement(_ToggleInputButton2.default, { name: 'stream_large_bodies', placeholder: 'stream...', - checked: !!settings.stream_large_bodies, - txt: settings.stream_large_bodies, - inputType: 'number', - onToggleChanged: function onToggleChanged(txt) { - return updateSettings({ stream_large_bodies: !settings.stream_large_bodies ? txt : null }); - } - }) - ), - _react2.default.createElement('div', { className: 'clearfix' }) + return React.createElement( + MenuToggle, + { + value: settings[setting] || false // we don't have settings initially, so just pass false. + , onChange: function onChange() { + return updateSettings(_defineProperty({}, setting, !settings[setting])); + } + }, + children ); } - -exports.default = (0, _reactRedux.connect)(function (state) { +exports.SettingsToggle = SettingsToggle = (0, _reactRedux.connect)(function (state) { return { settings: state.settings }; }, { updateSettings: _settings.update -})(OptionMenu); +})(SettingsToggle); -},{"../../ducks/settings":51,"../common/ToggleButton":44,"../common/ToggleInputButton":45,"react":"react","react-redux":"react-redux"}],34:[function(require,module,exports){ -'use strict'; +function EventlogToggle(_ref3) { + var toggleVisibility = _ref3.toggleVisibility; + var eventLogVisible = _ref3.eventLogVisible; + + return React.createElement( + MenuToggle, + { + value: eventLogVisible, + onChange: toggleVisibility + }, + "Display Event Log" + ); +} +exports.EventlogToggle = EventlogToggle = (0, _reactRedux.connect)(function (state) { + return { + eventLogVisible: state.eventLog.visible + }; +}, { + toggleVisibility: _eventLog.toggleVisibility +})(EventlogToggle); + +},{"../../ducks/eventLog":48,"../../ducks/settings":51,"react":"react","react-redux":"react-redux"}],34:[function(require,module,exports){ +"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = OptionMenu; -var _react = require('react'); +var _react = require("react"); var _react2 = _interopRequireDefault(_react); -var _reactRedux = require('react-redux'); +var _reactRedux = require("react-redux"); -var _ToggleButton = require('../common/ToggleButton'); +var _MenuToggle = require("./MenuToggle"); -var _ToggleButton2 = _interopRequireDefault(_ToggleButton); +var _DocsLink = require("../common/DocsLink"); -var _eventLog = require('../../ducks/eventLog'); +var _DocsLink2 = _interopRequireDefault(_DocsLink); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -ViewMenu.title = 'View'; -ViewMenu.route = 'flows'; - -ViewMenu.propTypes = { - eventLogVisible: _react.PropTypes.bool.isRequired, - toggleEventLog: _react.PropTypes.func.isRequired -}; - -function ViewMenu(_ref) { - var eventLogVisible = _ref.eventLogVisible; - var toggleEventLog = _ref.toggleEventLog; +OptionMenu.title = 'Options'; +function OptionMenu() { return _react2.default.createElement( - 'div', + "div", null, _react2.default.createElement( - 'div', - { className: 'menu-row' }, - _react2.default.createElement(_ToggleButton2.default, { text: 'Show Event Log', checked: eventLogVisible, onToggle: toggleEventLog }) + "div", + { className: "menu-group" }, + _react2.default.createElement( + "div", + { className: "menu-content" }, + _react2.default.createElement( + _MenuToggle.SettingsToggle, + { setting: "http2" }, + "HTTP/2.0" + ), + _react2.default.createElement( + _MenuToggle.SettingsToggle, + { setting: "websocket" }, + "WebSockets" + ), + _react2.default.createElement( + _MenuToggle.SettingsToggle, + { setting: "rawtcp" }, + "Raw TCP" + ) + ), + _react2.default.createElement( + "div", + { className: "menu-legend" }, + "Protocol Support" + ) ), - _react2.default.createElement('div', { className: 'clearfix' }) + _react2.default.createElement( + "div", + { className: "menu-group" }, + _react2.default.createElement( + "div", + { className: "menu-content" }, + _react2.default.createElement( + _MenuToggle.SettingsToggle, + { setting: "anticache" }, + "Disable Caching ", + _react2.default.createElement(_DocsLink2.default, { resource: "features/anticache.html" }) + ), + _react2.default.createElement( + _MenuToggle.SettingsToggle, + { setting: "anticomp" }, + "Disable Compression ", + _react2.default.createElement("i", { className: "fa fa-question-circle", + title: "Do not forward Accept-Encoding headers to the server to force an uncompressed response." }) + ) + ), + _react2.default.createElement( + "div", + { className: "menu-legend" }, + "HTTP Options" + ) + ), + _react2.default.createElement( + "div", + { className: "menu-group" }, + _react2.default.createElement( + "div", + { className: "menu-content" }, + _react2.default.createElement( + _MenuToggle.SettingsToggle, + { setting: "showhost" }, + "Use Host Header ", + _react2.default.createElement("i", { className: "fa fa-question-circle", + title: "Use the Host header to construct URLs for display." }) + ), + _react2.default.createElement(_MenuToggle.EventlogToggle, null) + ), + _react2.default.createElement( + "div", + { className: "menu-legend" }, + "View Options" + ) + ) ); } -exports.default = (0, _reactRedux.connect)(function (state) { - return { - eventLogVisible: state.eventLog.visible - }; -}, { - toggleEventLog: _eventLog.toggleVisibility -})(ViewMenu); - -},{"../../ducks/eventLog":48,"../common/ToggleButton":44,"react":"react","react-redux":"react-redux"}],35:[function(require,module,exports){ +},{"../common/DocsLink":41,"./MenuToggle":33,"react":"react","react-redux":"react-redux"}],35:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -4004,7 +4117,7 @@ exports.default = (0, _reactRedux.connect)(function (state) { updateFlow: flowsActions.update })(MainView); -},{"../ducks/flows":49,"./FlowTable":16,"./FlowView":20,"./common/Splitter":43,"react":"react","react-redux":"react-redux"}],36:[function(require,module,exports){ +},{"../ducks/flows":49,"./FlowTable":16,"./FlowView":20,"./common/Splitter":44,"react":"react","react-redux":"react-redux"}],36:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -4506,18 +4619,18 @@ ValueEditor.defaultProps = { exports.default = ValueEditor; },{"../../utils":60,"classnames":"classnames","lodash":"lodash","react":"react"}],40:[function(require,module,exports){ -'use strict'; +"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = Button; -var _react = require('react'); +var _react = require("react"); var _react2 = _interopRequireDefault(_react); -var _classnames = require('classnames'); +var _classnames = require("classnames"); var _classnames2 = _interopRequireDefault(_classnames); @@ -4525,28 +4638,57 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de Button.propTypes = { onClick: _react.PropTypes.func.isRequired, - text: _react.PropTypes.string, - icon: _react.PropTypes.string + children: _react.PropTypes.node.isRequired, + icon: _react.PropTypes.string, + title: _react.PropTypes.string }; function Button(_ref) { var onClick = _ref.onClick; - var text = _ref.text; + var children = _ref.children; var icon = _ref.icon; var disabled = _ref.disabled; var className = _ref.className; + var title = _ref.title; return _react2.default.createElement( - 'div', + "div", { className: (0, _classnames2.default)(className, 'btn btn-default'), - onClick: onClick, - disabled: disabled }, - icon && _react2.default.createElement('i', { className: "fa fa-fw " + icon }), - text && text + onClick: !disabled && onClick, + disabled: disabled, + title: title }, + icon && _react2.default.createElement("i", { className: "fa fa-fw " + icon }), + children ); } },{"classnames":"classnames","react":"react"}],41:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = DocsLink; + +var _react = require("react"); + +DocsLink.propTypes = { + resource: _react.PropTypes.string.isRequired +}; + +function DocsLink(_ref) { + var children = _ref.children; + var resource = _ref.resource; + + var url = "http://docs.mitmproxy.org/en/stable/" + resource; + return React.createElement( + "a", + { target: "_blank", href: url }, + children || React.createElement("i", { className: "fa fa-question-circle" }) + ); +} + +},{"react":"react"}],42:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -4655,7 +4797,7 @@ Dropdown.defaultProps = { }; exports.default = Dropdown; -},{"classnames":"classnames","react":"react"}],42:[function(require,module,exports){ +},{"classnames":"classnames","react":"react"}],43:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -4707,7 +4849,7 @@ function FileChooser(_ref) { ); } -},{"react":"react"}],43:[function(require,module,exports){ +},{"react":"react"}],44:[function(require,module,exports){ 'use strict'; Object.defineProperty(exports, "__esModule", { @@ -4852,7 +4994,7 @@ var Splitter = function (_Component) { Splitter.defaultProps = { axis: 'x' }; exports.default = Splitter; -},{"classnames":"classnames","react":"react","react-dom":"react-dom"}],44:[function(require,module,exports){ +},{"classnames":"classnames","react":"react","react-dom":"react-dom"}],45:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4886,113 +5028,7 @@ function ToggleButton(_ref) { ); } -},{"react":"react"}],45:[function(require,module,exports){ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _react = require('react'); - -var _react2 = _interopRequireDefault(_react); - -var _classnames = require('classnames'); - -var _classnames2 = _interopRequireDefault(_classnames); - -var _utils = require('../../utils'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var ToggleInputButton = function (_Component) { - _inherits(ToggleInputButton, _Component); - - function ToggleInputButton(props) { - _classCallCheck(this, ToggleInputButton); - - var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(ToggleInputButton).call(this, props)); - - _this.state = { txt: props.txt || '' }; - return _this; - } - - _createClass(ToggleInputButton, [{ - key: 'onKeyDown', - value: function onKeyDown(e) { - e.stopPropagation(); - if (e.keyCode === _utils.Key.ENTER) { - this.props.onToggleChanged(this.state.txt); - } - } - }, { - key: 'render', - value: function render() { - var _this2 = this; - - var _props = this.props; - var checked = _props.checked; - var onToggleChanged = _props.onToggleChanged; - var name = _props.name; - var inputType = _props.inputType; - var placeholder = _props.placeholder; - - return _react2.default.createElement( - 'div', - { className: 'input-group toggle-input-btn' }, - _react2.default.createElement( - 'span', - { className: 'input-group-btn', - onClick: function onClick() { - return onToggleChanged(_this2.state.txt); - } }, - _react2.default.createElement( - 'div', - { className: (0, _classnames2.default)('btn', checked ? 'btn-primary' : 'btn-default') }, - _react2.default.createElement('span', { className: (0, _classnames2.default)('fa', checked ? 'fa-check-square-o' : 'fa-square-o') }), - ' ', - name - ) - ), - _react2.default.createElement('input', { - className: 'form-control', - placeholder: placeholder, - disabled: checked, - value: this.state.txt, - type: inputType || 'text', - onChange: function onChange(e) { - return _this2.setState({ txt: e.target.value }); - }, - onKeyDown: function onKeyDown(e) { - return _this2.onKeyDown(e); - } - }) - ); - } - }]); - - return ToggleInputButton; -}(_react.Component); - -ToggleInputButton.propTypes = { - name: _react.PropTypes.string.isRequired, - txt: _react.PropTypes.string, - onToggleChanged: _react.PropTypes.func.isRequired, - checked: _react.PropTypes.bool.isRequired, - placeholder: _react.PropTypes.string.isRequired, - inputType: _react.PropTypes.string -}; -exports.default = ToggleInputButton; - -},{"../../utils":60,"classnames":"classnames","react":"react"}],46:[function(require,module,exports){ +},{"react":"react"}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5254,8 +5290,10 @@ exports.setFilter = setFilter; exports.setHighlight = setHighlight; exports.setSort = setSort; exports.selectRelative = selectRelative; -exports.accept = accept; -exports.acceptAll = acceptAll; +exports.resume = resume; +exports.resumeAll = resumeAll; +exports.kill = kill; +exports.killAll = killAll; exports.remove = remove; exports.duplicate = duplicate; exports.replay = replay; @@ -5313,13 +5351,43 @@ function reduce() { // FIXME: Update state.selected on REMOVE: // The selected flow may have been removed, we need to select the next one in the view. var storeAction = storeActions[action.cmd](action.data, makeFilter(state.filter), makeSort(state.sort)); - return _extends({}, state, (0, storeActions.default)(state, storeAction)); + + var selected = state.selected; + if (action.type === REMOVE && state.selected.includes(action.data)) { + if (state.selected.length > 1) { + selected = selected.filter(function (x) { + return x !== action.data; + }); + } else { + selected = []; + if (action.data in state.viewIndex && state.view.length > 1) { + var currentIndex = state.viewIndex[action.data], + nextSelection = void 0; + if (currentIndex === state.view.length - 1) { + // last row + nextSelection = state.view[currentIndex - 1]; + } else { + nextSelection = state.view[currentIndex + 1]; + } + selected.push(nextSelection.id); + } + } + } + + return _extends({}, state, { + selected: selected + }, (0, storeActions.default)(state, storeAction)); case SET_FILTER: return _extends({}, state, { filter: action.filter }, (0, storeActions.default)(state, storeActions.setFilter(makeFilter(action.filter), makeSort(state.sort)))); + case SET_HIGHLIGHT: + return _extends({}, state, { + highlight: action.highlight + }); + case SET_SORT: return _extends({}, state, { sort: action.sort @@ -5424,15 +5492,27 @@ function selectRelative(shift) { }; } -function accept(flow) { +function resume(flow) { + return function (dispatch) { + return (0, _utils.fetchApi)("/flows/" + flow.id + "/resume", { method: 'POST' }); + }; +} + +function resumeAll() { + return function (dispatch) { + return (0, _utils.fetchApi)('/flows/resume', { method: 'POST' }); + }; +} + +function kill(flow) { return function (dispatch) { - return (0, _utils.fetchApi)("/flows/" + flow.id + "/accept", { method: 'POST' }); + return (0, _utils.fetchApi)("/flows/" + flow.id + "/kill", { method: 'POST' }); }; } -function acceptAll() { +function killAll() { return function (dispatch) { - return (0, _utils.fetchApi)('/flows/accept', { method: 'POST' }); + return (0, _utils.fetchApi)('/flows/kill', { method: 'POST' }); }; } @@ -5794,7 +5874,7 @@ function reducer() { case flowsActions.SELECT: // First Select - if (action.flowIds.length && !state.isFlowSelected) { + if (action.flowIds.length > 0 && !state.isFlowSelected) { return _extends({}, state, { activeMenu: 'Flow', isFlowSelected: true @@ -5802,7 +5882,7 @@ function reducer() { } // Deselect - if (!action.flowIds.length && state.isFlowSelected) { + if (action.flowIds.length === 0 && state.isFlowSelected) { var activeMenu = state.activeMenu; if (activeMenu == 'Flow') { activeMenu = 'Start'; @@ -5848,18 +5928,18 @@ exports.default = (0, _redux.combineReducers)({ }); },{"./flow":52,"./header":53,"redux":"redux"}],55:[function(require,module,exports){ -'use strict'; +"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.onKeyDown = onKeyDown; -var _utils = require('../../utils'); +var _utils = require("../../utils"); -var _flow = require('./flow'); +var _flow = require("./flow"); -var _flows = require('../flows'); +var _flows = require("../flows"); var flowsActions = _interopRequireWildcard(_flows); @@ -5934,12 +6014,6 @@ function onKeyDown(e) { break; } - case _utils.Key.C: - if (shiftKey) { - dispatch(flowsActions.clear()); - } - break; - case _utils.Key.D: { if (!flow) { @@ -5956,9 +6030,9 @@ function onKeyDown(e) { case _utils.Key.A: { if (shiftKey) { - dispatch(flowsActions.acceptAll()); + dispatch(flowsActions.resumeAll()); } else if (flow && flow.intercepted) { - dispatch(flowsActions.accept(flow)); + dispatch(flowsActions.resume(flow)); } break; } @@ -5979,6 +6053,24 @@ function onKeyDown(e) { break; } + case _utils.Key.X: + { + if (shiftKey) { + dispatch(flowsActions.killAll()); + } else if (flow && flow.intercepted) { + dispatch(flowsActions.kill(flow)); + } + break; + } + + case _utils.Key.Z: + { + if (!shiftKey) { + dispatch(flowsActions.clear()); + } + break; + } + default: return; } @@ -6109,6 +6201,7 @@ function reduce() { if (!(action.id in byId)) { break; } + byId = _extends({}, byId); delete byId[action.id]; var _removeData2 = removeData(list, listIndex, action.id); diff --git a/test/mitmproxy/test_web_app.py b/test/mitmproxy/test_web_app.py index 2cab5bf4..1fb5ccf0 100644 --- a/test/mitmproxy/test_web_app.py +++ b/test/mitmproxy/test_web_app.py @@ -80,17 +80,30 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): self.view.add(f) self.events.data = events - def test_accept(self): + def test_resume(self): for f in self.view: f.reply.handle() f.intercept(self.master) assert self.fetch( - "/flows/42/accept", method="POST").code == 200 + "/flows/42/resume", method="POST").code == 200 assert sum(f.intercepted for f in self.view) == 1 - assert self.fetch("/flows/accept", method="POST").code == 200 + assert self.fetch("/flows/resume", method="POST").code == 200 assert all(not f.intercepted for f in self.view) + def test_kill(self): + for f in self.view: + f.backup() + f.reply.handle() + f.intercept(self.master) + + assert self.fetch("/flows/42/kill", method="POST").code == 200 + assert sum(f.killable for f in self.view) == 1 + assert self.fetch("/flows/kill", method="POST").code == 200 + assert all(not f.killable for f in self.view) + for f in self.view: + f.revert() + def test_flow_delete(self): f = self.view.get_by_id("42") assert f diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less index 1b560eba..e8d3d5af 100644 --- a/web/src/css/flowtable.less +++ b/web/src/css/flowtable.less @@ -109,6 +109,9 @@ .fa-pause { color: @interceptorange; } + .fa-exclamation, .fa-times { + color: darkred; + } } .col-method { width: 60px; @@ -125,4 +128,4 @@ td.col-time, td.col-size { text-align: right; } -}
\ No newline at end of file +} diff --git a/web/src/css/header.less b/web/src/css/header.less index a026d8aa..042d6811 100644 --- a/web/src/css/header.less +++ b/web/src/css/header.less @@ -47,6 +47,7 @@ header { .menu-entry { + text-align: left; height: (@menu-height - @menu-legend-height)/3; line-height: 1; padding: 0.5rem 1rem; diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx index cfd96dd8..fd68991e 100644 --- a/web/src/js/components/ContentView/ShowFullContentButton.jsx +++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx @@ -16,7 +16,9 @@ function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLi return ( !showFullContent && <div> - <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()} text="Show full content"/> + <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()}> + Show full content + </Button> <span className="pull-right"> {visibleLines}/{contentLines} are visible </span> </div> ) diff --git a/web/src/js/components/FlowTable/FlowColumns.jsx b/web/src/js/components/FlowTable/FlowColumns.jsx index 0ff80453..02a4fba1 100644 --- a/web/src/js/components/FlowTable/FlowColumns.jsx +++ b/web/src/js/components/FlowTable/FlowColumns.jsx @@ -54,6 +54,15 @@ IconColumn.getIcon = flow => { } export function PathColumn({ flow }) { + + let err; + if(flow.error){ + if (flow.error.msg === "Connection killed"){ + err = <i className="fa fa-fw fa-times pull-right"></i> + } else { + err = <i className="fa fa-fw fa-exclamation pull-right"></i> + } + } return ( <td className="col-path"> {flow.request.is_replay && ( @@ -62,6 +71,7 @@ export function PathColumn({ flow }) { {flow.intercepted && ( <i className="fa fa-fw fa-pause pull-right"></i> )} + {err} {RequestUtils.pretty_url(flow.request)} </td> ) @@ -109,7 +119,7 @@ export function TimeColumn({ flow }) { return ( <td className="col-time"> {flow.response ? ( - formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start)) + formatTimeDelta(1000 * (flow.response.timestamp_end - flow.server_conn.timestamp_start)) ) : ( '...' )} diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx index 1500db1b..c15c951f 100644 --- a/web/src/js/components/Header.jsx +++ b/web/src/js/components/Header.jsx @@ -22,7 +22,9 @@ class Header extends Component { if(selectedFlowId) entries.push(FlowMenu) - const Active = _.find(entries, (e) => e.title == activeMenu) + // Make sure to have a fallback in case FlowMenu is selected but we don't have any flows + // (e.g. because they are all deleted or not yet received) + const Active = _.find(entries, (e) => e.title == activeMenu) || MainMenu return ( <header> diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx index 53c63ea1..ec32c857 100644 --- a/web/src/js/components/Header/FileMenu.jsx +++ b/web/src/js/components/Header/FileMenu.jsx @@ -21,23 +21,23 @@ function FileMenu ({clearFlows, loadFlows, saveFlows}) { <Dropdown className="pull-left" btnClass="special" text="mitmproxy"> <a href="#" onClick={e => FileMenu.onNewClick(e, clearFlows)}> <i className="fa fa-fw fa-file"></i> - New + New </a> <FileChooser icon="fa-folder-open" - text="Open..." + text=" Open..." onOpenFile={file => loadFlows(file)} /> <a href="#" onClick={e =>{ e.preventDefault(); saveFlows();}}> <i className="fa fa-fw fa-floppy-o"></i> - Save... + Save... </a> <Divider/> <a href="http://mitm.it/" target="_blank"> <i className="fa fa-fw fa-external-link"></i> - Install Certificates... + Install Certificates... </a> </Dropdown> ) diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx index 420cb054..a404fdb7 100644 --- a/web/src/js/components/Header/FlowMenu.jsx +++ b/web/src/js/components/Header/FlowMenu.jsx @@ -8,21 +8,23 @@ FlowMenu.title = 'Flow' FlowMenu.propTypes = { flow: PropTypes.object, - acceptFlow: PropTypes.func.isRequired, + resumeFlow: PropTypes.func.isRequired, + killFlow: PropTypes.func.isRequired, replayFlow: PropTypes.func.isRequired, duplicateFlow: PropTypes.func.isRequired, removeFlow: PropTypes.func.isRequired, revertFlow: PropTypes.func.isRequired } -function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) { +function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) { if (!flow) return <div/> return ( <div> <div className="menu-group"> <div className="menu-content"> - <Button title="[r]eplay flow" icon="fa-repeat text-primary" onClick={() => replayFlow(flow)}> + <Button title="[r]eplay flow" icon="fa-repeat text-primary" + onClick={() => replayFlow(flow)}> Replay </Button> <Button title="[D]uplicate flow" icon="fa-copy text-info" @@ -33,7 +35,8 @@ function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, rev icon="fa-history text-warning" onClick={() => revertFlow(flow)}> Revert </Button> - <Button title="[d]elete flow" icon="fa-trash text-danger" onClick={() => removeFlow(flow)}> + <Button title="[d]elete flow" icon="fa-trash text-danger" + onClick={() => removeFlow(flow)}> Delete </Button> </div> @@ -51,17 +54,18 @@ function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, rev <div className="menu-group"> <div className="menu-content"> <Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow" - icon="fa-play text-success" onClick={() => acceptFlow(flow)} - > - Resume - </Button> - + icon="fa-play text-success" onClick={() => resumeFlow(flow)}> + Resume + </Button> + <Button disabled={!flow || !flow.intercepted} title="kill intercepted flow [x]" + icon="fa-times text-danger" onClick={() => killFlow(flow)}> + Abort + </Button> </div> <div className="menu-legend">Interception</div> </div> - </div> ) } @@ -71,7 +75,8 @@ export default connect( flow: state.flows.byId[state.flows.selected[0]], }), { - acceptFlow: flowsActions.accept, + resumeFlow: flowsActions.resume, + killFlow: flowsActions.kill, replayFlow: flowsActions.replay, duplicateFlow: flowsActions.duplicate, removeFlow: flowsActions.remove, diff --git a/web/src/js/components/Header/MenuToggle.jsx b/web/src/js/components/Header/MenuToggle.jsx index 8977f3b9..91f093c6 100644 --- a/web/src/js/components/Header/MenuToggle.jsx +++ b/web/src/js/components/Header/MenuToggle.jsx @@ -14,7 +14,7 @@ export function MenuToggle({ value, onChange, children }) { <div className="menu-entry"> <label> <input type="checkbox" - value={value} + checked={value} onChange={onChange}/> {children} </label> diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx index 69471f25..f05a68d0 100644 --- a/web/src/js/components/common/Button.jsx +++ b/web/src/js/components/common/Button.jsx @@ -11,7 +11,7 @@ Button.propTypes = { export default function Button({ onClick, children, icon, disabled, className, title }) { return ( <div className={classnames(className, 'btn btn-default')} - onClick={onClick} + onClick={!disabled && onClick} disabled={disabled} title={title}> {icon && (<i className={"fa fa-fw " + icon}/> )} diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js index d3717533..92408891 100644 --- a/web/src/js/ducks/flows.js +++ b/web/src/js/ducks/flows.js @@ -36,8 +36,29 @@ export default function reduce(state = defaultState, action) { makeFilter(state.filter), makeSort(state.sort) ) + + let selected = state.selected + if(action.type === REMOVE && state.selected.includes(action.data)) { + if(state.selected.length > 1){ + selected = selected.filter(x => x !== action.data) + } else { + selected = [] + if (action.data in state.viewIndex && state.view.length > 1) { + let currentIndex = state.viewIndex[action.data], + nextSelection + if(currentIndex === state.view.length -1){ // last row + nextSelection = state.view[currentIndex - 1] + } else { + nextSelection = state.view[currentIndex + 1] + } + selected.push(nextSelection.id) + } + } + } + return { ...state, + selected, ...reduceStore(state, storeAction) } @@ -48,6 +69,12 @@ export default function reduce(state = defaultState, action) { ...reduceStore(state, storeActions.setFilter(makeFilter(action.filter), makeSort(state.sort))) } + case SET_HIGHLIGHT: + return { + ...state, + highlight: action.highlight + } + case SET_SORT: return { ...state, @@ -144,14 +171,23 @@ export function selectRelative(shift) { } -export function accept(flow) { - return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' }) +export function resume(flow) { + return dispatch => fetchApi(`/flows/${flow.id}/resume`, { method: 'POST' }) +} + +export function resumeAll() { + return dispatch => fetchApi('/flows/resume', { method: 'POST' }) +} + +export function kill(flow) { + return dispatch => fetchApi(`/flows/${flow.id}/kill`, { method: 'POST' }) } -export function acceptAll() { - return dispatch => fetchApi('/flows/accept', { method: 'POST' }) +export function killAll() { + return dispatch => fetchApi('/flows/kill', { method: 'POST' }) } + export function remove(flow) { return dispatch => fetchApi(`/flows/${flow.id}`, { method: 'DELETE' }) } diff --git a/web/src/js/ducks/ui/header.js b/web/src/js/ducks/ui/header.js index 25dfe602..6581149e 100644 --- a/web/src/js/ducks/ui/header.js +++ b/web/src/js/ducks/ui/header.js @@ -1,4 +1,4 @@ -import * as flowsActions from '../flows' +import * as flowsActions from "../flows" export const SET_ACTIVE_MENU = 'UI_SET_ACTIVE_MENU' @@ -19,7 +19,7 @@ export default function reducer(state = defaultState, action) { case flowsActions.SELECT: // First Select - if (action.flowIds.length && !state.isFlowSelected) { + if (action.flowIds.length > 0 && !state.isFlowSelected) { return { ...state, activeMenu: 'Flow', @@ -28,7 +28,7 @@ export default function reducer(state = defaultState, action) { } // Deselect - if (!action.flowIds.length && state.isFlowSelected) { + if (action.flowIds.length === 0 && state.isFlowSelected) { let activeMenu = state.activeMenu if (activeMenu == 'Flow') { activeMenu = 'Start' diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js index 7418eca9..30fd76e1 100644 --- a/web/src/js/ducks/ui/keyboard.js +++ b/web/src/js/ducks/ui/keyboard.js @@ -1,6 +1,6 @@ -import { Key } from '../../utils' -import { selectTab } from './flow' -import * as flowsActions from '../flows' +import { Key } from "../../utils" +import { selectTab } from "./flow" +import * as flowsActions from "../flows" export function onKeyDown(e) { @@ -9,7 +9,7 @@ export function onKeyDown(e) { return () => { } } - var key = e.keyCode + var key = e.keyCode var shiftKey = e.shiftKey e.preventDefault() return (dispatch, getState) => { @@ -48,9 +48,8 @@ export function onKeyDown(e) { dispatch(flowsActions.select(null)) break - case Key.LEFT: - { - if(!flow) break + case Key.LEFT: { + if (!flow) break let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']), currentTab = getState().ui.flow.tab, nextTab = tabs[(tabs.indexOf(currentTab) - 1 + tabs.length) % tabs.length] @@ -59,9 +58,8 @@ export function onKeyDown(e) { } case Key.TAB: - case Key.RIGHT: - { - if(!flow) break + case Key.RIGHT: { + if (!flow) break let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']), currentTab = getState().ui.flow.tab, nextTab = tabs[(tabs.indexOf(currentTab) + 1) % tabs.length] @@ -69,14 +67,7 @@ export function onKeyDown(e) { break } - case Key.C: - if (shiftKey) { - dispatch(flowsActions.clear()) - } - break - - case Key.D: - { + case Key.D: { if (!flow) { return } @@ -88,32 +79,46 @@ export function onKeyDown(e) { break } - case Key.A: - { + case Key.A: { if (shiftKey) { - dispatch(flowsActions.acceptAll()) + dispatch(flowsActions.resumeAll()) } else if (flow && flow.intercepted) { - dispatch(flowsActions.accept(flow)) + dispatch(flowsActions.resume(flow)) } break } - case Key.R: - { + case Key.R: { if (!shiftKey && flow) { dispatch(flowsActions.replay(flow)) } break } - case Key.V: - { + case Key.V: { if (!shiftKey && flow && flow.modified) { dispatch(flowsActions.revert(flow)) } break } + case Key.X: { + if (shiftKey) { + dispatch(flowsActions.killAll()) + } else if (flow && flow.intercepted) { + dispatch(flowsActions.kill(flow)) + } + break + } + + case Key.Z: { + if (!shiftKey) { + dispatch(flowsActions.clear()) + } + break + } + + default: return } diff --git a/web/src/js/ducks/utils/store.js b/web/src/js/ducks/utils/store.js index 9ea4f02e..ac272650 100644 --- a/web/src/js/ducks/utils/store.js +++ b/web/src/js/ducks/utils/store.js @@ -85,6 +85,7 @@ export default function reduce(state = defaultState, action) { if (!(action.id in byId)) { break } + byId = {...byId} delete byId[action.id]; ({data: list, dataIndex: listIndex} = removeData(list, listIndex, action.id)) |