From c39b6e4277357c9da1dfd5e3e8c41b5b3427e0ce Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 28 Nov 2014 19:16:47 +0100 Subject: web: various fixes, add clear button --- libmproxy/web/__init__.py | 2 +- libmproxy/web/app.py | 14 ++++- libmproxy/web/static/js/app.js | 105 ++++++++++++++++++++++---------- web/src/js/components/flowdetail.jsx.js | 5 +- web/src/js/components/flowtable.jsx.js | 25 ++++---- web/src/js/components/header.jsx.js | 9 ++- web/src/js/components/mainview.jsx.js | 3 +- web/src/js/components/utils.jsx.js | 17 ++++++ web/src/js/stores/flowstore.js | 46 +++++++++----- 9 files changed, 156 insertions(+), 70 deletions(-) diff --git a/libmproxy/web/__init__.py b/libmproxy/web/__init__.py index f762466a..a110aa4d 100644 --- a/libmproxy/web/__init__.py +++ b/libmproxy/web/__init__.py @@ -27,7 +27,7 @@ class WebFlowView(flow.FlowView): def _recalculate(self, flows): super(WebFlowView, self)._recalculate(flows) - app.FlowUpdates.broadcast("recalculate", None) + app.FlowUpdates.broadcast("reset", None) class WebState(flow.State): diff --git a/libmproxy/web/app.py b/libmproxy/web/app.py index 4fdff783..05ca7e79 100644 --- a/libmproxy/web/app.py +++ b/libmproxy/web/app.py @@ -1,4 +1,5 @@ import os.path +import sys import tornado.web import tornado.websocket import logging @@ -8,6 +9,7 @@ from .. import flow class IndexHandler(tornado.web.RequestHandler): def get(self): + _ = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645 self.render("index.html") @@ -35,13 +37,18 @@ class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): logging.error("Error sending message", exc_info=True) -class FlowsHandler(tornado.web.RequestHandler): +class Flows(tornado.web.RequestHandler): def get(self): self.write(dict( flows=[f.get_state(short=True) for f in self.application.state.flows] )) +class FlowClear(tornado.web.RequestHandler): + def post(self): + self.application.state.clear() + + class FlowUpdates(WebSocketEventBroadcaster): connections = set() @@ -56,14 +63,15 @@ class Application(tornado.web.Application): handlers = [ (r"/", IndexHandler), (r"/updates", ClientConnection), - (r"/flows", FlowsHandler), + (r"/flows", Flows), + (r"/flows/clear", FlowClear), (r"/flows/updates", FlowUpdates), ] settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, - cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", + cookie_secret=os.urandom(256), debug=debug, ) tornado.web.Application.__init__(self, handlers, **settings) diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index dd1259f4..64d6ba44 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -386,7 +386,8 @@ _.extend(FlowStore.prototype, { function LiveFlowStore(endpoint) { FlowStore.call(this); - this.updates_before_init = []; // (empty array is true in js) + this.updates_before_fetch = undefined; + this._fetchxhr = false; this.endpoint = endpoint || "/flows"; this.conn = new Connection(this.endpoint + "/updates"); this.conn.onopen = this._onopen.bind(this); @@ -401,33 +402,46 @@ _.extend(LiveFlowStore.prototype, FlowStore.prototype, { }, add: function (flow) { // Make sure that deferred adds don't add an element twice. - if (!this._pos_map[flow.id]) { + if (!(flow.id in this._pos_map)) { FlowStore.prototype.add.call(this, flow); } }, + _onopen: function () { + //Update stream openend, fetch list of flows. + console.log("Update Connection opened, fetching flows..."); + this.fetch(); + }, + fetch: function () { + if (this._fetchxhr) { + this._fetchxhr.abort(); + } + this._fetchxhr = $.getJSON(this.endpoint, this.handle_fetch.bind(this)); + this.updates_before_fetch = []; // (JS: empty array is true) + }, handle_update: function (type, data) { console.log("LiveFlowStore.handle_update", type, data); - if (this.updates_before_init) { + + if (type === "reset") { + return this.fetch(); + } + + if (this.updates_before_fetch) { console.log("defer update", type, data); - this.updates_before_init.push(arguments); + this.updates_before_fetch.push(arguments); } else { this[type](data); } }, handle_fetch: function (data) { + this._fetchxhr = false; console.log("Flows fetched."); this.reset(data.flows); - var updates = this.updates_before_init; - this.updates_before_init = false; + var updates = this.updates_before_fetch; + this.updates_before_fetch = false; for (var i = 0; i < updates.length; i++) { this.handle_update.apply(this, updates[i]); } }, - _onopen: function () { - //Update stream openend, fetch list of flows. - console.log("Update Connection opened, fetching flows..."); - $.getJSON(this.endpoint, this.handle_fetch.bind(this)); - }, }); function SortByInsertionOrder() { @@ -471,20 +485,22 @@ _.extend(FlowView.prototype, EventEmitter.prototype, { //Ugly workaround: Call .sortfun() for each flow once in order, //so that SortByInsertionOrder make sense. - var i = flows.length; - while(i--){ + for(var i = 0; i < flows.length; i++) { this.sortfun(flows[i]); } this.flows = flows.filter(this.filt); this.flows.sort(function (a, b) { - return this.sortfun(b) - this.sortfun(a); + return this.sortfun(a) - this.sortfun(b); }.bind(this)); this.emit("recalculate"); }, + index: function (flow) { + return _.sortedIndex(this.flows, flow, this.sortfun); + }, add: function (flow) { if (this.filt(flow)) { - var idx = _.sortedIndex(this.flows, flow, this.sortfun); + var idx = this.index(flow); if (idx === this.flows.length) { //happens often, .push is way faster. this.flows.push(flow); } else { @@ -665,6 +681,23 @@ var Splitter = React.createClass({displayName: 'Splitter', ); } }); + +function getCookie(name) { + var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + return r ? r[1] : undefined; +} +var xsrf = $.param({_xsrf: getCookie("_xsrf")}); + +//Tornado XSRF Protection. +$.ajaxPrefilter(function(options){ + if(options.type === "post" && options.url[0] === "/"){ + if(options.data){ + options.data += ("&" + xsrf); + } else { + options.data = xsrf; + } + } +}); var MainMenu = React.createClass({displayName: 'MainMenu', statics: { title: "Traffic", @@ -675,12 +708,17 @@ var MainMenu = React.createClass({displayName: 'MainMenu', showEventLog: !this.props.settings.showEventLog }); }, + clearFlows: function(){ + $.post("/flows/clear"); + }, render: function () { 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("i", {className: "fa fa-database"}), " Display Event Log" + ), " ", + React.createElement("button", {className: "btn btn-default", onClick: this.clearFlows}, + React.createElement("i", {className: "fa fa-eraser"}), " Clear Flows" ) ) ); @@ -999,20 +1037,23 @@ var FlowTable = React.createClass({displayName: 'FlowTable', scrollIntoView: function (flow) { // Now comes the fun part: Scroll the flow into the view. var viewport = this.getDOMNode(); - var flowNode = this.refs.body.refs[flow.id].getDOMNode(); + var thead_height = this.refs.body.getDOMNode().offsetTop; + + var flow_top = (this.props.view.index(flow) * ROW_HEIGHT) + thead_height; + var viewport_top = viewport.scrollTop; var viewport_bottom = viewport_top + viewport.offsetHeight; - var flowNode_top = flowNode.offsetTop; - var flowNode_bottom = flowNode_top + flowNode.offsetHeight; + var flow_bottom = flow_top + ROW_HEIGHT; - // Account for pinned thead by pretending that the flowNode starts - // -thead_height pixel earlier. - flowNode_top -= this.refs.body.getDOMNode().offsetTop; + // Account for pinned thead - if (flowNode_top < viewport_top) { - viewport.scrollTop = flowNode_top; - } else if (flowNode_bottom > viewport_bottom) { - viewport.scrollTop = flowNode_bottom - viewport.offsetHeight; + + console.log("scrollInto", flow_top, flow_bottom, viewport_top, viewport_bottom, thead_height); + + if (flow_top - thead_height < viewport_top) { + viewport.scrollTop = flow_top - thead_height; + } else if (flow_bottom > viewport_bottom) { + viewport.scrollTop = flow_bottom - viewport.offsetHeight; } }, render: function () { @@ -1050,7 +1091,7 @@ var FlowTable = React.createClass({displayName: 'FlowTable', React.createElement("table", null, React.createElement(FlowTableHead, {ref: "head", columns: this.state.columns}), - React.createElement("tbody", null, + React.createElement("tbody", {ref: "body"}, React.createElement("tr", {style: {height: space_top}}), fix_nth_row, rows, @@ -1068,9 +1109,9 @@ var FlowDetailNav = React.createClass({displayName: 'FlowDetailNav', var items = this.props.tabs.map(function (e) { var str = e.charAt(0).toUpperCase() + e.slice(1); var className = this.props.active === e ? "active" : ""; - var onClick = function (e) { + var onClick = function (event) { this.props.selectTab(e); - e.preventDefault(); + event.preventDefault(); }.bind(this); return React.createElement("a", {key: e, href: "#", @@ -1366,7 +1407,6 @@ var FlowDetail = React.createClass({displayName: 'FlowDetail', ); }, render: function () { - var flow = JSON.stringify(this.props.flow, null, 2); var Tab = tabs[this.props.active]; return ( React.createElement("div", {className: "flow-detail", onScroll: this.adjustHead}, @@ -1416,8 +1456,7 @@ var MainView = React.createClass({displayName: 'MainView', detailTab: this.getParams().detailTab || "request" } ); - console.log("TODO: Scroll into view"); - //this.refs.flowTable.scrollIntoView(flow); + this.refs.flowTable.scrollIntoView(flow); } else { this.replaceWith("flows"); } diff --git a/web/src/js/components/flowdetail.jsx.js b/web/src/js/components/flowdetail.jsx.js index 5c4168a9..74522f57 100644 --- a/web/src/js/components/flowdetail.jsx.js +++ b/web/src/js/components/flowdetail.jsx.js @@ -4,9 +4,9 @@ var FlowDetailNav = React.createClass({ var items = this.props.tabs.map(function (e) { var str = e.charAt(0).toUpperCase() + e.slice(1); var className = this.props.active === e ? "active" : ""; - var onClick = function (e) { + var onClick = function (event) { this.props.selectTab(e); - e.preventDefault(); + event.preventDefault(); }.bind(this); return diff --git a/web/src/js/components/flowtable.jsx.js b/web/src/js/components/flowtable.jsx.js index 76ceea41..6d0f5ee7 100644 --- a/web/src/js/components/flowtable.jsx.js +++ b/web/src/js/components/flowtable.jsx.js @@ -83,20 +83,23 @@ var FlowTable = React.createClass({ scrollIntoView: function (flow) { // Now comes the fun part: Scroll the flow into the view. var viewport = this.getDOMNode(); - var flowNode = this.refs.body.refs[flow.id].getDOMNode(); + var thead_height = this.refs.body.getDOMNode().offsetTop; + + var flow_top = (this.props.view.index(flow) * ROW_HEIGHT) + thead_height; + var viewport_top = viewport.scrollTop; var viewport_bottom = viewport_top + viewport.offsetHeight; - var flowNode_top = flowNode.offsetTop; - var flowNode_bottom = flowNode_top + flowNode.offsetHeight; + var flow_bottom = flow_top + ROW_HEIGHT; + + // Account for pinned thead + - // Account for pinned thead by pretending that the flowNode starts - // -thead_height pixel earlier. - flowNode_top -= this.refs.body.getDOMNode().offsetTop; + console.log("scrollInto", flow_top, flow_bottom, viewport_top, viewport_bottom, thead_height); - if (flowNode_top < viewport_top) { - viewport.scrollTop = flowNode_top; - } else if (flowNode_bottom > viewport_bottom) { - viewport.scrollTop = flowNode_bottom - viewport.offsetHeight; + if (flow_top - thead_height < viewport_top) { + viewport.scrollTop = flow_top - thead_height; + } else if (flow_bottom > viewport_bottom) { + viewport.scrollTop = flow_bottom - viewport.offsetHeight; } }, render: function () { @@ -134,7 +137,7 @@ var FlowTable = React.createClass({ - + { fix_nth_row } {rows} diff --git a/web/src/js/components/header.jsx.js b/web/src/js/components/header.jsx.js index e50c4274..a3fe4d51 100644 --- a/web/src/js/components/header.jsx.js +++ b/web/src/js/components/header.jsx.js @@ -8,12 +8,17 @@ var MainMenu = React.createClass({ showEventLog: !this.props.settings.showEventLog }); }, + clearFlows: function(){ + $.post("/flows/clear"); + }, render: function () { return (
  +
); diff --git a/web/src/js/components/mainview.jsx.js b/web/src/js/components/mainview.jsx.js index fd9fdb8d..fe5d1c7c 100644 --- a/web/src/js/components/mainview.jsx.js +++ b/web/src/js/components/mainview.jsx.js @@ -35,8 +35,7 @@ var MainView = React.createClass({ detailTab: this.getParams().detailTab || "request" } ); - console.log("TODO: Scroll into view"); - //this.refs.flowTable.scrollIntoView(flow); + this.refs.flowTable.scrollIntoView(flow); } else { this.replaceWith("flows"); } diff --git a/web/src/js/components/utils.jsx.js b/web/src/js/components/utils.jsx.js index b1d9a006..12775adc 100644 --- a/web/src/js/components/utils.jsx.js +++ b/web/src/js/components/utils.jsx.js @@ -95,4 +95,21 @@ var Splitter = React.createClass({ ); } +}); + +function getCookie(name) { + var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + return r ? r[1] : undefined; +} +var xsrf = $.param({_xsrf: getCookie("_xsrf")}); + +//Tornado XSRF Protection. +$.ajaxPrefilter(function(options){ + if(options.type === "post" && options.url[0] === "/"){ + if(options.data){ + options.data += ("&" + xsrf); + } else { + options.data = xsrf; + } + } }); \ No newline at end of file diff --git a/web/src/js/stores/flowstore.js b/web/src/js/stores/flowstore.js index 37eb40eb..cc7318a2 100644 --- a/web/src/js/stores/flowstore.js +++ b/web/src/js/stores/flowstore.js @@ -45,7 +45,8 @@ _.extend(FlowStore.prototype, { function LiveFlowStore(endpoint) { FlowStore.call(this); - this.updates_before_init = []; // (empty array is true in js) + this.updates_before_fetch = undefined; + this._fetchxhr = false; this.endpoint = endpoint || "/flows"; this.conn = new Connection(this.endpoint + "/updates"); this.conn.onopen = this._onopen.bind(this); @@ -60,33 +61,46 @@ _.extend(LiveFlowStore.prototype, FlowStore.prototype, { }, add: function (flow) { // Make sure that deferred adds don't add an element twice. - if (!this._pos_map[flow.id]) { + if (!(flow.id in this._pos_map)) { FlowStore.prototype.add.call(this, flow); } }, + _onopen: function () { + //Update stream openend, fetch list of flows. + console.log("Update Connection opened, fetching flows..."); + this.fetch(); + }, + fetch: function () { + if (this._fetchxhr) { + this._fetchxhr.abort(); + } + this._fetchxhr = $.getJSON(this.endpoint, this.handle_fetch.bind(this)); + this.updates_before_fetch = []; // (JS: empty array is true) + }, handle_update: function (type, data) { console.log("LiveFlowStore.handle_update", type, data); - if (this.updates_before_init) { + + if (type === "reset") { + return this.fetch(); + } + + if (this.updates_before_fetch) { console.log("defer update", type, data); - this.updates_before_init.push(arguments); + this.updates_before_fetch.push(arguments); } else { this[type](data); } }, handle_fetch: function (data) { + this._fetchxhr = false; console.log("Flows fetched."); this.reset(data.flows); - var updates = this.updates_before_init; - this.updates_before_init = false; + var updates = this.updates_before_fetch; + this.updates_before_fetch = false; for (var i = 0; i < updates.length; i++) { this.handle_update.apply(this, updates[i]); } }, - _onopen: function () { - //Update stream openend, fetch list of flows. - console.log("Update Connection opened, fetching flows..."); - $.getJSON(this.endpoint, this.handle_fetch.bind(this)); - }, }); function SortByInsertionOrder() { @@ -130,20 +144,22 @@ _.extend(FlowView.prototype, EventEmitter.prototype, { //Ugly workaround: Call .sortfun() for each flow once in order, //so that SortByInsertionOrder make sense. - var i = flows.length; - while(i--){ + for(var i = 0; i < flows.length; i++) { this.sortfun(flows[i]); } this.flows = flows.filter(this.filt); this.flows.sort(function (a, b) { - return this.sortfun(b) - this.sortfun(a); + return this.sortfun(a) - this.sortfun(b); }.bind(this)); this.emit("recalculate"); }, + index: function (flow) { + return _.sortedIndex(this.flows, flow, this.sortfun); + }, add: function (flow) { if (this.filt(flow)) { - var idx = _.sortedIndex(this.flows, flow, this.sortfun); + var idx = this.index(flow); if (idx === this.flows.length) { //happens often, .push is way faster. this.flows.push(flow); } else { -- cgit v1.2.3