web: CSP, revert functionality, serve content

This commit is contained in:
Maximilian Hils
2014-12-25 16:10:47 +01:00
parent 7d793ae162
commit 1f454b577f
10 changed files with 143 additions and 147 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))]

View File

@@ -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):

View File

@@ -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)

View File

@@ -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',
var NavAction = React.createClass({displayName: 'NavAction',
onClick: function (e) {
e.preventDefault();
FlowActions.delete(this.props.flow);
this.props.onClick();
},
render: function () {
return (
React.createElement("a", {title: "[d]elete Flow",
React.createElement("a", {title: this.props.title,
href: "#",
className: "nav-action",
onClick: this.onClick},
React.createElement("i", {className: "fa fa-fw fa-trash"})
)
);
}
});
var DuplicateButton = React.createClass({displayName: 'DuplicateButton',
onClick: function (e) {
e.preventDefault();
FlowActions.duplicate(this.props.flow);
},
render: function () {
return (
React.createElement("a", {title: "[D]uplicate Flow",
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;

View File

@@ -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,

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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;