aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--libmproxy/console/__init__.py4
-rw-r--r--libmproxy/console/common.py6
-rw-r--r--libmproxy/console/flowlist.py2
-rw-r--r--libmproxy/console/flowview.py6
-rw-r--r--libmproxy/flow.py16
-rw-r--r--libmproxy/protocol/http.py29
-rw-r--r--libmproxy/protocol/primitives.py32
-rw-r--r--libmproxy/web/__init__.py20
-rw-r--r--libmproxy/web/app.py55
-rw-r--r--libmproxy/web/static/css/app.css11
-rw-r--r--libmproxy/web/static/js/app.js133
-rw-r--r--test/test_flow.py12
-rw-r--r--web/gulpfile.js1
-rw-r--r--web/src/css/flowtable.less11
-rw-r--r--web/src/js/actions.js6
-rw-r--r--web/src/js/components/flowtable.jsx.js9
-rw-r--r--web/src/js/components/footer.jsx.js3
-rw-r--r--web/src/js/components/header.jsx.js91
-rw-r--r--web/src/js/components/mainview.jsx.js11
-rw-r--r--web/src/js/components/utils.jsx.js2
-rw-r--r--web/src/js/store/settingstore.js0
-rw-r--r--web/src/js/utils.js8
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}
+ &nbsp;
+ {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>
&nbsp;Display Event Log
</button>
- &nbsp;
+ <span> </span>
<button className="btn btn-default" onClick={this.clearFlows}>
<i className="fa fa-eraser"></i>
&nbsp;Clear Flows
</button>
- &nbsp;
+ <span> </span>
<form className="form-inline" style={{display: "inline"}}>
- <FilterInput type="filter" color="black" value={this.state.filter} onChange={this.onFilterChange} />
- &nbsp;
- <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) {