mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2025-01-30 23:09:44 +00:00
web: intercept feature
This commit is contained in:
parent
459772a8ef
commit
2861d99de4
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
def __init__(self, state, debug):
|
||||
self.state = state
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self.master.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),
|
||||
]
|
||||
|
@ -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 {
|
||||
|
@ -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");
|
||||
},
|
||||
applyFilter: function (filter, highlight) {
|
||||
var d = {};
|
||||
d[Query.FILTER] = filter;
|
||||
d[Query.HIGHLIGHT] = highlight;
|
||||
this.setQuery(d);
|
||||
jQuery.post("/clear");
|
||||
},
|
||||
onFilterChange: function (val) {
|
||||
this.setState({filter: val});
|
||||
this.applyFilter(val, this.state.highlight);
|
||||
var d = {};
|
||||
d[Query.FILTER] = val;
|
||||
this.setQuery(d);
|
||||
},
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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)}>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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");
|
||||
},
|
||||
applyFilter: function (filter, highlight) {
|
||||
var d = {};
|
||||
d[Query.FILTER] = filter;
|
||||
d[Query.HIGHLIGHT] = highlight;
|
||||
this.setQuery(d);
|
||||
jQuery.post("/clear");
|
||||
},
|
||||
onFilterChange: function (val) {
|
||||
this.setState({filter: val});
|
||||
this.applyFilter(val, this.state.highlight);
|
||||
var d = {};
|
||||
d[Query.FILTER] = val;
|
||||
this.setQuery(d);
|
||||
},
|
||||
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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user