diff options
-rw-r--r-- | libmproxy/console/flowlist.py | 1 | ||||
-rw-r--r-- | libmproxy/console/flowview.py | 1 | ||||
-rw-r--r-- | libmproxy/flow.py | 2 | ||||
-rw-r--r-- | libmproxy/protocol/primitives.py | 5 | ||||
-rw-r--r-- | libmproxy/web/app.py | 107 | ||||
-rw-r--r-- | libmproxy/web/static/js/app.js | 87 | ||||
-rw-r--r-- | web/src/js/actions.js | 3 | ||||
-rw-r--r-- | web/src/js/components/flowdetail.jsx.js | 75 | ||||
-rw-r--r-- | web/src/js/components/header.jsx.js | 2 | ||||
-rw-r--r-- | web/src/js/components/mainview.jsx.js | 7 |
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; |