aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--libmproxy/console/flowlist.py1
-rw-r--r--libmproxy/console/flowview.py1
-rw-r--r--libmproxy/flow.py2
-rw-r--r--libmproxy/protocol/primitives.py5
-rw-r--r--libmproxy/web/app.py107
-rw-r--r--libmproxy/web/static/js/app.js87
-rw-r--r--web/src/js/actions.js3
-rw-r--r--web/src/js/components/flowdetail.jsx.js75
-rw-r--r--web/src/js/components/header.jsx.js2
-rw-r--r--web/src/js/components/mainview.jsx.js7
10 files changed, 143 insertions, 147 deletions
diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py
index be25be83..2c6bfe32 100644
--- a/libmproxy/console/flowlist.py
+++ b/libmproxy/console/flowlist.py
@@ -150,7 +150,6 @@ class ConnectionItem(common.WWrap):
f = self.master.duplicate_flow(self.flow)
self.master.view_flow(f)
elif key == "r":
- self.flow.backup()
r = self.master.replay_request(self.flow)
if r:
self.master.statusbar.message(r)
diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py
index f00e0ff2..9e305b8a 100644
--- a/libmproxy/console/flowview.py
+++ b/libmproxy/console/flowview.py
@@ -763,7 +763,6 @@ class FlowView(common.WWrap):
elif key == "p":
self.view_prev_flow(self.flow)
elif key == "r":
- self.flow.backup()
r = self.master.replay_request(self.flow)
if r:
self.master.statusbar.message(r)
diff --git a/libmproxy/flow.py b/libmproxy/flow.py
index bfd99c9c..58b4604c 100644
--- a/libmproxy/flow.py
+++ b/libmproxy/flow.py
@@ -584,6 +584,7 @@ class State(object):
def revert(self, f):
f.revert()
+ self.update_flow(f)
def killall(self, master):
self.flows.kill_all(master)
@@ -821,6 +822,7 @@ class FlowMaster(controller.Master):
if f.request.content == http.CONTENT_MISSING:
return "Can't replay request with missing content..."
if f.request:
+ f.backup()
f.request.is_replay = True
if f.request.content:
f.request.headers["Content-Length"] = [str(len(f.request.content))]
diff --git a/libmproxy/protocol/primitives.py b/libmproxy/protocol/primitives.py
index 11ebb97f..f9c22e1a 100644
--- a/libmproxy/protocol/primitives.py
+++ b/libmproxy/protocol/primitives.py
@@ -88,6 +88,11 @@ class Flow(stateobject.StateObject):
def get_state(self, short=False):
d = super(Flow, self).get_state(short)
d.update(version=version.IVERSION)
+ if self._backup and self._backup != d:
+ if short:
+ d.update(modified=True)
+ else:
+ d.update(backup=self._backup)
return d
def __eq__(self, other):
diff --git a/libmproxy/web/app.py b/libmproxy/web/app.py
index b5ec41c9..27e9aefc 100644
--- a/libmproxy/web/app.py
+++ b/libmproxy/web/app.py
@@ -1,4 +1,5 @@
import os.path
+import re
import tornado.web
import tornado.websocket
import logging
@@ -9,7 +10,43 @@ from .. import version
class APIError(tornado.web.HTTPError):
pass
-class IndexHandler(tornado.web.RequestHandler):
+
+class RequestHandler(tornado.web.RequestHandler):
+ def set_default_headers(self):
+ super(RequestHandler, self).set_default_headers()
+ self.set_header("Server", version.NAMEVERSION)
+ self.set_header("X-Frame-Options", "DENY")
+ self.add_header("X-XSS-Protection", "1; mode=block")
+ self.add_header("X-Content-Type-Options", "nosniff")
+ self.add_header("Content-Security-Policy", "default-src 'self'; "
+ "connect-src 'self' ws://* ; "
+ "style-src 'self' 'unsafe-inline'")
+
+ @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 IndexHandler(RequestHandler):
def get(self):
_ = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
self.render("index.html")
@@ -38,31 +75,6 @@ class ClientConnection(WebSocketEventBroadcaster):
connections = set()
-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(
@@ -95,13 +107,49 @@ class DuplicateFlow(RequestHandler):
def post(self, flow_id):
self.master.duplicate_flow(self.flow)
+
+class RevertFlow(RequestHandler):
+ def post(self, flow_id):
+ self.state.revert(self.flow)
+
+
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 FlowContent(RequestHandler):
+ def get(self, flow_id, message):
+ message = getattr(self.flow, message)
+
+ if not message.content:
+ raise APIError(400, "No content.")
+
+ content_encoding = message.headers.get_first("Content-Encoding", None)
+ if content_encoding:
+ content_encoding = re.sub(r"[^\w]", "", content_encoding)
+ self.set_header("Content-Encoding", content_encoding)
+
+ original_cd = message.headers.get_first("Content-Disposition", None)
+ filename = None
+ if original_cd:
+ filename = re.search("filename=([\w\" \.\-\(\)]+)", original_cd)
+ if filename:
+ filename = filename.group(1)
+ if not filename:
+ filename = self.flow.request.path.split("?")[0].split("/")[-1]
+
+ filename = re.sub(r"[^\w\" \.\-\(\)]", "", filename)
+ cd = "attachment; filename={}".format(filename)
+ self.set_header("Content-Disposition", cd)
+ self.set_header("Content-Type", "application/text")
+ self.set_header("X-Content-Type-Options", "nosniff")
+ self.set_header("X-Frame-Options", "DENY")
+ self.write(message.content)
+
+
class Events(RequestHandler):
def get(self):
self.write(dict(
@@ -154,6 +202,8 @@ class Application(tornado.web.Application):
(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"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),
+ (r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content", FlowContent),
(r"/settings", Settings),
(r"/clear", ClearAll),
]
@@ -164,5 +214,4 @@ class Application(tornado.web.Application):
cookie_secret=os.urandom(256),
debug=debug,
)
- tornado.web.Application.__init__(self, handlers, **settings)
-
+ super(Application, self).__init__(handlers, **settings) \ No newline at end of file
diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js
index 92f48d14..984db943 100644
--- a/libmproxy/web/static/js/app.js
+++ b/libmproxy/web/static/js/app.js
@@ -231,6 +231,9 @@ var FlowActions = {
replay: function(flow){
jQuery.post("/flows/" + flow.id + "/replay");
},
+ revert: function(flow){
+ jQuery.post("/flows/" + flow.id + "/revert");
+ },
update: function (flow) {
AppDispatcher.dispatchViewAction({
type: ActionTypes.FLOW_STORE,
@@ -2891,7 +2894,7 @@ var FileMenu = React.createClass({displayName: 'FileMenu',
React.createElement("li", {role: "presentation", className: "divider"}),
React.createElement("li", null,
React.createElement("a", {href: "http://mitm.it/", target: "_blank"},
- React.createElement("i", {className: "fa fa-fw fa-lock"}),
+ React.createElement("i", {className: "fa fa-fw fa-external-link"}),
"Install Certificates..."
)
)
@@ -3258,70 +3261,23 @@ var FlowTable = React.createClass({displayName: 'FlowTable',
}
});
-var DeleteButton = React.createClass({displayName: 'DeleteButton',
- onClick: function (e) {
- e.preventDefault();
- FlowActions.delete(this.props.flow);
- },
- render: function () {
- return (
- React.createElement("a", {title: "[d]elete Flow",
- href: "#",
- className: "nav-action",
- onClick: this.onClick},
- React.createElement("i", {className: "fa fa-fw fa-trash"})
- )
- );
- }
-});
-var DuplicateButton = React.createClass({displayName: 'DuplicateButton',
+var NavAction = React.createClass({displayName: 'NavAction',
onClick: function (e) {
e.preventDefault();
- FlowActions.duplicate(this.props.flow);
+ this.props.onClick();
},
render: function () {
return (
- React.createElement("a", {title: "[D]uplicate Flow",
+ React.createElement("a", {title: this.props.title,
href: "#",
className: "nav-action",
onClick: this.onClick},
- React.createElement("i", {className: "fa fa-fw fa-edit"})
- )
- );
- }
-});
-var ReplayButton = React.createClass({displayName: 'ReplayButton',
- onClick: function (e) {
- e.preventDefault();
- FlowActions.replay(this.props.flow);
- },
- render: function () {
- return (
- React.createElement("a", {title: "[r]eplay Flow",
- href: "#",
- className: "nav-action",
- onClick: this.onClick},
- React.createElement("i", {className: "fa fa-fw fa-close"})
- )
- );
- }
-});
-var AcceptButton = React.createClass({displayName: 'AcceptButton',
- onClick: function (e) {
- e.preventDefault();
- FlowActions.accept(this.props.flow);
- },
- render: function () {
- return (
- React.createElement("a", {title: "[a]ccept (resume) Flow",
- href: "#",
- className: "nav-action",
- onClick: this.onClick},
- React.createElement("i", {className: "fa fa-fw fa-play"})
+ React.createElement("i", {className: "fa fa-fw " + this.props.icon})
)
);
}
});
+
var FlowDetailNav = React.createClass({displayName: 'FlowDetailNav',
render: function () {
var flow = this.props.flow;
@@ -3339,13 +3295,23 @@ var FlowDetailNav = React.createClass({displayName: 'FlowDetailNav',
onClick: onClick}, str);
}.bind(this));
+ var acceptButton = null;
+ if(flow.intercepted){
+ acceptButton = React.createElement(NavAction, {title: "[a]ccept intercepted flow", icon: "fa-play", onClick: FlowActions.accept.bind(null, flow)})
+ }
+ var revertButton = null;
+ if(flow.modified){
+ revertButton = React.createElement(NavAction, {title: "revert changes to flow [V]", icon: "fa-history", onClick: FlowActions.revert.bind(null, flow)})
+ }
+
return (
React.createElement("nav", {ref: "head", className: "nav-tabs nav-tabs-sm"},
tabs,
- React.createElement(DeleteButton, {flow: flow}),
- React.createElement(DuplicateButton, {flow: flow}),
- React.createElement(ReplayButton, {flow: flow}),
- flow.intercepted ? React.createElement(AcceptButton, {flow: this.props.flow}) : null
+ React.createElement(NavAction, {title: "[d]elete flow", icon: "fa-trash", onClick: FlowActions.delete.bind(null, flow)}),
+ React.createElement(NavAction, {title: "[D]uplicate flow", icon: "fa-copy", onClick: FlowActions.duplicate.bind(null, flow)}),
+ React.createElement(NavAction, {disabled: true, title: "[r]eplay flow", icon: "fa-repeat", onClick: FlowActions.replay.bind(null, flow)}),
+ acceptButton,
+ revertButton
)
);
}
@@ -3855,7 +3821,7 @@ var MainView = React.createClass({displayName: 'MainView',
case Key.A:
if (e.shiftKey) {
FlowActions.accept_all();
- } else if (flow) {
+ } else if (flow && flow.intercepted) {
FlowActions.accept(flow);
}
break;
@@ -3864,6 +3830,11 @@ var MainView = React.createClass({displayName: 'MainView',
FlowActions.replay(flow);
}
break;
+ case Key.V:
+ if(e.shiftKey && flow && flow.modified) {
+ FlowActions.revert(flow);
+ }
+ break;
default:
console.debug("keydown", e.keyCode);
return;
diff --git a/web/src/js/actions.js b/web/src/js/actions.js
index 83dcb801..7f4fd0b0 100644
--- a/web/src/js/actions.js
+++ b/web/src/js/actions.js
@@ -89,6 +89,9 @@ var FlowActions = {
replay: function(flow){
jQuery.post("/flows/" + flow.id + "/replay");
},
+ revert: function(flow){
+ jQuery.post("/flows/" + flow.id + "/revert");
+ },
update: function (flow) {
AppDispatcher.dispatchViewAction({
type: ActionTypes.FLOW_STORE,
diff --git a/web/src/js/components/flowdetail.jsx.js b/web/src/js/components/flowdetail.jsx.js
index dfc0099e..594d1a0e 100644
--- a/web/src/js/components/flowdetail.jsx.js
+++ b/web/src/js/components/flowdetail.jsx.js
@@ -1,67 +1,20 @@
-var DeleteButton = React.createClass({
+var NavAction = React.createClass({
onClick: function (e) {
e.preventDefault();
- FlowActions.delete(this.props.flow);
+ this.props.onClick();
},
render: function () {
return (
- <a title="[d]elete Flow"
+ <a title={this.props.title}
href="#"
className="nav-action"
onClick={this.onClick}>
- <i className="fa fa-fw fa-trash"></i>
- </a>
- );
- }
-});
-var DuplicateButton = React.createClass({
- onClick: function (e) {
- e.preventDefault();
- FlowActions.duplicate(this.props.flow);
- },
- render: function () {
- return (
- <a title="[D]uplicate Flow"
- href="#"
- className="nav-action"
- onClick={this.onClick}>
- <i className="fa fa-fw fa-edit"></i>
- </a>
- );
- }
-});
-var ReplayButton = React.createClass({
- onClick: function (e) {
- e.preventDefault();
- FlowActions.replay(this.props.flow);
- },
- render: function () {
- return (
- <a title="[r]eplay Flow"
- href="#"
- className="nav-action"
- onClick={this.onClick}>
- <i className="fa fa-fw fa-close"></i>
- </a>
- );
- }
-});
-var AcceptButton = React.createClass({
- onClick: function (e) {
- e.preventDefault();
- FlowActions.accept(this.props.flow);
- },
- render: function () {
- return (
- <a title="[a]ccept (resume) Flow"
- href="#"
- className="nav-action"
- onClick={this.onClick}>
- <i className="fa fa-fw fa-play"></i>
+ <i className={"fa fa-fw " + this.props.icon}></i>
</a>
);
}
});
+
var FlowDetailNav = React.createClass({
render: function () {
var flow = this.props.flow;
@@ -79,13 +32,23 @@ var FlowDetailNav = React.createClass({
onClick={onClick}>{str}</a>;
}.bind(this));
+ var acceptButton = null;
+ if(flow.intercepted){
+ acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={FlowActions.accept.bind(null, flow)} />
+ }
+ var revertButton = null;
+ if(flow.modified){
+ revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={FlowActions.revert.bind(null, flow)} />
+ }
+
return (
<nav ref="head" className="nav-tabs nav-tabs-sm">
{tabs}
- <DeleteButton flow={flow}/>
- <DuplicateButton flow={flow}/>
- <ReplayButton flow={flow}/>
- { flow.intercepted ? <AcceptButton flow={this.props.flow}/> : null }
+ <NavAction title="[d]elete flow" icon="fa-trash" onClick={FlowActions.delete.bind(null, flow)} />
+ <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={FlowActions.duplicate.bind(null, flow)} />
+ <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={FlowActions.replay.bind(null, flow)} />
+ {acceptButton}
+ {revertButton}
</nav>
);
}
diff --git a/web/src/js/components/header.jsx.js b/web/src/js/components/header.jsx.js
index e1016950..ba63f12e 100644
--- a/web/src/js/components/header.jsx.js
+++ b/web/src/js/components/header.jsx.js
@@ -260,7 +260,7 @@ var FileMenu = React.createClass({
<li role="presentation" className="divider"></li>
<li>
<a href="http://mitm.it/" target="_blank">
- <i className="fa fa-fw fa-lock"></i>
+ <i className="fa fa-fw fa-external-link"></i>
Install Certificates...
</a>
</li>
diff --git a/web/src/js/components/mainview.jsx.js b/web/src/js/components/mainview.jsx.js
index 41f22a95..af65ca1e 100644
--- a/web/src/js/components/mainview.jsx.js
+++ b/web/src/js/components/mainview.jsx.js
@@ -171,7 +171,7 @@ var MainView = React.createClass({
case Key.A:
if (e.shiftKey) {
FlowActions.accept_all();
- } else if (flow) {
+ } else if (flow && flow.intercepted) {
FlowActions.accept(flow);
}
break;
@@ -180,6 +180,11 @@ var MainView = React.createClass({
FlowActions.replay(flow);
}
break;
+ case Key.V:
+ if(e.shiftKey && flow && flow.modified) {
+ FlowActions.revert(flow);
+ }
+ break;
default:
console.debug("keydown", e.keyCode);
return;