aboutsummaryrefslogtreecommitdiffstats
path: root/libmproxy/web
diff options
context:
space:
mode:
Diffstat (limited to 'libmproxy/web')
-rw-r--r--libmproxy/web/__init__.py2
-rw-r--r--libmproxy/web/app.py96
-rw-r--r--libmproxy/web/static/js/app.js248
3 files changed, 214 insertions, 132 deletions
diff --git a/libmproxy/web/__init__.py b/libmproxy/web/__init__.py
index ec3576db..d981ab30 100644
--- a/libmproxy/web/__init__.py
+++ b/libmproxy/web/__init__.py
@@ -35,7 +35,7 @@ class WebFlowView(flow.FlowView):
app.ClientConnection.broadcast(
type="flows",
cmd="remove",
- data=f.get_state(short=True)
+ data=f.id
)
def _recalculate(self, flows):
diff --git a/libmproxy/web/app.py b/libmproxy/web/app.py
index e9fbba8b..c90922cb 100644
--- a/libmproxy/web/app.py
+++ b/libmproxy/web/app.py
@@ -1,12 +1,13 @@
import os.path
-import sys
import tornado.web
import tornado.websocket
import logging
import json
-from .. import flow
+class APIError(tornado.web.HTTPError):
+ pass
+
class IndexHandler(tornado.web.RequestHandler):
def get(self):
_ = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
@@ -36,38 +37,82 @@ class ClientConnection(WebSocketEventBroadcaster):
connections = set()
-class Flows(tornado.web.RequestHandler):
+class RequestHandler(tornado.web.RequestHandler):
+ @property
+ def state(self):
+ return self.application.master.state
+
+ @property
+ def master(self):
+ return self.application.master
+
+ @property
+ def flow(self):
+ flow_id = str(self.path_kwargs["flow_id"])
+ flow = self.state.flows.get(flow_id)
+ if flow:
+ return flow
+ else:
+ raise APIError(400, "Flow not found.")
+
+ def write_error(self, status_code, **kwargs):
+ if "exc_info" in kwargs and isinstance(kwargs["exc_info"][1], APIError):
+ self.finish(kwargs["exc_info"][1].log_message)
+ else:
+ super(RequestHandler, self).write_error(status_code, **kwargs)
+
+
+class Flows(RequestHandler):
def get(self):
self.write(dict(
- data=[f.get_state(short=True) for f in self.application.state.flows]
+ data=[f.get_state(short=True) for f in self.state.flows]
))
-class AcceptFlows(tornado.web.RequestHandler):
+class ClearAll(RequestHandler):
+ def post(self):
+ self.state.clear()
+
+
+class AcceptFlows(RequestHandler):
def post(self):
- self.application.state.flows.accept_all(self.application.master)
+ self.state.flows.accept_all(self.master)
+
+
+class AcceptFlow(RequestHandler):
+ def post(self, flow_id):
+ self.flow.accept_intercept(self.master)
+
+
+class FlowHandler(RequestHandler):
+ def delete(self, flow_id):
+ self.flow.kill(self.master)
+ self.state.delete_flow(self.flow)
-class AcceptFlow(tornado.web.RequestHandler):
+class DuplicateFlow(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
+ self.master.duplicate_flow(self.flow)
-class Events(tornado.web.RequestHandler):
+class ReplayFlow(RequestHandler):
+ def post(self, flow_id):
+ self.flow.backup()
+ r = self.master.replay_request(self.flow)
+ if r:
+ raise APIError(400, r)
+
+class Events(RequestHandler):
def get(self):
self.write(dict(
- data=list(self.application.state.events)
+ data=list(self.state.events)
))
-class Settings(tornado.web.RequestHandler):
+class Settings(RequestHandler):
def get(self):
self.write(dict(
data=dict(
- intercept=self.application.state.intercept_txt
+ intercept=self.state.intercept_txt
)
))
@@ -81,7 +126,7 @@ class Settings(tornado.web.RequestHandler):
if k == "_xsrf":
continue
elif k == "intercept":
- self.application.state.set_intercept(v[0])
+ self.state.set_intercept(v[0])
update[k] = v[0]
else:
print "Warning: Unknown setting {}: {}".format(k, v)
@@ -93,17 +138,7 @@ class Settings(tornado.web.RequestHandler):
)
-class Clear(tornado.web.RequestHandler):
- def post(self):
- self.application.state.clear()
-
-
class Application(tornado.web.Application):
-
- @property
- def state(self):
- return self.master.state
-
def __init__(self, master, debug):
self.master = master
handlers = [
@@ -112,9 +147,12 @@ class Application(tornado.web.Application):
(r"/events", Events),
(r"/flows", Flows),
(r"/flows/accept", AcceptFlows),
- (r"/flows/([0-9a-f\-]+)/accept", AcceptFlow),
+ (r"/flows/(?P<flow_id>[0-9a-f\-]+)", FlowHandler),
+ (r"/flows/(?P<flow_id>[0-9a-f\-]+)/accept", AcceptFlow),
+ (r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),
+ (r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
(r"/settings", Settings),
- (r"/clear", Clear),
+ (r"/clear", ClearAll),
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),
diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js
index b8835449..ff961294 100644
--- a/libmproxy/web/static/js/app.js
+++ b/libmproxy/web/static/js/app.js
@@ -1,81 +1,3 @@
-// http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html (also contains inverse example)
-var AutoScrollMixin = {
- componentWillUpdate: function () {
- var node = this.getDOMNode();
- this._shouldScrollBottom = (
- node.scrollTop !== 0 &&
- node.scrollTop + node.clientHeight === node.scrollHeight
- );
- },
- componentDidUpdate: function () {
- if (this._shouldScrollBottom) {
- var node = this.getDOMNode();
- node.scrollTop = node.scrollHeight;
- }
- },
-};
-
-
-var StickyHeadMixin = {
- adjustHead: function () {
- // Abusing CSS transforms to set the element
- // referenced as head into some kind of position:sticky.
- var head = this.refs.head.getDOMNode();
- head.style.transform = "translate(0," + this.getDOMNode().scrollTop + "px)";
- }
-};
-
-
-var Navigation = _.extend({}, ReactRouter.Navigation, {
- setQuery: function (dict) {
- var q = this.context.getCurrentQuery();
- for(var i in dict){
- if(dict.hasOwnProperty(i)){
- q[i] = dict[i] || undefined; //falsey values shall be removed.
- }
- }
- q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/599
- this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q);
- },
- replaceWith: function(routeNameOrPath, params, query) {
- if(routeNameOrPath === undefined){
- routeNameOrPath = this.context.getCurrentPath();
- }
- if(params === undefined){
- params = this.context.getCurrentParams();
- }
- if(query === undefined){
- query = this.context.getCurrentQuery();
- }
- ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query);
- }
-});
-_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes);
-
-var State = _.extend({}, ReactRouter.State, {
- getInitialState: function () {
- this._query = this.context.getCurrentQuery();
- this._queryWatches = [];
- return null;
- },
- onQueryChange: function (key, callback) {
- this._queryWatches.push({
- key: key,
- callback: callback
- });
- },
- componentWillReceiveProps: function (nextProps, nextState) {
- var q = this.context.getCurrentQuery();
- for (var i = 0; i < this._queryWatches.length; i++) {
- var watch = this._queryWatches[i];
- if (this._query[watch.key] !== q[watch.key]) {
- watch.callback(this._query[watch.key], q[watch.key], watch.key);
- }
- }
- this._query = q;
- }
-});
-
var Key = {
UP: 38,
DOWN: 40,
@@ -92,7 +14,7 @@ var Key = {
BACKSPACE: 8,
};
// Add A-Z
-for(var i=65; i <= 90; i++){
+for (var i = 65; i <= 90; i++) {
Key[String.fromCharCode(i)] = i;
}
@@ -157,6 +79,31 @@ EventEmitter.prototype.removeListener = function (events, f) {
}
}.bind(this));
};
+
+
+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.
+jQuery.ajaxPrefilter(function (options) {
+ if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
+ if (options.data) {
+ options.data += ("&" + xsrf);
+ } else {
+ options.data = xsrf;
+ }
+ }
+});
+// Log AJAX Errors
+$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
+ var message = jqXHR.responseText;
+ console.error(message, arguments);
+ EventLogActions.add_event(thrownError + ": " + message);
+ window.alert(message);
+});
const PayloadSources = {
VIEW: "view",
SERVER: "server"
@@ -272,7 +219,18 @@ var FlowActions = {
accept_all: function(){
jQuery.post("/flows/accept");
},
-
+ "delete": function(flow){
+ jQuery.ajax({
+ type:"DELETE",
+ url: "/flows/" + flow.id
+ });
+ },
+ duplicate: function(flow){
+ jQuery.post("/flows/" + flow.id + "/duplicate");
+ },
+ replay: function(flow){
+ jQuery.post("/flows/" + flow.id + "/replay");
+ },
update: function (flow) {
AppDispatcher.dispatchViewAction({
type: ActionTypes.FLOW_STORE,
@@ -2140,7 +2098,7 @@ _.extend(ListStore.prototype, EventEmitter.prototype, {
this.emit("update", elem);
},
remove: function (elem_id) {
- if (!(elem.id in this._pos_map)) {
+ if (!(elem_id in this._pos_map)) {
return;
}
this.list.splice(this._pos_map[elem_id], 1);
@@ -2317,8 +2275,8 @@ _.extend(StoreView.prototype, EventEmitter.prototype, {
this.store.removeListener("update", this.update);
this.store.removeListener("remove", this.remove);
this.store.removeListener("recalculate", this.recalculate);
- },
- recalculate: function (filt, sortfun) {
+ },
+ recalculate: function (filt, sortfun) {
if (filt) {
this.filt = filt.bind(this);
}
@@ -2408,6 +2366,84 @@ function Connection(url) {
}
//React utils. For other utilities, see ../utils.js
+// http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html (also contains inverse example)
+var AutoScrollMixin = {
+ componentWillUpdate: function () {
+ var node = this.getDOMNode();
+ this._shouldScrollBottom = (
+ node.scrollTop !== 0 &&
+ node.scrollTop + node.clientHeight === node.scrollHeight
+ );
+ },
+ componentDidUpdate: function () {
+ if (this._shouldScrollBottom) {
+ var node = this.getDOMNode();
+ node.scrollTop = node.scrollHeight;
+ }
+ },
+};
+
+
+var StickyHeadMixin = {
+ adjustHead: function () {
+ // Abusing CSS transforms to set the element
+ // referenced as head into some kind of position:sticky.
+ var head = this.refs.head.getDOMNode();
+ head.style.transform = "translate(0," + this.getDOMNode().scrollTop + "px)";
+ }
+};
+
+
+var Navigation = _.extend({}, ReactRouter.Navigation, {
+ setQuery: function (dict) {
+ var q = this.context.getCurrentQuery();
+ for(var i in dict){
+ if(dict.hasOwnProperty(i)){
+ q[i] = dict[i] || undefined; //falsey values shall be removed.
+ }
+ }
+ q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/599
+ this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q);
+ },
+ replaceWith: function(routeNameOrPath, params, query) {
+ if(routeNameOrPath === undefined){
+ routeNameOrPath = this.context.getCurrentPath();
+ }
+ if(params === undefined){
+ params = this.context.getCurrentParams();
+ }
+ if(query === undefined){
+ query = this.context.getCurrentQuery();
+ }
+ ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query);
+ }
+});
+_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes);
+
+var State = _.extend({}, ReactRouter.State, {
+ getInitialState: function () {
+ this._query = this.context.getCurrentQuery();
+ this._queryWatches = [];
+ return null;
+ },
+ onQueryChange: function (key, callback) {
+ this._queryWatches.push({
+ key: key,
+ callback: callback
+ });
+ },
+ componentWillReceiveProps: function (nextProps, nextState) {
+ var q = this.context.getCurrentQuery();
+ for (var i = 0; i < this._queryWatches.length; i++) {
+ var watch = this._queryWatches[i];
+ if (this._query[watch.key] !== q[watch.key]) {
+ watch.callback(this._query[watch.key], q[watch.key], watch.key);
+ }
+ }
+ this._query = q;
+ }
+});
+
var Splitter = React.createClass({displayName: 'Splitter',
getDefaultProps: function () {
return {
@@ -2512,23 +2548,6 @@ 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 (["post","put","delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
- if (options.data) {
- options.data += ("&" + xsrf);
- } else {
- options.data = xsrf;
- }
- }
-});
var VirtualScrollMixin = {
getInitialState: function () {
return {
@@ -3686,6 +3705,10 @@ var MainView = React.createClass({displayName: 'MainView',
this.selectFlow(flows[index]);
},
onKeyDown: function (e) {
+ var flow = this.getSelected();
+ if(e.ctrlKey){
+ return;
+ }
switch (e.keyCode) {
case Key.K:
case Key.UP:
@@ -3724,11 +3747,32 @@ var MainView = React.createClass({displayName: 'MainView',
this.refs.flowDetails.nextTab(+1);
}
break;
+ case Key.C:
+ if (e.shiftKey) {
+ FlowActions.clear();
+ }
+ break;
+ case Key.D:
+ if (flow) {
+ if (e.shiftKey) {
+ FlowActions.duplicate(flow);
+ } else {
+ var last_flow = this.state.view.index(flow) === this.state.view.list.length - 1;
+ this.selectFlowRelative(last_flow ? -1 : +1);
+ FlowActions.delete(flow);
+ }
+ }
+ break;
case Key.A:
if (e.shiftKey) {
FlowActions.accept_all();
- } else if(this.getSelected()) {
- FlowActions.accept(this.getSelected());
+ } else if (flow) {
+ FlowActions.accept(flow);
+ }
+ break;
+ case Key.R:
+ if(!e.shiftKey && flow){
+ FlowActions.replay(flow);
}
break;
default: