diff options
author | Maximilian Hils <git@maximilianhils.com> | 2014-12-23 20:33:42 +0100 |
---|---|---|
committer | Maximilian Hils <git@maximilianhils.com> | 2014-12-23 20:33:42 +0100 |
commit | 2861d99de4d329bcba0a3c2193523398a22673c0 (patch) | |
tree | 0aa1a2aae666c2909285a6fd5049f2ef7d6d8ebc | |
parent | 459772a8ef79ac0adeaeba56577972e86074265e (diff) | |
download | mitmproxy-2861d99de4d329bcba0a3c2193523398a22673c0.tar.gz mitmproxy-2861d99de4d329bcba0a3c2193523398a22673c0.tar.bz2 mitmproxy-2861d99de4d329bcba0a3c2193523398a22673c0.zip |
web: intercept feature
-rw-r--r-- | libmproxy/console/__init__.py | 4 | ||||
-rw-r--r-- | libmproxy/console/common.py | 6 | ||||
-rw-r--r-- | libmproxy/console/flowlist.py | 2 | ||||
-rw-r--r-- | libmproxy/console/flowview.py | 6 | ||||
-rw-r--r-- | libmproxy/flow.py | 16 | ||||
-rw-r--r-- | libmproxy/protocol/http.py | 29 | ||||
-rw-r--r-- | libmproxy/protocol/primitives.py | 32 | ||||
-rw-r--r-- | libmproxy/web/__init__.py | 20 | ||||
-rw-r--r-- | libmproxy/web/app.py | 55 | ||||
-rw-r--r-- | libmproxy/web/static/css/app.css | 11 | ||||
-rw-r--r-- | libmproxy/web/static/js/app.js | 133 | ||||
-rw-r--r-- | test/test_flow.py | 12 | ||||
-rw-r--r-- | web/gulpfile.js | 1 | ||||
-rw-r--r-- | web/src/css/flowtable.less | 11 | ||||
-rw-r--r-- | web/src/js/actions.js | 6 | ||||
-rw-r--r-- | web/src/js/components/flowtable.jsx.js | 9 | ||||
-rw-r--r-- | web/src/js/components/footer.jsx.js | 3 | ||||
-rw-r--r-- | web/src/js/components/header.jsx.js | 91 | ||||
-rw-r--r-- | web/src/js/components/mainview.jsx.js | 11 | ||||
-rw-r--r-- | web/src/js/components/utils.jsx.js | 2 | ||||
-rw-r--r-- | web/src/js/store/settingstore.js | 0 | ||||
-rw-r--r-- | web/src/js/utils.js | 8 |
22 files changed, 303 insertions, 165 deletions
diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py index 38a16751..7d25d428 100644 --- a/libmproxy/console/__init__.py +++ b/libmproxy/console/__init__.py @@ -767,7 +767,7 @@ class ConsoleMaster(flow.FlowMaster): self.prompt_done() def accept_all(self): - self.state.accept_all() + self.state.accept_all(self) def set_limit(self, txt): v = self.state.set_limit(txt) @@ -1040,7 +1040,7 @@ class ConsoleMaster(flow.FlowMaster): def process_flow(self, f): if self.state.intercept and f.match(self.state.intercept) and not f.request.is_replay: - f.intercept() + f.intercept(self) else: f.reply() self.sync_list_view() diff --git a/libmproxy/console/common.py b/libmproxy/console/common.py index 9d42d4fc..3e6e5ccc 100644 --- a/libmproxy/console/common.py +++ b/libmproxy/console/common.py @@ -108,7 +108,7 @@ def raw_format_flow(f, focus, extended, padding): preamble = sum(i[1] for i in req) + len(req) -1 - if f["intercepting"] and not f["acked"]: + if f["intercepted"] and not f["acked"]: uc = "intercept" elif f["resp_code"] or f["err_msg"]: uc = "text" @@ -138,7 +138,7 @@ def raw_format_flow(f, focus, extended, padding): if f["resp_is_replay"]: resp.append(fcol(SYMBOL_REPLAY, "replay")) resp.append(fcol(f["resp_code"], ccol)) - if f["intercepting"] and f["resp_code"] and not f["acked"]: + if f["intercepted"] and f["resp_code"] and not f["acked"]: rc = "intercept" else: rc = "text" @@ -171,7 +171,7 @@ flowcache = FlowCache() def format_flow(f, focus, extended=False, hostheader=False, padding=2): d = dict( - intercepting = f.intercepting, + intercepted = f.intercepted, acked = f.reply.acked, req_timestamp = f.request.timestamp_start, diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index 3eb4eb1a..be25be83 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -140,7 +140,7 @@ class ConnectionItem(common.WWrap): def keypress(self, (maxcol,), key): key = common.shortcuts(key) if key == "a": - self.flow.accept_intercept() + self.flow.accept_intercept(self.master) self.master.sync_list_view() elif key == "d": self.flow.kill(self.master) diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py index 1ec57a4e..24804c58 100644 --- a/libmproxy/console/flowview.py +++ b/libmproxy/console/flowview.py @@ -230,7 +230,7 @@ class FlowView(common.WWrap): def wrap_body(self, active, body): parts = [] - if self.flow.intercepting and not self.flow.reply.acked and not self.flow.response: + if self.flow.intercepted and not self.flow.reply.acked and not self.flow.response: qt = "Request intercepted" else: qt = "Request" @@ -239,7 +239,7 @@ class FlowView(common.WWrap): else: parts.append(self._tab(qt, "heading_inactive")) - if self.flow.intercepting and not self.flow.reply.acked and self.flow.response: + if self.flow.intercepted and not self.flow.reply.acked and self.flow.response: st = "Response intercepted" else: st = "Response" @@ -677,7 +677,7 @@ class FlowView(common.WWrap): # Why doesn't this just work?? self.w.keypress(size, key) elif key == "a": - self.flow.accept_intercept() + self.flow.accept_intercept(self.master) self.master.view_flow(self.flow) elif key == "A": self.master.accept_all() diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 26699cc7..34c7a753 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -494,9 +494,9 @@ class FlowStore(FlowList): return c # TODO: Should accept_all operate on views or on all flows? - def accept_all(self): + def accept_all(self, master): for f in self._list: - f.accept_intercept() + f.accept_intercept(master) def kill_all(self, master): for f in self._list: @@ -574,8 +574,8 @@ class State(object): def clear(self): self.flows._clear() - def accept_all(self): - self.flows.accept_all() + def accept_all(self, master): + self.flows.accept_all(master) def revert(self, f): f.revert() @@ -811,7 +811,7 @@ class FlowMaster(controller.Master): """ if f.live: return "Can't replay request which is still live..." - if f.intercepting: + if f.intercepted: return "Can't replay while intercepting..." if f.request.content == http.CONTENT_MISSING: return "Can't replay request with missing content..." @@ -902,6 +902,12 @@ class FlowMaster(controller.Master): self.stream.add(f) return f + def handle_intercept(self, f): + self.state.update_flow(f) + + def handle_accept_intercept(self, f): + self.state.update_flow(f) + def shutdown(self): self.unload_scripts() controller.Master.shutdown(self) diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index d3945579..c6e67498 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -882,7 +882,7 @@ class HTTPFlow(Flow): The following additional attributes are exposed: - intercepting: Is this flow currently being intercepted? + intercepted: Is this flow currently being intercepted? live: Does this flow have a live client connection? """ @@ -893,9 +893,6 @@ class HTTPFlow(Flow): self.response = None """@type: HTTPResponse""" - # FIXME: Should that rather be an attribute of Flow? - self.intercepting = False - _stateobject_attributes = Flow._stateobject_attributes.copy() _stateobject_attributes.update( request=HTTPRequest, @@ -942,30 +939,6 @@ class HTTPFlow(Flow): return f(self) return True - def kill(self, master): - """ - Kill this request. - """ - self.error = Error("Connection killed") - self.intercepting = False - self.reply(KILL) - self.reply = controller.DummyReply() - master.handle_error(self) - - def intercept(self): - """ - Intercept this Flow. Processing will stop until accept_intercept is - called. - """ - self.intercepting = True - - def accept_intercept(self): - """ - Continue with the flow - called after an intercept(). - """ - self.intercepting = False - self.reply() - def replace(self, pattern, repl, *args, **kwargs): """ Replaces a regular expression pattern with repl in both request and diff --git a/libmproxy/protocol/primitives.py b/libmproxy/protocol/primitives.py index 34526d01..49c71c9f 100644 --- a/libmproxy/protocol/primitives.py +++ b/libmproxy/protocol/primitives.py @@ -71,14 +71,18 @@ class Flow(stateobject.StateObject): self.error = None """@type: Error""" + self.intercepted = False + """@type: bool""" self._backup = None + self.reply = None _stateobject_attributes = dict( id=str, error=Error, client_conn=ClientConnection, server_conn=ServerConnection, - type=str + type=str, + intercepted=bool ) def get_state(self, short=False): @@ -124,6 +128,32 @@ class Flow(stateobject.StateObject): self.load_state(self._backup) self._backup = None + def kill(self, master): + """ + Kill this request. + """ + self.error = Error("Connection killed") + self.intercepted = False + self.reply(KILL) + master.handle_error(self) + + def intercept(self, master): + """ + Intercept this Flow. Processing will stop until accept_intercept is + called. + """ + self.intercepted = True + master.handle_intercept(self) + + def accept_intercept(self, master): + """ + Continue with the flow - called after an intercept(). + """ + self.intercepted = False + self.reply() + master.handle_accept_intercept(self) + + class ProtocolHandler(object): """ diff --git a/libmproxy/web/__init__.py b/libmproxy/web/__init__.py index 4c36c009..ec3576db 100644 --- a/libmproxy/web/__init__.py +++ b/libmproxy/web/__init__.py @@ -123,7 +123,7 @@ class WebMaster(flow.FlowMaster): def __init__(self, server, options): self.options = options super(WebMaster, self).__init__(server, WebState()) - self.app = app.Application(self.state, self.options.wdebug) + self.app = app.Application(self, self.options.wdebug) def tick(self): flow.FlowMaster.tick(self, self.masterq, timeout=0) @@ -144,17 +144,23 @@ class WebMaster(flow.FlowMaster): except (Stop, KeyboardInterrupt): self.shutdown() + def _process_flow(self, f): + if self.state.intercept and self.state.intercept(f) and not f.request.is_replay: + f.intercept(self) + else: + f.reply() + def handle_request(self, f): super(WebMaster, self).handle_request(f) - if f: - f.reply() - return f + self._process_flow(f) def handle_response(self, f): super(WebMaster, self).handle_response(f) - if f: - f.reply() - return f + self._process_flow(f) + + def handle_error(self, f): + super(WebMaster, self).handle_error(f) + self._process_flow(f) def add_event(self, e, level="info"): super(WebMaster, self).add_event(e, level) diff --git a/libmproxy/web/app.py b/libmproxy/web/app.py index 37da4a42..7f1964ae 100644 --- a/libmproxy/web/app.py +++ b/libmproxy/web/app.py @@ -32,12 +32,30 @@ class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): logging.error("Error sending message", exc_info=True) +class ClientConnection(WebSocketEventBroadcaster): + connections = set() + + class Flows(tornado.web.RequestHandler): def get(self): self.write(dict( data=[f.get_state(short=True) for f in self.application.state.flows] )) + +class AcceptFlows(tornado.web.RequestHandler): + def post(self): + self.application.state.flows.accept_all(self.application.master) + + +class AcceptFlow(tornado.web.RequestHandler): + def post(self, flow_id): + flow_id = str(flow_id) + for flow in self.application.state.flows: + if flow.id == flow_id: + flow.accept_intercept(self.application.master) + break + class Events(tornado.web.RequestHandler): def get(self): self.write(dict( @@ -49,28 +67,53 @@ class Settings(tornado.web.RequestHandler): def get(self): self.write(dict( data=dict( - showEventLog=True + showEventLog=True, + intercept=self.application.state.intercept_txt ) )) + def put(self, *update, **kwargs): + update = {} + for k, v in self.request.arguments.iteritems(): + if len(v) != 1: + print "Warning: Unknown length for setting {}: {}".format(k, v) + continue + + if k == "_xsrf": + continue + elif k == "intercept": + self.application.state.set_intercept(v[0]) + update[k] = v[0] + else: + print "Warning: Unknown setting {}: {}".format(k, v) + + ClientConnection.broadcast( + type="settings", + cmd="update", + data=update + ) + class Clear(tornado.web.RequestHandler): def post(self): self.application.state.clear() -class ClientConnection(WebSocketEventBroadcaster): - connections = set() +class Application(tornado.web.Application): + @property + def state(self): + return self.master.state -class Application(tornado.web.Application): - def __init__(self, state, debug): - self.state = state + def __init__(self, master, debug): + self.master = master handlers = [ (r"/", IndexHandler), (r"/updates", ClientConnection), (r"/events", Events), (r"/flows", Flows), + (r"/flows/accept", AcceptFlows), + (r"/flows/([0-9a-f\-]+)/accept", AcceptFlow), (r"/settings", Settings), (r"/clear", Clear), ] diff --git a/libmproxy/web/static/css/app.css b/libmproxy/web/static/css/app.css index 5af87b67..4faf739b 100644 --- a/libmproxy/web/static/css/app.css +++ b/libmproxy/web/static/css/app.css @@ -174,7 +174,16 @@ header .menu { white-space: nowrap; text-overflow: ellipsis; } -.flow-table tr .col-tls { +.flow-table tr.intercepted:not(.has-response) .col-path, +.flow-table tr.intercepted:not(.has-response) .col-method { + color: #ff8000; +} +.flow-table tr.intercepted.has-response .col-status, +.flow-table tr.intercepted.has-response .col-size, +.flow-table tr.intercepted.has-response .col-time { + color: #ff8000; +} +.flow-table .col-tls { width: 10px; } .flow-table .col-tls-https { diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 4347ada5..440eec5d 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -90,11 +90,11 @@ var Key = { TAB: 9, SPACE: 32, BACKSPACE: 8, - J: 74, - K: 75, - H: 72, - L: 76 }; +// Add A-Z +for(var i=65; i <= 90; i++){ + Key[String.fromCharCode(i)] = i; +} var formatSize = function (bytes) { @@ -233,7 +233,11 @@ var ConnectionActions = { var SettingsActions = { update: function (settings) { - //TODO: Update server. + jQuery.ajax({ + type: "PUT", + url: "/settings", + data: settings + }); //Facebook Flux: We do an optimistic update on the client already. AppDispatcher.dispatchViewAction({ @@ -2355,7 +2359,6 @@ _.extend(StoreView.prototype, EventEmitter.prototype, { } } }); - function Connection(url) { if (url[0] === "/") { @@ -2495,7 +2498,7 @@ var xsrf = $.param({_xsrf: getCookie("_xsrf")}); //Tornado XSRF Protection. $.ajaxPrefilter(function (options) { - if (options.type === "post" && options.url[0] === "/") { + if (["post","put","delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") { if (options.data) { options.data += ("&" + xsrf); } else { @@ -2586,10 +2589,9 @@ var VirtualScrollMixin = { }; var FilterInput = React.createClass({displayName: 'FilterInput', getInitialState: function () { - // Focus: Show popover - // Mousefocus: Mouse over Tooltip - // onBlur is triggered before click on tooltip, - // hiding the tooltip before link is clicked. + // Consider both focus and mouseover for showing/hiding the tooltip, + // because onBlur of the input is triggered before the click on the tooltip + // finalized, hiding the tooltip just as the user clicks on it. return { value: this.props.value, focus: false, @@ -2604,16 +2606,14 @@ var FilterInput = React.createClass({displayName: 'FilterInput', this.setState({ value: nextValue }); - try { - Filt.parse(nextValue); - } catch (err) { - return; + // Only propagate valid filters upwards. + if (this.isValid(nextValue)) { + this.props.onChange(nextValue); } - this.props.onChange(nextValue); }, - isValid: function () { + isValid: function (filt) { try { - Filt.parse(this.state.value); + Filt.parse(filt || this.state.value); return true; } catch (e) { return false; @@ -2650,16 +2650,14 @@ var FilterInput = React.createClass({displayName: 'FilterInput', this.setState({mousefocus: false}); }, onKeyDown: function (e) { - if (e.target.value === "" && - e.keyCode === Key.BACKSPACE) { - e.preventDefault(); - this.remove(); + if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) { + this.blur(); + // If closed using ESC/ENTER, hide the tooltip. + this.setState({mousefocus: false}); } }, - remove: function () { - if (this.props.onRemove) { - this.props.onRemove(); - } + blur: function () { + this.refs.input.getDOMNode().blur(); }, focus: function () { this.refs.input.getDOMNode().select(); @@ -2686,7 +2684,7 @@ var FilterInput = React.createClass({displayName: 'FilterInput', React.createElement("span", {className: "input-group-addon"}, React.createElement("i", {className: icon, style: {color: this.props.color}}) ), - React.createElement("input", {type: "text", placeholder: "filter expression", className: "form-control", + React.createElement("input", {type: "text", placeholder: this.props.placeholder, className: "form-control", ref: "input", onChange: this.onChange, onFocus: this.onFocus, @@ -2701,12 +2699,6 @@ var FilterInput = React.createClass({displayName: 'FilterInput', var MainMenu = React.createClass({displayName: 'MainMenu', mixins: [Navigation, State], - getInitialState: function () { - return { - filter: this.getQuery()[Query.FILTER] || "", - highlight: this.getQuery()[Query.HIGHLIGHT] || "" - }; - }, statics: { title: "Traffic", route: "flows" @@ -2717,39 +2709,59 @@ var MainMenu = React.createClass({displayName: 'MainMenu', }); }, clearFlows: function () { - $.post("/clear"); + jQuery.post("/clear"); }, - applyFilter: function (filter, highlight) { + onFilterChange: function (val) { var d = {}; - d[Query.FILTER] = filter; - d[Query.HIGHLIGHT] = highlight; + d[Query.FILTER] = val; this.setQuery(d); }, - onFilterChange: function (val) { - this.setState({filter: val}); - this.applyFilter(val, this.state.highlight); - }, onHighlightChange: function (val) { - this.setState({highlight: val}); - this.applyFilter(this.state.filter, val); + var d = {}; + d[Query.HIGHLIGHT] = val; + this.setQuery(d); + }, + onInterceptChange: function (val) { + SettingsActions.update({intercept: val}); }, render: function () { + var filter = this.getQuery()[Query.FILTER] || ""; + var highlight = this.getQuery()[Query.HIGHLIGHT] || ""; + var intercept = this.props.settings.intercept || ""; + return ( React.createElement("div", null, React.createElement("button", {className: "btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default"), onClick: this.toggleEventLog}, React.createElement("i", {className: "fa fa-database"}), " Display Event Log" ), - " ", + React.createElement("span", null, " "), React.createElement("button", {className: "btn btn-default", onClick: this.clearFlows}, React.createElement("i", {className: "fa fa-eraser"}), " Clear Flows" ), - " ", + React.createElement("span", null, " "), React.createElement("form", {className: "form-inline", style: {display: "inline"}}, - React.createElement(FilterInput, {type: "filter", color: "black", value: this.state.filter, onChange: this.onFilterChange}), - " ", - React.createElement(FilterInput, {type: "tag", color: "hsl(48, 100%, 50%)", value: this.state.highlight, onChange: this.onHighlightChange}) + React.createElement(FilterInput, { + placeholder: "Filter", + type: "filter", + color: "black", + value: filter, + onChange: this.onFilterChange}), + React.createElement("span", null, " "), + React.createElement(FilterInput, { + placeholder: "Highlight", + type: "tag", + color: "hsl(48, 100%, 50%)", + value: highlight, + onChange: this.onHighlightChange}), + React.createElement("span", null, " "), + React.createElement(FilterInput, { + placeholder: "Intercept", + type: "pause", + color: "hsl(208, 56%, 53%)", + value: intercept, + onChange: this.onInterceptChange}) ) ) ); @@ -3067,6 +3079,15 @@ var FlowRow = React.createClass({displayName: 'FlowRow', if (this.props.highlighted) { className += " highlighted"; } + if (flow.intercepted) { + className += " intercepted"; + } + if (flow.request) { + className += " has-request"; + } + if (flow.response) { + className += " has-response"; + } return ( React.createElement("tr", {className: className, onClick: this.props.selectFlow.bind(null, flow)}, @@ -3544,12 +3565,12 @@ var MainView = React.createClass({displayName: 'MainView', var filt = Filt.parse(this.getQuery()[Query.FILTER] || ""); var highlightStr = this.getQuery()[Query.HIGHLIGHT]; var highlight = highlightStr ? Filt.parse(highlightStr) : false; - } catch(e){ + } catch (e) { console.error("Error when processing filter: " + e); } return function filter_and_highlight(flow) { - if(!this._highlight){ + if (!this._highlight) { this._highlight = {}; } this._highlight[flow.id] = highlight && highlight(flow); @@ -3671,6 +3692,13 @@ var MainView = React.createClass({displayName: 'MainView', this.refs.flowDetails.nextTab(+1); } break; + case Key.A: + if (e.shiftKey) { + $.post("/flows/accept"); + } else if(this.getSelected()) { + $.post("/flows/" + this.getSelected().id + "/accept"); + } + break; default: console.debug("keydown", e.keyCode); return; @@ -3855,9 +3883,12 @@ var EventLog = React.createClass({displayName: 'EventLog', var Footer = React.createClass({displayName: 'Footer', render: function () { var mode = this.props.settings.mode; + var intercept = this.props.settings.intercept; return ( React.createElement("footer", null, - mode != "regular" ? React.createElement("span", {className: "label label-success"}, mode, " mode") : null + mode != "regular" ? React.createElement("span", {className: "label label-success"}, mode, " mode") : null, + " ", + intercept ? React.createElement("span", {className: "label label-success"}, "Intercept: ", intercept) : null ) ); } diff --git a/test/test_flow.py b/test/test_flow.py index 48f5ba55..764b9f24 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -344,7 +344,7 @@ class TestFlow: s = flow.State() fm = flow.FlowMaster(None, s) f = tutils.tflow() - f.intercept() + f.intercept(mock.Mock()) assert not f.reply.acked f.kill(fm) assert f.reply.acked @@ -368,9 +368,9 @@ class TestFlow: def test_accept_intercept(self): f = tutils.tflow() - f.intercept() + f.intercept(mock.Mock()) assert not f.reply.acked - f.accept_intercept() + f.accept_intercept(mock.Mock()) assert f.reply.acked def test_replace_unicode(self): @@ -520,7 +520,7 @@ class TestState: def test_clear(self): c = flow.State() f = self._add_request(c) - f.intercepting = True + f.intercepted = True c.clear() assert c.flow_count() == 0 @@ -546,7 +546,7 @@ class TestState: self._add_request(c) self._add_response(c) self._add_request(c) - c.accept_all() + c.accept_all(mock.Mock()) class TestSerialize: @@ -660,7 +660,7 @@ class TestFlowMaster: f.request.content = CONTENT_MISSING assert "missing" in fm.replay_request(f) - f.intercepting = True + f.intercepted = True assert "intercepting" in fm.replay_request(f) f.live = True diff --git a/web/gulpfile.js b/web/gulpfile.js index 8dc888e7..5a0b93af 100644 --- a/web/gulpfile.js +++ b/web/gulpfile.js @@ -40,7 +40,6 @@ var path = { 'js/flow/utils.js', 'js/store/store.js', 'js/store/view.js', - 'js/store/settingstore.js', 'js/connection.js', 'js/components/utils.jsx.js', 'js/components/virtualscroll.jsx.js', diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less index 2b0e8df3..b18a71fa 100644 --- a/web/src/css/flowtable.less +++ b/web/src/css/flowtable.less @@ -40,7 +40,16 @@ text-overflow: ellipsis; } - tr + tr.intercepted:not(.has-response) { + .col-path, .col-method { + color: hsl(30, 100%, 50%); + } + } + tr.intercepted.has-response { + .col-status, .col-size, .col-time { + color: hsl(30, 100%, 50%); + } + } .col-tls { width: 10px; diff --git a/web/src/js/actions.js b/web/src/js/actions.js index 863663f3..e7799118 100644 --- a/web/src/js/actions.js +++ b/web/src/js/actions.js @@ -38,7 +38,11 @@ var ConnectionActions = { var SettingsActions = { update: function (settings) { - //TODO: Update server. + jQuery.ajax({ + type: "PUT", + url: "/settings", + data: settings + }); //Facebook Flux: We do an optimistic update on the client already. AppDispatcher.dispatchViewAction({ diff --git a/web/src/js/components/flowtable.jsx.js b/web/src/js/components/flowtable.jsx.js index efc975a6..a3a37c40 100644 --- a/web/src/js/components/flowtable.jsx.js +++ b/web/src/js/components/flowtable.jsx.js @@ -11,6 +11,15 @@ var FlowRow = React.createClass({ if (this.props.highlighted) { className += " highlighted"; } + if (flow.intercepted) { + className += " intercepted"; + } + if (flow.request) { + className += " has-request"; + } + if (flow.response) { + className += " has-response"; + } return ( <tr className={className} onClick={this.props.selectFlow.bind(null, flow)}> diff --git a/web/src/js/components/footer.jsx.js b/web/src/js/components/footer.jsx.js index 73fadef2..52d52e0f 100644 --- a/web/src/js/components/footer.jsx.js +++ b/web/src/js/components/footer.jsx.js @@ -1,9 +1,12 @@ var Footer = React.createClass({ render: function () { var mode = this.props.settings.mode; + var intercept = this.props.settings.intercept; return ( <footer> {mode != "regular" ? <span className="label label-success">{mode} mode</span> : null} + + {intercept ? <span className="label label-success">Intercept: {intercept}</span> : null} </footer> ); } diff --git a/web/src/js/components/header.jsx.js b/web/src/js/components/header.jsx.js index 9e090770..cb9cd149 100644 --- a/web/src/js/components/header.jsx.js +++ b/web/src/js/components/header.jsx.js @@ -1,9 +1,8 @@ var FilterInput = React.createClass({ getInitialState: function () { - // Focus: Show popover - // Mousefocus: Mouse over Tooltip - // onBlur is triggered before click on tooltip, - // hiding the tooltip before link is clicked. + // Consider both focus and mouseover for showing/hiding the tooltip, + // because onBlur of the input is triggered before the click on the tooltip + // finalized, hiding the tooltip just as the user clicks on it. return { value: this.props.value, focus: false, @@ -18,16 +17,14 @@ var FilterInput = React.createClass({ this.setState({ value: nextValue }); - try { - Filt.parse(nextValue); - } catch (err) { - return; + // Only propagate valid filters upwards. + if (this.isValid(nextValue)) { + this.props.onChange(nextValue); } - this.props.onChange(nextValue); }, - isValid: function () { + isValid: function (filt) { try { - Filt.parse(this.state.value); + Filt.parse(filt || this.state.value); return true; } catch (e) { return false; @@ -64,16 +61,14 @@ var FilterInput = React.createClass({ this.setState({mousefocus: false}); }, onKeyDown: function (e) { - if (e.target.value === "" && - e.keyCode === Key.BACKSPACE) { - e.preventDefault(); - this.remove(); + if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) { + this.blur(); + // If closed using ESC/ENTER, hide the tooltip. + this.setState({mousefocus: false}); } }, - remove: function () { - if (this.props.onRemove) { - this.props.onRemove(); - } + blur: function () { + this.refs.input.getDOMNode().blur(); }, focus: function () { this.refs.input.getDOMNode().select(); @@ -100,7 +95,7 @@ var FilterInput = React.createClass({ <span className="input-group-addon"> <i className={icon} style={{color: this.props.color}}></i> </span> - <input type="text" placeholder="filter expression" className="form-control" + <input type="text" placeholder={this.props.placeholder} className="form-control" ref="input" onChange={this.onChange} onFocus={this.onFocus} @@ -115,12 +110,6 @@ var FilterInput = React.createClass({ var MainMenu = React.createClass({ mixins: [Navigation, State], - getInitialState: function () { - return { - filter: this.getQuery()[Query.FILTER] || "", - highlight: this.getQuery()[Query.HIGHLIGHT] || "" - }; - }, statics: { title: "Traffic", route: "flows" @@ -131,39 +120,59 @@ var MainMenu = React.createClass({ }); }, clearFlows: function () { - $.post("/clear"); + jQuery.post("/clear"); }, - applyFilter: function (filter, highlight) { + onFilterChange: function (val) { var d = {}; - d[Query.FILTER] = filter; - d[Query.HIGHLIGHT] = highlight; + d[Query.FILTER] = val; this.setQuery(d); }, - onFilterChange: function (val) { - this.setState({filter: val}); - this.applyFilter(val, this.state.highlight); - }, onHighlightChange: function (val) { - this.setState({highlight: val}); - this.applyFilter(this.state.filter, val); + var d = {}; + d[Query.HIGHLIGHT] = val; + this.setQuery(d); + }, + onInterceptChange: function (val) { + SettingsActions.update({intercept: val}); }, render: function () { + var filter = this.getQuery()[Query.FILTER] || ""; + var highlight = this.getQuery()[Query.HIGHLIGHT] || ""; + var intercept = this.props.settings.intercept || ""; + return ( <div> <button className={"btn " + (this.props.settings.showEventLog ? "btn-primary" : "btn-default")} onClick={this.toggleEventLog}> <i className="fa fa-database"></i> Display Event Log </button> - + <span> </span> <button className="btn btn-default" onClick={this.clearFlows}> <i className="fa fa-eraser"></i> Clear Flows </button> - + <span> </span> <form className="form-inline" style={{display: "inline"}}> - <FilterInput type="filter" color="black" value={this.state.filter} onChange={this.onFilterChange} /> - - <FilterInput type="tag" color="hsl(48, 100%, 50%)" value={this.state.highlight} onChange={this.onHighlightChange}/> + <FilterInput + placeholder="Filter" + type="filter" + color="black" + value={filter} + onChange={this.onFilterChange} /> + <span> </span> + <FilterInput + placeholder="Highlight" + type="tag" + color="hsl(48, 100%, 50%)" + value={highlight} + onChange={this.onHighlightChange}/> + <span> </span> + <FilterInput + placeholder="Intercept" + type="pause" + color="hsl(208, 56%, 53%)" + value={intercept} + onChange={this.onInterceptChange}/> </form> </div> ); diff --git a/web/src/js/components/mainview.jsx.js b/web/src/js/components/mainview.jsx.js index 113b0896..046d6af0 100644 --- a/web/src/js/components/mainview.jsx.js +++ b/web/src/js/components/mainview.jsx.js @@ -16,12 +16,12 @@ var MainView = React.createClass({ var filt = Filt.parse(this.getQuery()[Query.FILTER] || ""); var highlightStr = this.getQuery()[Query.HIGHLIGHT]; var highlight = highlightStr ? Filt.parse(highlightStr) : false; - } catch(e){ + } catch (e) { console.error("Error when processing filter: " + e); } return function filter_and_highlight(flow) { - if(!this._highlight){ + if (!this._highlight) { this._highlight = {}; } this._highlight[flow.id] = highlight && highlight(flow); @@ -143,6 +143,13 @@ var MainView = React.createClass({ this.refs.flowDetails.nextTab(+1); } break; + case Key.A: + if (e.shiftKey) { + $.post("/flows/accept"); + } else if(this.getSelected()) { + $.post("/flows/" + this.getSelected().id + "/accept"); + } + break; default: console.debug("keydown", e.keyCode); return; diff --git a/web/src/js/components/utils.jsx.js b/web/src/js/components/utils.jsx.js index 81ba6b4d..20dbda94 100644 --- a/web/src/js/components/utils.jsx.js +++ b/web/src/js/components/utils.jsx.js @@ -113,7 +113,7 @@ var xsrf = $.param({_xsrf: getCookie("_xsrf")}); //Tornado XSRF Protection. $.ajaxPrefilter(function (options) { - if (options.type === "post" && options.url[0] === "/") { + if (["post","put","delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") { if (options.data) { options.data += ("&" + xsrf); } else { diff --git a/web/src/js/store/settingstore.js b/web/src/js/store/settingstore.js deleted file mode 100644 index e69de29b..00000000 --- a/web/src/js/store/settingstore.js +++ /dev/null diff --git a/web/src/js/utils.js b/web/src/js/utils.js index b96aed0b..082f7272 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -90,11 +90,11 @@ var Key = { TAB: 9, SPACE: 32, BACKSPACE: 8, - J: 74, - K: 75, - H: 72, - L: 76 }; +// Add A-Z +for(var i=65; i <= 90; i++){ + Key[String.fromCharCode(i)] = i; +} var formatSize = function (bytes) { |