mirror of
https://github.com/mitmproxy/mitmproxy.git
synced 2025-02-21 14:22:18 +00:00
web: add basic edit capability for first line
This commit is contained in:
parent
2acd77dea0
commit
968c7021df
@ -25,6 +25,12 @@ class RequestHandler(tornado.web.RequestHandler):
|
|||||||
"style-src 'self' 'unsafe-inline'"
|
"style-src 'self' 'unsafe-inline'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def json(self):
|
||||||
|
if not self.request.headers.get("Content-Type").startswith("application/json"):
|
||||||
|
return None
|
||||||
|
return json.loads(self.request.body)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
return self.application.master.state
|
return self.application.master.state
|
||||||
@ -111,6 +117,35 @@ class FlowHandler(RequestHandler):
|
|||||||
self.flow.kill(self.master)
|
self.flow.kill(self.master)
|
||||||
self.state.delete_flow(self.flow)
|
self.state.delete_flow(self.flow)
|
||||||
|
|
||||||
|
def put(self, flow_id):
|
||||||
|
flow = self.flow
|
||||||
|
for a, b in self.json.iteritems():
|
||||||
|
|
||||||
|
if a == "request":
|
||||||
|
request = flow.request
|
||||||
|
for k, v in b.iteritems():
|
||||||
|
if k in ["method", "scheme", "host", "path"]:
|
||||||
|
setattr(request, k, str(v))
|
||||||
|
elif k == "port":
|
||||||
|
request.port = int(v)
|
||||||
|
elif k == "httpversion":
|
||||||
|
request.httpversion = tuple(int(x) for x in v)
|
||||||
|
else:
|
||||||
|
print "Warning: Unknown update {}.{}: {}".format(a, k, v)
|
||||||
|
|
||||||
|
elif a == "response":
|
||||||
|
response = flow.response
|
||||||
|
for k, v in b.iteritems():
|
||||||
|
if k == "msg":
|
||||||
|
response.msg = str(v)
|
||||||
|
elif k == "code":
|
||||||
|
response.code = int(v)
|
||||||
|
elif k == "httpversion":
|
||||||
|
response.httpversion = tuple(int(x) for x in v)
|
||||||
|
else:
|
||||||
|
print "Warning: Unknown update {}: {}".format(a, b)
|
||||||
|
self.state.update_flow(flow)
|
||||||
|
|
||||||
|
|
||||||
class DuplicateFlow(RequestHandler):
|
class DuplicateFlow(RequestHandler):
|
||||||
def post(self, flow_id):
|
def post(self, flow_id):
|
||||||
@ -176,18 +211,12 @@ class Settings(RequestHandler):
|
|||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
||||||
def put(self, *update, **kwargs):
|
def put(self):
|
||||||
update = {}
|
update = {}
|
||||||
for k, v in self.request.arguments.iteritems():
|
for k, v in self.json.iteritems():
|
||||||
if len(v) != 1:
|
if k == "intercept":
|
||||||
print "Warning: Unknown length for setting {}: {}".format(k, v)
|
self.state.set_intercept(v)
|
||||||
continue
|
update[k] = v
|
||||||
|
|
||||||
if k == "_xsrf":
|
|
||||||
continue
|
|
||||||
elif k == "intercept":
|
|
||||||
self.state.set_intercept(v[0])
|
|
||||||
update[k] = v[0]
|
|
||||||
else:
|
else:
|
||||||
print "Warning: Unknown setting {}: {}".format(k, v)
|
print "Warning: Unknown setting {}: {}".format(k, v)
|
||||||
|
|
||||||
|
@ -273,6 +273,10 @@ header .menu {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
/*.request .response-line,
|
||||||
|
.response .request-line {
|
||||||
|
opacity: 0.7;
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
.flow-detail nav {
|
.flow-detail nav {
|
||||||
background-color: #F2F2F2;
|
background-color: #F2F2F2;
|
||||||
@ -291,9 +295,22 @@ header .menu {
|
|||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
.flow-detail .request-line {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
.flow-detail hr {
|
.flow-detail hr {
|
||||||
margin: 0 0 5px;
|
margin: 0 0 5px;
|
||||||
}
|
}
|
||||||
|
.inline-input {
|
||||||
|
margin: 0 -5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.inline-input[contenteditable] {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.inline-input[contenteditable].has-warning {
|
||||||
|
color: #ffb8b8;
|
||||||
|
}
|
||||||
.view-options {
|
.view-options {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
@ -303,6 +303,7 @@ function isUndefined(arg) {
|
|||||||
|
|
||||||
},{}],2:[function(require,module,exports){
|
},{}],2:[function(require,module,exports){
|
||||||
var $ = require("jquery");
|
var $ = require("jquery");
|
||||||
|
var _ = require("lodash");
|
||||||
var AppDispatcher = require("./dispatcher.js").AppDispatcher;
|
var AppDispatcher = require("./dispatcher.js").AppDispatcher;
|
||||||
|
|
||||||
var ActionTypes = {
|
var ActionTypes = {
|
||||||
@ -348,7 +349,8 @@ var SettingsActions = {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
url: "/settings",
|
url: "/settings",
|
||||||
data: settings
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(settings)
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -399,15 +401,26 @@ var FlowActions = {
|
|||||||
revert: function(flow){
|
revert: function(flow){
|
||||||
$.post("/flows/" + flow.id + "/revert");
|
$.post("/flows/" + flow.id + "/revert");
|
||||||
},
|
},
|
||||||
update: function (flow) {
|
update: function (flow, nextProps) {
|
||||||
|
/*
|
||||||
|
//Facebook Flux: We do an optimistic update on the client already.
|
||||||
|
var nextFlow = _.cloneDeep(flow);
|
||||||
|
_.merge(nextFlow, nextProps);
|
||||||
AppDispatcher.dispatchViewAction({
|
AppDispatcher.dispatchViewAction({
|
||||||
type: ActionTypes.FLOW_STORE,
|
type: ActionTypes.FLOW_STORE,
|
||||||
cmd: StoreCmds.UPDATE,
|
cmd: StoreCmds.UPDATE,
|
||||||
data: flow
|
data: nextFlow
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
$.ajax({
|
||||||
|
type: "PUT",
|
||||||
|
url: "/flows/" + flow.id,
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(nextProps)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
clear: function(){
|
clear: function(){
|
||||||
$.post("/clear");
|
$.post("/flows/" + flow.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -427,7 +440,7 @@ module.exports = {
|
|||||||
Query: Query
|
Query: Query
|
||||||
};
|
};
|
||||||
|
|
||||||
},{"./dispatcher.js":19,"jquery":"jquery"}],3:[function(require,module,exports){
|
},{"./dispatcher.js":19,"jquery":"jquery","lodash":"lodash"}],3:[function(require,module,exports){
|
||||||
|
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
var ReactRouter = require("react-router");
|
var ReactRouter = require("react-router");
|
||||||
@ -478,6 +491,13 @@ var StickyHeadMixin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var ChildFocus = {
|
||||||
|
contextTypes: {
|
||||||
|
returnFocus: React.PropTypes.func
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
var Navigation = _.extend({}, ReactRouter.Navigation, {
|
var Navigation = _.extend({}, ReactRouter.Navigation, {
|
||||||
setQuery: function (dict) {
|
setQuery: function (dict) {
|
||||||
var q = this.context.router.getCurrentQuery();
|
var q = this.context.router.getCurrentQuery();
|
||||||
@ -624,6 +644,7 @@ var Splitter = React.createClass({displayName: "Splitter",
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
ChildFocus: ChildFocus,
|
||||||
State: State,
|
State: State,
|
||||||
Navigation: Navigation,
|
Navigation: Navigation,
|
||||||
StickyHeadMixin: StickyHeadMixin,
|
StickyHeadMixin: StickyHeadMixin,
|
||||||
@ -810,7 +831,7 @@ var TLSColumn = React.createClass({displayName: "TLSColumn",
|
|||||||
},
|
},
|
||||||
render: function () {
|
render: function () {
|
||||||
var flow = this.props.flow;
|
var flow = this.props.flow;
|
||||||
var ssl = (flow.request.scheme == "https");
|
var ssl = (flow.request.scheme === "https");
|
||||||
var classes;
|
var classes;
|
||||||
if (ssl) {
|
if (ssl) {
|
||||||
classes = "col-tls col-tls-https";
|
classes = "col-tls col-tls-https";
|
||||||
@ -838,7 +859,7 @@ var IconColumn = React.createClass({displayName: "IconColumn",
|
|||||||
var contentType = ResponseUtils.getContentType(flow.response);
|
var contentType = ResponseUtils.getContentType(flow.response);
|
||||||
|
|
||||||
//TODO: We should assign a type to the flow somewhere else.
|
//TODO: We should assign a type to the flow somewhere else.
|
||||||
if (flow.response.code == 304) {
|
if (flow.response.code === 304) {
|
||||||
icon = "resource-icon-not-modified";
|
icon = "resource-icon-not-modified";
|
||||||
} else if (300 <= flow.response.code && flow.response.code < 400) {
|
} else if (300 <= flow.response.code && flow.response.code < 400) {
|
||||||
icon = "resource-icon-redirect";
|
icon = "resource-icon-redirect";
|
||||||
@ -1224,7 +1245,7 @@ var RawMixin = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestContent: function (nextProps) {
|
requestContent: function (nextProps) {
|
||||||
if(this.state.request){
|
if (this.state.request) {
|
||||||
this.state.request.abort();
|
this.state.request.abort();
|
||||||
}
|
}
|
||||||
var request = MessageUtils.getContent(nextProps.flow, nextProps.message);
|
var request = MessageUtils.getContent(nextProps.flow, nextProps.message);
|
||||||
@ -1235,11 +1256,11 @@ var RawMixin = {
|
|||||||
request.done(function (data) {
|
request.done(function (data) {
|
||||||
this.setState({content: data});
|
this.setState({content: data});
|
||||||
}.bind(this)).fail(function (jqXHR, textStatus, errorThrown) {
|
}.bind(this)).fail(function (jqXHR, textStatus, errorThrown) {
|
||||||
if(textStatus === "abort"){
|
if (textStatus === "abort") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown});
|
this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown});
|
||||||
}.bind(this)).always(function(){
|
}.bind(this)).always(function () {
|
||||||
this.setState({request: undefined});
|
this.setState({request: undefined});
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
@ -1252,8 +1273,8 @@ var RawMixin = {
|
|||||||
this.requestContent(nextProps);
|
this.requestContent(nextProps);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
componentWillUnmount: function(){
|
componentWillUnmount: function () {
|
||||||
if(this.state.request){
|
if (this.state.request) {
|
||||||
this.state.request.abort();
|
this.state.request.abort();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1291,7 +1312,7 @@ var ViewJSON = React.createClass({displayName: "ViewJSON",
|
|||||||
var json = this.state.content;
|
var json = this.state.content;
|
||||||
try {
|
try {
|
||||||
json = JSON.stringify(JSON.parse(json), null, 2);
|
json = JSON.stringify(JSON.parse(json), null, 2);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
return React.createElement("pre", null, json);
|
return React.createElement("pre", null, json);
|
||||||
}
|
}
|
||||||
@ -1336,7 +1357,7 @@ var ContentMissing = React.createClass({displayName: "ContentMissing",
|
|||||||
|
|
||||||
var TooLarge = React.createClass({displayName: "TooLarge",
|
var TooLarge = React.createClass({displayName: "TooLarge",
|
||||||
statics: {
|
statics: {
|
||||||
isTooLarge: function(message){
|
isTooLarge: function (message) {
|
||||||
var max_mb = ViewImage.matches(message) ? 10 : 0.2;
|
var max_mb = ViewImage.matches(message) ? 10 : 0.2;
|
||||||
return message.contentLength > 1024 * 1024 * max_mb;
|
return message.contentLength > 1024 * 1024 * max_mb;
|
||||||
}
|
}
|
||||||
@ -1422,8 +1443,10 @@ var ContentView = React.createClass({displayName: "ContentView",
|
|||||||
React.createElement(this.state.View, React.__spread({}, this.props)),
|
React.createElement(this.state.View, React.__spread({}, this.props)),
|
||||||
React.createElement("div", {className: "view-options text-center"},
|
React.createElement("div", {className: "view-options text-center"},
|
||||||
React.createElement(ViewSelector, {selectView: this.selectView, active: this.state.View, message: message}),
|
React.createElement(ViewSelector, {selectView: this.selectView, active: this.state.View, message: message}),
|
||||||
" ",
|
" ",
|
||||||
React.createElement("a", {className: "btn btn-default btn-xs", href: downloadUrl}, React.createElement("i", {className: "fa fa-download"}))
|
React.createElement("a", {className: "btn btn-default btn-xs", href: downloadUrl},
|
||||||
|
React.createElement("i", {className: "fa fa-download"})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1692,7 +1715,10 @@ module.exports = FlowView;
|
|||||||
|
|
||||||
},{"../common.js":4,"./details.js":9,"./messages.js":11,"./nav.js":12,"lodash":"lodash","react":"react"}],11:[function(require,module,exports){
|
},{"../common.js":4,"./details.js":9,"./messages.js":11,"./nav.js":12,"lodash":"lodash","react":"react"}],11:[function(require,module,exports){
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
|
var _ = require("lodash");
|
||||||
|
|
||||||
|
var common = require("../common.js");
|
||||||
|
var actions = require("../../actions.js");
|
||||||
var flowutils = require("../../flow/utils.js");
|
var flowutils = require("../../flow/utils.js");
|
||||||
var utils = require("../../utils.js");
|
var utils = require("../../utils.js");
|
||||||
var ContentView = require("./contentview.js");
|
var ContentView = require("./contentview.js");
|
||||||
@ -1717,20 +1743,217 @@ var Headers = React.createClass({displayName: "Headers",
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var InlineInput = React.createClass({displayName: "InlineInput",
|
||||||
|
mixins: [common.ChildFocus],
|
||||||
|
getInitialState: function () {
|
||||||
|
return {
|
||||||
|
editable: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render: function () {
|
||||||
|
var Tag = this.props.tag || "span";
|
||||||
|
var className = "inline-input " + (this.props.className || "");
|
||||||
|
var html = {__html: _.escape(this.props.content)};
|
||||||
|
return React.createElement(Tag, React.__spread({},
|
||||||
|
this.props,
|
||||||
|
{tabIndex: "0",
|
||||||
|
className: className,
|
||||||
|
contentEditable: this.state.editable || undefined,
|
||||||
|
onInput: this.onInput,
|
||||||
|
onFocus: this.onFocus,
|
||||||
|
onBlur: this.onBlur,
|
||||||
|
onKeyDown: this.onKeyDown,
|
||||||
|
dangerouslySetInnerHTML: html})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onKeyDown: function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case utils.Key.ESC:
|
||||||
|
this.blur();
|
||||||
|
break;
|
||||||
|
case utils.Key.ENTER:
|
||||||
|
e.preventDefault();
|
||||||
|
if (!e.ctrlKey) {
|
||||||
|
this.blur();
|
||||||
|
} else {
|
||||||
|
this.props.onDone && this.props.onDone();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blur: function(){
|
||||||
|
this.getDOMNode().blur();
|
||||||
|
this.context.returnFocus && this.context.returnFocus();
|
||||||
|
},
|
||||||
|
selectContents: function () {
|
||||||
|
var range = document.createRange();
|
||||||
|
range.selectNodeContents(this.getDOMNode());
|
||||||
|
var sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
},
|
||||||
|
onFocus: function () {
|
||||||
|
this.setState({editable: true}, this.selectContents);
|
||||||
|
},
|
||||||
|
onBlur: function (e) {
|
||||||
|
this.setState({editable: false});
|
||||||
|
this.handleChange();
|
||||||
|
this.props.onDone && this.props.onDone();
|
||||||
|
},
|
||||||
|
onInput: function () {
|
||||||
|
this.handleChange();
|
||||||
|
},
|
||||||
|
handleChange: function () {
|
||||||
|
var content = this.getDOMNode().textContent;
|
||||||
|
if (content !== this.props.content) {
|
||||||
|
this.props.onChange(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var ValidateInlineInput = React.createClass({displayName: "ValidateInlineInput",
|
||||||
|
getInitialState: function () {
|
||||||
|
return {
|
||||||
|
content: ""+this.props.content,
|
||||||
|
originalContent: ""+this.props.content
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onChange: function (val) {
|
||||||
|
this.setState({
|
||||||
|
content: val
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDone: function () {
|
||||||
|
if (this.state.content === this.state.originalContent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.props.isValid(this.state.content)) {
|
||||||
|
this.props.onChange(this.state.content);
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
content: this.state.originalContent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
componentWillReceiveProps: function (nextProps) {
|
||||||
|
if (nextProps.content !== this.state.content) {
|
||||||
|
this.setState({
|
||||||
|
content: ""+nextProps.content,
|
||||||
|
originalContent: ""+nextProps.content
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: function () {
|
||||||
|
var className = this.props.className || "";
|
||||||
|
if (this.props.isValid(this.state.content)) {
|
||||||
|
className += " has-success";
|
||||||
|
} else {
|
||||||
|
className += " has-warning"
|
||||||
|
}
|
||||||
|
return React.createElement(InlineInput, React.__spread({}, this.props,
|
||||||
|
{className: className,
|
||||||
|
content: this.state.content,
|
||||||
|
onChange: this.onChange,
|
||||||
|
onDone: this.onDone})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var RequestLine = React.createClass({displayName: "RequestLine",
|
||||||
|
render: function () {
|
||||||
|
var flow = this.props.flow;
|
||||||
|
var url = flowutils.RequestUtils.pretty_url(flow.request);
|
||||||
|
var httpver = "HTTP/" + flow.request.httpversion.join(".");
|
||||||
|
|
||||||
|
return React.createElement("div", {className: "first-line request-line"},
|
||||||
|
React.createElement(ValidateInlineInput, {content: flow.request.method, onChange: this.onMethodChange, isValid: this.isValidMethod}),
|
||||||
|
" ",
|
||||||
|
React.createElement(ValidateInlineInput, {content: url, onChange: this.onUrlChange, isValid: this.isValidUrl}),
|
||||||
|
" ",
|
||||||
|
React.createElement(ValidateInlineInput, {content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isValidMethod: function (method) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
isValidUrl: function (url) {
|
||||||
|
var u = flowutils.parseUrl(url);
|
||||||
|
return !!u.host;
|
||||||
|
},
|
||||||
|
onMethodChange: function (nextMethod) {
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{request: {method: nextMethod}}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onUrlChange: function (nextUrl) {
|
||||||
|
var props = flowutils.parseUrl(nextUrl);
|
||||||
|
props.path = props.path || "";
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{request: props}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onHttpVersionChange: function (nextVer) {
|
||||||
|
var ver = flowutils.parseHttpVersion(nextVer);
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{request: {httpversion: ver}}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var ResponseLine = React.createClass({displayName: "ResponseLine",
|
||||||
|
render: function () {
|
||||||
|
var flow = this.props.flow;
|
||||||
|
var httpver = "HTTP/" + flow.response.httpversion.join(".");
|
||||||
|
return React.createElement("div", {className: "first-line response-line"},
|
||||||
|
React.createElement(ValidateInlineInput, {content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion}),
|
||||||
|
" ",
|
||||||
|
React.createElement(ValidateInlineInput, {content: flow.response.code, onChange: this.onCodeChange, isValid: this.isValidCode}),
|
||||||
|
" ",
|
||||||
|
React.createElement(ValidateInlineInput, {content: flow.response.msg, onChange: this.onMsgChange, isValid: this.isValidMsg})
|
||||||
|
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isValidCode: function (code) {
|
||||||
|
return /^\d+$/.test(code);
|
||||||
|
},
|
||||||
|
isValidMsg: function () {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onHttpVersionChange: function (nextVer) {
|
||||||
|
var ver = flowutils.parseHttpVersion(nextVer);
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{response: {httpversion: ver}}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onMsgChange: function(nextMsg){
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{response: {msg: nextMsg}}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onCodeChange: function(nextCode){
|
||||||
|
nextCode = parseInt(nextCode);
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{response: {code: nextCode}}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var Request = React.createClass({displayName: "Request",
|
var Request = React.createClass({displayName: "Request",
|
||||||
render: function () {
|
render: function () {
|
||||||
var flow = this.props.flow;
|
var flow = this.props.flow;
|
||||||
var first_line = [
|
|
||||||
flow.request.method,
|
|
||||||
flowutils.RequestUtils.pretty_url(flow.request),
|
|
||||||
"HTTP/" + flow.request.httpversion.join(".")
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
//TODO: Styling
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
React.createElement("section", null,
|
React.createElement("section", {className: "request"},
|
||||||
React.createElement("div", {className: "first-line"}, first_line ),
|
React.createElement(RequestLine, {flow: flow}),
|
||||||
|
/*<ResponseLine flow={flow}/>*/
|
||||||
React.createElement(Headers, {message: flow.request}),
|
React.createElement(Headers, {message: flow.request}),
|
||||||
React.createElement("hr", null),
|
React.createElement("hr", null),
|
||||||
React.createElement(ContentView, {flow: flow, message: flow.request})
|
React.createElement(ContentView, {flow: flow, message: flow.request})
|
||||||
@ -1742,17 +1965,10 @@ var Request = React.createClass({displayName: "Request",
|
|||||||
var Response = React.createClass({displayName: "Response",
|
var Response = React.createClass({displayName: "Response",
|
||||||
render: function () {
|
render: function () {
|
||||||
var flow = this.props.flow;
|
var flow = this.props.flow;
|
||||||
var first_line = [
|
|
||||||
"HTTP/" + flow.response.httpversion.join("."),
|
|
||||||
flow.response.code,
|
|
||||||
flow.response.msg
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
//TODO: Styling
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
React.createElement("section", null,
|
React.createElement("section", {className: "response"},
|
||||||
React.createElement("div", {className: "first-line"}, first_line ),
|
/*<RequestLine flow={flow}/>*/
|
||||||
|
React.createElement(ResponseLine, {flow: flow}),
|
||||||
React.createElement(Headers, {message: flow.response}),
|
React.createElement(Headers, {message: flow.response}),
|
||||||
React.createElement("hr", null),
|
React.createElement("hr", null),
|
||||||
React.createElement(ContentView, {flow: flow, message: flow.response})
|
React.createElement(ContentView, {flow: flow, message: flow.response})
|
||||||
@ -1783,7 +1999,7 @@ module.exports = {
|
|||||||
Error: Error
|
Error: Error
|
||||||
};
|
};
|
||||||
|
|
||||||
},{"../../flow/utils.js":21,"../../utils.js":24,"./contentview.js":8,"react":"react"}],12:[function(require,module,exports){
|
},{"../../actions.js":2,"../../flow/utils.js":21,"../../utils.js":24,"../common.js":4,"./contentview.js":8,"lodash":"lodash","react":"react"}],12:[function(require,module,exports){
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
|
|
||||||
var actions = require("../../actions.js");
|
var actions = require("../../actions.js");
|
||||||
@ -2273,6 +2489,15 @@ var FlowView = require("./flowview/index.js");
|
|||||||
|
|
||||||
var MainView = React.createClass({displayName: "MainView",
|
var MainView = React.createClass({displayName: "MainView",
|
||||||
mixins: [common.Navigation, common.State],
|
mixins: [common.Navigation, common.State],
|
||||||
|
childContextTypes: {
|
||||||
|
returnFocus: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
getChildContext: function() {
|
||||||
|
return { returnFocus: this.returnFocus };
|
||||||
|
},
|
||||||
|
returnFocus: function(){
|
||||||
|
this.getDOMNode().focus();
|
||||||
|
},
|
||||||
getInitialState: function () {
|
getInitialState: function () {
|
||||||
return {
|
return {
|
||||||
flows: [],
|
flows: [],
|
||||||
@ -4522,9 +4747,17 @@ module.exports = (function() {
|
|||||||
var _ = require("lodash");
|
var _ = require("lodash");
|
||||||
var $ = require("jquery");
|
var $ = require("jquery");
|
||||||
|
|
||||||
|
var defaultPorts = {
|
||||||
|
"http": 80,
|
||||||
|
"https": 443
|
||||||
|
};
|
||||||
|
|
||||||
var MessageUtils = {
|
var MessageUtils = {
|
||||||
getContentType: function (message) {
|
getContentType: function (message) {
|
||||||
return this.get_first_header(message, /^Content-Type$/i).split(";")[0].trim();
|
var ct = this.get_first_header(message, /^Content-Type$/i);
|
||||||
|
if(ct){
|
||||||
|
return ct.split(";")[0].trim();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
get_first_header: function (message, regex) {
|
get_first_header: function (message, regex) {
|
||||||
//FIXME: Cache Invalidation.
|
//FIXME: Cache Invalidation.
|
||||||
@ -4557,25 +4790,20 @@ var MessageUtils = {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
getContentURL: function(flow, message){
|
getContentURL: function (flow, message) {
|
||||||
if(message === flow.request){
|
if (message === flow.request) {
|
||||||
message = "request";
|
message = "request";
|
||||||
} else if (message === flow.response){
|
} else if (message === flow.response) {
|
||||||
message = "response";
|
message = "response";
|
||||||
}
|
}
|
||||||
return "/flows/" + flow.id + "/" + message + "/content";
|
return "/flows/" + flow.id + "/" + message + "/content";
|
||||||
},
|
},
|
||||||
getContent: function(flow, message){
|
getContent: function (flow, message) {
|
||||||
var url = MessageUtils.getContentURL(flow, message);
|
var url = MessageUtils.getContentURL(flow, message);
|
||||||
return $.get(url);
|
return $.get(url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var defaultPorts = {
|
|
||||||
"http": 80,
|
|
||||||
"https": 443
|
|
||||||
};
|
|
||||||
|
|
||||||
var RequestUtils = _.extend(MessageUtils, {
|
var RequestUtils = _.extend(MessageUtils, {
|
||||||
pretty_host: function (request) {
|
pretty_host: function (request) {
|
||||||
//FIXME: Add hostheader
|
//FIXME: Add hostheader
|
||||||
@ -4593,10 +4821,55 @@ var RequestUtils = _.extend(MessageUtils, {
|
|||||||
var ResponseUtils = _.extend(MessageUtils, {});
|
var ResponseUtils = _.extend(MessageUtils, {});
|
||||||
|
|
||||||
|
|
||||||
|
var parseUrl_regex = /^(?:(https?):\/\/)?([^\/:]+)?(?::(\d+))?(\/.*)?$/i;
|
||||||
|
var parseUrl = function (url) {
|
||||||
|
//there are many correct ways to parse a URL,
|
||||||
|
//however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-)
|
||||||
|
var parts = parseUrl_regex.exec(url);
|
||||||
|
|
||||||
|
var scheme = parts[1],
|
||||||
|
host = parts[2],
|
||||||
|
port = parseInt(parts[3]),
|
||||||
|
path = parts[4];
|
||||||
|
if (scheme) {
|
||||||
|
port = port || defaultPorts[scheme];
|
||||||
|
}
|
||||||
|
var ret = {};
|
||||||
|
if (scheme) {
|
||||||
|
ret.scheme = scheme;
|
||||||
|
}
|
||||||
|
if (host) {
|
||||||
|
ret.host = host;
|
||||||
|
}
|
||||||
|
if (port) {
|
||||||
|
ret.port = port;
|
||||||
|
}
|
||||||
|
if (path) {
|
||||||
|
ret.path = path;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var isValidHttpVersion_regex = /^HTTP\/\d+(\.\d+)*$/i;
|
||||||
|
var isValidHttpVersion = function (httpVersion) {
|
||||||
|
return isValidHttpVersion_regex.test(httpVersion);
|
||||||
|
};
|
||||||
|
|
||||||
|
var parseHttpVersion = function (httpVersion) {
|
||||||
|
httpVersion = httpVersion.replace("HTTP/", "").split(".");
|
||||||
|
return _.map(httpVersion, function (x) {
|
||||||
|
return parseInt(x);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ResponseUtils: ResponseUtils,
|
ResponseUtils: ResponseUtils,
|
||||||
RequestUtils: RequestUtils,
|
RequestUtils: RequestUtils,
|
||||||
MessageUtils: MessageUtils
|
MessageUtils: MessageUtils,
|
||||||
|
parseUrl: parseUrl,
|
||||||
|
parseHttpVersion: parseHttpVersion,
|
||||||
|
isValidHttpVersion: isValidHttpVersion
|
||||||
};
|
};
|
||||||
|
|
||||||
},{"jquery":"jquery","lodash":"lodash"}],22:[function(require,module,exports){
|
},{"jquery":"jquery","lodash":"lodash"}],22:[function(require,module,exports){
|
||||||
@ -4903,6 +5176,10 @@ var $ = require("jquery");
|
|||||||
var _ = require("lodash");
|
var _ = require("lodash");
|
||||||
var actions = require("./actions.js");
|
var actions = require("./actions.js");
|
||||||
|
|
||||||
|
//debug
|
||||||
|
window.$ = $;
|
||||||
|
window._ = _;
|
||||||
|
|
||||||
var Key = {
|
var Key = {
|
||||||
UP: 38,
|
UP: 38,
|
||||||
DOWN: 40,
|
DOWN: 40,
|
||||||
@ -4929,17 +5206,17 @@ var formatSize = function (bytes) {
|
|||||||
if (bytes === 0)
|
if (bytes === 0)
|
||||||
return "0";
|
return "0";
|
||||||
var prefix = ["b", "kb", "mb", "gb", "tb"];
|
var prefix = ["b", "kb", "mb", "gb", "tb"];
|
||||||
for (var i = 0; i < prefix.length; i++){
|
for (var i = 0; i < prefix.length; i++) {
|
||||||
if (Math.pow(1024, i + 1) > bytes){
|
if (Math.pow(1024, i + 1) > bytes) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var precision;
|
var precision;
|
||||||
if (bytes%Math.pow(1024, i) === 0)
|
if (bytes % Math.pow(1024, i) === 0)
|
||||||
precision = 0;
|
precision = 0;
|
||||||
else
|
else
|
||||||
precision = 1;
|
precision = 1;
|
||||||
return (bytes/Math.pow(1024, i)).toFixed(precision) + prefix[i];
|
return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -4961,17 +5238,16 @@ var formatTimeStamp = function (seconds) {
|
|||||||
return ts.replace("T", " ").replace("Z", "");
|
return ts.replace("T", " ").replace("Z", "");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// At some places, we need to sort strings alphabetically descending,
|
// At some places, we need to sort strings alphabetically descending,
|
||||||
// but we can only provide a key function.
|
// but we can only provide a key function.
|
||||||
// This beauty "reverses" a JS string.
|
// This beauty "reverses" a JS string.
|
||||||
var end = String.fromCharCode(0xffff);
|
var end = String.fromCharCode(0xffff);
|
||||||
function reverseString(s){
|
function reverseString(s) {
|
||||||
return String.fromCharCode.apply(String,
|
return String.fromCharCode.apply(String,
|
||||||
_.map(s.split(""), function (c) {
|
_.map(s.split(""), function (c) {
|
||||||
return 0xffff - c.charCodeAt(0);
|
return 0xffff - c.charCodeAt(0);
|
||||||
})
|
})
|
||||||
) + end;
|
) + end;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
@ -4983,21 +5259,22 @@ var xsrf = $.param({_xsrf: getCookie("_xsrf")});
|
|||||||
//Tornado XSRF Protection.
|
//Tornado XSRF Protection.
|
||||||
$.ajaxPrefilter(function (options) {
|
$.ajaxPrefilter(function (options) {
|
||||||
if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
|
if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
|
||||||
if (options.data) {
|
if(options.url.indexOf("?") === -1){
|
||||||
options.data += ("&" + xsrf);
|
options.url += "?" + xsrf;
|
||||||
} else {
|
} else {
|
||||||
options.data = xsrf;
|
options.url += "&" + xsrf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Log AJAX Errors
|
// Log AJAX Errors
|
||||||
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
|
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
|
||||||
if(thrownError === "abort"){
|
if (thrownError === "abort") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var message = jqXHR.responseText;
|
var message = jqXHR.responseText;
|
||||||
console.error(thrownError, message, arguments);
|
console.error(thrownError, message, arguments);
|
||||||
actions.EventLogActions.add_event(thrownError + ": " + message);
|
actions.EventLogActions.add_event(thrownError + ": " + message);
|
||||||
|
alert(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -5005,7 +5282,7 @@ module.exports = {
|
|||||||
formatTimeDelta: formatTimeDelta,
|
formatTimeDelta: formatTimeDelta,
|
||||||
formatTimeStamp: formatTimeStamp,
|
formatTimeStamp: formatTimeStamp,
|
||||||
reverseString: reverseString,
|
reverseString: reverseString,
|
||||||
Key: Key
|
Key: Key,
|
||||||
};
|
};
|
||||||
|
|
||||||
},{"./actions.js":2,"jquery":"jquery","lodash":"lodash"}]},{},[3])
|
},{"./actions.js":2,"jquery":"jquery","lodash":"lodash"}]},{},[3])
|
||||||
|
@ -27,6 +27,13 @@
|
|||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
.request-line {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
/*.request .response-line,
|
||||||
|
.response .request-line {
|
||||||
|
opacity: 0.7;
|
||||||
|
}*/
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 0 0 5px;
|
margin: 0 0 5px;
|
||||||
@ -34,6 +41,23 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-input {
|
||||||
|
margin: 0 -5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
|
||||||
|
&[contenteditable] {
|
||||||
|
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
&.has-warning {
|
||||||
|
color: rgb(255, 184, 184);
|
||||||
|
}
|
||||||
|
&.has-success {
|
||||||
|
//color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.view-options {
|
.view-options {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
var $ = require("jquery");
|
var $ = require("jquery");
|
||||||
|
var _ = require("lodash");
|
||||||
var AppDispatcher = require("./dispatcher.js").AppDispatcher;
|
var AppDispatcher = require("./dispatcher.js").AppDispatcher;
|
||||||
|
|
||||||
var ActionTypes = {
|
var ActionTypes = {
|
||||||
@ -44,7 +45,8 @@ var SettingsActions = {
|
|||||||
$.ajax({
|
$.ajax({
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
url: "/settings",
|
url: "/settings",
|
||||||
data: settings
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(settings)
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -95,15 +97,26 @@ var FlowActions = {
|
|||||||
revert: function(flow){
|
revert: function(flow){
|
||||||
$.post("/flows/" + flow.id + "/revert");
|
$.post("/flows/" + flow.id + "/revert");
|
||||||
},
|
},
|
||||||
update: function (flow) {
|
update: function (flow, nextProps) {
|
||||||
|
/*
|
||||||
|
//Facebook Flux: We do an optimistic update on the client already.
|
||||||
|
var nextFlow = _.cloneDeep(flow);
|
||||||
|
_.merge(nextFlow, nextProps);
|
||||||
AppDispatcher.dispatchViewAction({
|
AppDispatcher.dispatchViewAction({
|
||||||
type: ActionTypes.FLOW_STORE,
|
type: ActionTypes.FLOW_STORE,
|
||||||
cmd: StoreCmds.UPDATE,
|
cmd: StoreCmds.UPDATE,
|
||||||
data: flow
|
data: nextFlow
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
$.ajax({
|
||||||
|
type: "PUT",
|
||||||
|
url: "/flows/" + flow.id,
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(nextProps)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
clear: function(){
|
clear: function(){
|
||||||
$.post("/clear");
|
$.post("/flows/" + flow.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,6 +30,13 @@ var StickyHeadMixin = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var ChildFocus = {
|
||||||
|
contextTypes: {
|
||||||
|
returnFocus: React.PropTypes.func
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
var Navigation = _.extend({}, ReactRouter.Navigation, {
|
var Navigation = _.extend({}, ReactRouter.Navigation, {
|
||||||
setQuery: function (dict) {
|
setQuery: function (dict) {
|
||||||
var q = this.context.router.getCurrentQuery();
|
var q = this.context.router.getCurrentQuery();
|
||||||
@ -176,6 +183,7 @@ var Splitter = React.createClass({
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
ChildFocus: ChildFocus,
|
||||||
State: State,
|
State: State,
|
||||||
Navigation: Navigation,
|
Navigation: Navigation,
|
||||||
StickyHeadMixin: StickyHeadMixin,
|
StickyHeadMixin: StickyHeadMixin,
|
||||||
|
@ -16,7 +16,7 @@ var TLSColumn = React.createClass({
|
|||||||
},
|
},
|
||||||
render: function () {
|
render: function () {
|
||||||
var flow = this.props.flow;
|
var flow = this.props.flow;
|
||||||
var ssl = (flow.request.scheme == "https");
|
var ssl = (flow.request.scheme === "https");
|
||||||
var classes;
|
var classes;
|
||||||
if (ssl) {
|
if (ssl) {
|
||||||
classes = "col-tls col-tls-https";
|
classes = "col-tls col-tls-https";
|
||||||
@ -44,7 +44,7 @@ var IconColumn = React.createClass({
|
|||||||
var contentType = ResponseUtils.getContentType(flow.response);
|
var contentType = ResponseUtils.getContentType(flow.response);
|
||||||
|
|
||||||
//TODO: We should assign a type to the flow somewhere else.
|
//TODO: We should assign a type to the flow somewhere else.
|
||||||
if (flow.response.code == 304) {
|
if (flow.response.code === 304) {
|
||||||
icon = "resource-icon-not-modified";
|
icon = "resource-icon-not-modified";
|
||||||
} else if (300 <= flow.response.code && flow.response.code < 400) {
|
} else if (300 <= flow.response.code && flow.response.code < 400) {
|
||||||
icon = "resource-icon-redirect";
|
icon = "resource-icon-redirect";
|
||||||
|
@ -27,7 +27,7 @@ var RawMixin = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestContent: function (nextProps) {
|
requestContent: function (nextProps) {
|
||||||
if(this.state.request){
|
if (this.state.request) {
|
||||||
this.state.request.abort();
|
this.state.request.abort();
|
||||||
}
|
}
|
||||||
var request = MessageUtils.getContent(nextProps.flow, nextProps.message);
|
var request = MessageUtils.getContent(nextProps.flow, nextProps.message);
|
||||||
@ -38,11 +38,11 @@ var RawMixin = {
|
|||||||
request.done(function (data) {
|
request.done(function (data) {
|
||||||
this.setState({content: data});
|
this.setState({content: data});
|
||||||
}.bind(this)).fail(function (jqXHR, textStatus, errorThrown) {
|
}.bind(this)).fail(function (jqXHR, textStatus, errorThrown) {
|
||||||
if(textStatus === "abort"){
|
if (textStatus === "abort") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown});
|
this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown});
|
||||||
}.bind(this)).always(function(){
|
}.bind(this)).always(function () {
|
||||||
this.setState({request: undefined});
|
this.setState({request: undefined});
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
@ -55,8 +55,8 @@ var RawMixin = {
|
|||||||
this.requestContent(nextProps);
|
this.requestContent(nextProps);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
componentWillUnmount: function(){
|
componentWillUnmount: function () {
|
||||||
if(this.state.request){
|
if (this.state.request) {
|
||||||
this.state.request.abort();
|
this.state.request.abort();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -94,7 +94,7 @@ var ViewJSON = React.createClass({
|
|||||||
var json = this.state.content;
|
var json = this.state.content;
|
||||||
try {
|
try {
|
||||||
json = JSON.stringify(JSON.parse(json), null, 2);
|
json = JSON.stringify(JSON.parse(json), null, 2);
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
return <pre>{json}</pre>;
|
return <pre>{json}</pre>;
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ var ContentMissing = React.createClass({
|
|||||||
|
|
||||||
var TooLarge = React.createClass({
|
var TooLarge = React.createClass({
|
||||||
statics: {
|
statics: {
|
||||||
isTooLarge: function(message){
|
isTooLarge: function (message) {
|
||||||
var max_mb = ViewImage.matches(message) ? 10 : 0.2;
|
var max_mb = ViewImage.matches(message) ? 10 : 0.2;
|
||||||
return message.contentLength > 1024 * 1024 * max_mb;
|
return message.contentLength > 1024 * 1024 * max_mb;
|
||||||
}
|
}
|
||||||
@ -225,8 +225,10 @@ var ContentView = React.createClass({
|
|||||||
<this.state.View {...this.props} />
|
<this.state.View {...this.props} />
|
||||||
<div className="view-options text-center">
|
<div className="view-options text-center">
|
||||||
<ViewSelector selectView={this.selectView} active={this.state.View} message={message}/>
|
<ViewSelector selectView={this.selectView} active={this.state.View} message={message}/>
|
||||||
|
|
||||||
<a className="btn btn-default btn-xs" href={downloadUrl}><i className="fa fa-download"/></a>
|
<a className="btn btn-default btn-xs" href={downloadUrl}>
|
||||||
|
<i className="fa fa-download"/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
var React = require("react");
|
var React = require("react");
|
||||||
|
var _ = require("lodash");
|
||||||
|
|
||||||
|
var common = require("../common.js");
|
||||||
|
var actions = require("../../actions.js");
|
||||||
var flowutils = require("../../flow/utils.js");
|
var flowutils = require("../../flow/utils.js");
|
||||||
var utils = require("../../utils.js");
|
var utils = require("../../utils.js");
|
||||||
var ContentView = require("./contentview.js");
|
var ContentView = require("./contentview.js");
|
||||||
@ -24,20 +27,217 @@ var Headers = React.createClass({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var InlineInput = React.createClass({
|
||||||
|
mixins: [common.ChildFocus],
|
||||||
|
getInitialState: function () {
|
||||||
|
return {
|
||||||
|
editable: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render: function () {
|
||||||
|
var Tag = this.props.tag || "span";
|
||||||
|
var className = "inline-input " + (this.props.className || "");
|
||||||
|
var html = {__html: _.escape(this.props.content)};
|
||||||
|
return <Tag
|
||||||
|
{...this.props}
|
||||||
|
tabIndex="0"
|
||||||
|
className={className}
|
||||||
|
contentEditable={this.state.editable || undefined}
|
||||||
|
onInput={this.onInput}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
dangerouslySetInnerHTML={html}
|
||||||
|
/>;
|
||||||
|
},
|
||||||
|
onKeyDown: function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case utils.Key.ESC:
|
||||||
|
this.blur();
|
||||||
|
break;
|
||||||
|
case utils.Key.ENTER:
|
||||||
|
e.preventDefault();
|
||||||
|
if (!e.ctrlKey) {
|
||||||
|
this.blur();
|
||||||
|
} else {
|
||||||
|
this.props.onDone && this.props.onDone();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blur: function(){
|
||||||
|
this.getDOMNode().blur();
|
||||||
|
this.context.returnFocus && this.context.returnFocus();
|
||||||
|
},
|
||||||
|
selectContents: function () {
|
||||||
|
var range = document.createRange();
|
||||||
|
range.selectNodeContents(this.getDOMNode());
|
||||||
|
var sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
},
|
||||||
|
onFocus: function () {
|
||||||
|
this.setState({editable: true}, this.selectContents);
|
||||||
|
},
|
||||||
|
onBlur: function (e) {
|
||||||
|
this.setState({editable: false});
|
||||||
|
this.handleChange();
|
||||||
|
this.props.onDone && this.props.onDone();
|
||||||
|
},
|
||||||
|
onInput: function () {
|
||||||
|
this.handleChange();
|
||||||
|
},
|
||||||
|
handleChange: function () {
|
||||||
|
var content = this.getDOMNode().textContent;
|
||||||
|
if (content !== this.props.content) {
|
||||||
|
this.props.onChange(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var ValidateInlineInput = React.createClass({
|
||||||
|
getInitialState: function () {
|
||||||
|
return {
|
||||||
|
content: ""+this.props.content,
|
||||||
|
originalContent: ""+this.props.content
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onChange: function (val) {
|
||||||
|
this.setState({
|
||||||
|
content: val
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDone: function () {
|
||||||
|
if (this.state.content === this.state.originalContent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.props.isValid(this.state.content)) {
|
||||||
|
this.props.onChange(this.state.content);
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
content: this.state.originalContent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
componentWillReceiveProps: function (nextProps) {
|
||||||
|
if (nextProps.content !== this.state.content) {
|
||||||
|
this.setState({
|
||||||
|
content: ""+nextProps.content,
|
||||||
|
originalContent: ""+nextProps.content
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: function () {
|
||||||
|
var className = this.props.className || "";
|
||||||
|
if (this.props.isValid(this.state.content)) {
|
||||||
|
className += " has-success";
|
||||||
|
} else {
|
||||||
|
className += " has-warning"
|
||||||
|
}
|
||||||
|
return <InlineInput {...this.props}
|
||||||
|
className={className}
|
||||||
|
content={this.state.content}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onDone={this.onDone}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var RequestLine = React.createClass({
|
||||||
|
render: function () {
|
||||||
|
var flow = this.props.flow;
|
||||||
|
var url = flowutils.RequestUtils.pretty_url(flow.request);
|
||||||
|
var httpver = "HTTP/" + flow.request.httpversion.join(".");
|
||||||
|
|
||||||
|
return <div className="first-line request-line">
|
||||||
|
<ValidateInlineInput content={flow.request.method} onChange={this.onMethodChange} isValid={this.isValidMethod}/>
|
||||||
|
|
||||||
|
<ValidateInlineInput content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
|
||||||
|
|
||||||
|
<ValidateInlineInput content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
|
||||||
|
</div>
|
||||||
|
},
|
||||||
|
isValidMethod: function (method) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
isValidUrl: function (url) {
|
||||||
|
var u = flowutils.parseUrl(url);
|
||||||
|
return !!u.host;
|
||||||
|
},
|
||||||
|
onMethodChange: function (nextMethod) {
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{request: {method: nextMethod}}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onUrlChange: function (nextUrl) {
|
||||||
|
var props = flowutils.parseUrl(nextUrl);
|
||||||
|
props.path = props.path || "";
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{request: props}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onHttpVersionChange: function (nextVer) {
|
||||||
|
var ver = flowutils.parseHttpVersion(nextVer);
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{request: {httpversion: ver}}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var ResponseLine = React.createClass({
|
||||||
|
render: function () {
|
||||||
|
var flow = this.props.flow;
|
||||||
|
var httpver = "HTTP/" + flow.response.httpversion.join(".");
|
||||||
|
return <div className="first-line response-line">
|
||||||
|
<ValidateInlineInput content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
|
||||||
|
|
||||||
|
<ValidateInlineInput content={flow.response.code} onChange={this.onCodeChange} isValid={this.isValidCode} />
|
||||||
|
|
||||||
|
<ValidateInlineInput content={flow.response.msg} onChange={this.onMsgChange} isValid={this.isValidMsg} />
|
||||||
|
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
isValidCode: function (code) {
|
||||||
|
return /^\d+$/.test(code);
|
||||||
|
},
|
||||||
|
isValidMsg: function () {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onHttpVersionChange: function (nextVer) {
|
||||||
|
var ver = flowutils.parseHttpVersion(nextVer);
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{response: {httpversion: ver}}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onMsgChange: function(nextMsg){
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{response: {msg: nextMsg}}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onCodeChange: function(nextCode){
|
||||||
|
nextCode = parseInt(nextCode);
|
||||||
|
actions.FlowActions.update(
|
||||||
|
this.props.flow,
|
||||||
|
{response: {code: nextCode}}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var Request = React.createClass({
|
var Request = React.createClass({
|
||||||
render: function () {
|
render: function () {
|
||||||
var flow = this.props.flow;
|
var flow = this.props.flow;
|
||||||
var first_line = [
|
|
||||||
flow.request.method,
|
|
||||||
flowutils.RequestUtils.pretty_url(flow.request),
|
|
||||||
"HTTP/" + flow.request.httpversion.join(".")
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
//TODO: Styling
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section className="request">
|
||||||
<div className="first-line">{ first_line }</div>
|
<RequestLine flow={flow}/>
|
||||||
|
{/*<ResponseLine flow={flow}/>*/}
|
||||||
<Headers message={flow.request}/>
|
<Headers message={flow.request}/>
|
||||||
<hr/>
|
<hr/>
|
||||||
<ContentView flow={flow} message={flow.request}/>
|
<ContentView flow={flow} message={flow.request}/>
|
||||||
@ -49,17 +249,10 @@ var Request = React.createClass({
|
|||||||
var Response = React.createClass({
|
var Response = React.createClass({
|
||||||
render: function () {
|
render: function () {
|
||||||
var flow = this.props.flow;
|
var flow = this.props.flow;
|
||||||
var first_line = [
|
|
||||||
"HTTP/" + flow.response.httpversion.join("."),
|
|
||||||
flow.response.code,
|
|
||||||
flow.response.msg
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
//TODO: Styling
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section className="response">
|
||||||
<div className="first-line">{ first_line }</div>
|
{/*<RequestLine flow={flow}/>*/}
|
||||||
|
<ResponseLine flow={flow}/>
|
||||||
<Headers message={flow.response}/>
|
<Headers message={flow.response}/>
|
||||||
<hr/>
|
<hr/>
|
||||||
<ContentView flow={flow} message={flow.response}/>
|
<ContentView flow={flow} message={flow.response}/>
|
||||||
|
@ -11,6 +11,15 @@ var FlowView = require("./flowview/index.js");
|
|||||||
|
|
||||||
var MainView = React.createClass({
|
var MainView = React.createClass({
|
||||||
mixins: [common.Navigation, common.State],
|
mixins: [common.Navigation, common.State],
|
||||||
|
childContextTypes: {
|
||||||
|
returnFocus: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
getChildContext: function() {
|
||||||
|
return { returnFocus: this.returnFocus };
|
||||||
|
},
|
||||||
|
returnFocus: function(){
|
||||||
|
this.getDOMNode().focus();
|
||||||
|
},
|
||||||
getInitialState: function () {
|
getInitialState: function () {
|
||||||
return {
|
return {
|
||||||
flows: [],
|
flows: [],
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
var _ = require("lodash");
|
var _ = require("lodash");
|
||||||
var $ = require("jquery");
|
var $ = require("jquery");
|
||||||
|
|
||||||
|
var defaultPorts = {
|
||||||
|
"http": 80,
|
||||||
|
"https": 443
|
||||||
|
};
|
||||||
|
|
||||||
var MessageUtils = {
|
var MessageUtils = {
|
||||||
getContentType: function (message) {
|
getContentType: function (message) {
|
||||||
return this.get_first_header(message, /^Content-Type$/i).split(";")[0].trim();
|
var ct = this.get_first_header(message, /^Content-Type$/i);
|
||||||
|
if(ct){
|
||||||
|
return ct.split(";")[0].trim();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
get_first_header: function (message, regex) {
|
get_first_header: function (message, regex) {
|
||||||
//FIXME: Cache Invalidation.
|
//FIXME: Cache Invalidation.
|
||||||
@ -36,25 +44,20 @@ var MessageUtils = {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
getContentURL: function(flow, message){
|
getContentURL: function (flow, message) {
|
||||||
if(message === flow.request){
|
if (message === flow.request) {
|
||||||
message = "request";
|
message = "request";
|
||||||
} else if (message === flow.response){
|
} else if (message === flow.response) {
|
||||||
message = "response";
|
message = "response";
|
||||||
}
|
}
|
||||||
return "/flows/" + flow.id + "/" + message + "/content";
|
return "/flows/" + flow.id + "/" + message + "/content";
|
||||||
},
|
},
|
||||||
getContent: function(flow, message){
|
getContent: function (flow, message) {
|
||||||
var url = MessageUtils.getContentURL(flow, message);
|
var url = MessageUtils.getContentURL(flow, message);
|
||||||
return $.get(url);
|
return $.get(url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var defaultPorts = {
|
|
||||||
"http": 80,
|
|
||||||
"https": 443
|
|
||||||
};
|
|
||||||
|
|
||||||
var RequestUtils = _.extend(MessageUtils, {
|
var RequestUtils = _.extend(MessageUtils, {
|
||||||
pretty_host: function (request) {
|
pretty_host: function (request) {
|
||||||
//FIXME: Add hostheader
|
//FIXME: Add hostheader
|
||||||
@ -72,8 +75,53 @@ var RequestUtils = _.extend(MessageUtils, {
|
|||||||
var ResponseUtils = _.extend(MessageUtils, {});
|
var ResponseUtils = _.extend(MessageUtils, {});
|
||||||
|
|
||||||
|
|
||||||
|
var parseUrl_regex = /^(?:(https?):\/\/)?([^\/:]+)?(?::(\d+))?(\/.*)?$/i;
|
||||||
|
var parseUrl = function (url) {
|
||||||
|
//there are many correct ways to parse a URL,
|
||||||
|
//however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-)
|
||||||
|
var parts = parseUrl_regex.exec(url);
|
||||||
|
|
||||||
|
var scheme = parts[1],
|
||||||
|
host = parts[2],
|
||||||
|
port = parseInt(parts[3]),
|
||||||
|
path = parts[4];
|
||||||
|
if (scheme) {
|
||||||
|
port = port || defaultPorts[scheme];
|
||||||
|
}
|
||||||
|
var ret = {};
|
||||||
|
if (scheme) {
|
||||||
|
ret.scheme = scheme;
|
||||||
|
}
|
||||||
|
if (host) {
|
||||||
|
ret.host = host;
|
||||||
|
}
|
||||||
|
if (port) {
|
||||||
|
ret.port = port;
|
||||||
|
}
|
||||||
|
if (path) {
|
||||||
|
ret.path = path;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var isValidHttpVersion_regex = /^HTTP\/\d+(\.\d+)*$/i;
|
||||||
|
var isValidHttpVersion = function (httpVersion) {
|
||||||
|
return isValidHttpVersion_regex.test(httpVersion);
|
||||||
|
};
|
||||||
|
|
||||||
|
var parseHttpVersion = function (httpVersion) {
|
||||||
|
httpVersion = httpVersion.replace("HTTP/", "").split(".");
|
||||||
|
return _.map(httpVersion, function (x) {
|
||||||
|
return parseInt(x);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ResponseUtils: ResponseUtils,
|
ResponseUtils: ResponseUtils,
|
||||||
RequestUtils: RequestUtils,
|
RequestUtils: RequestUtils,
|
||||||
MessageUtils: MessageUtils
|
MessageUtils: MessageUtils,
|
||||||
|
parseUrl: parseUrl,
|
||||||
|
parseHttpVersion: parseHttpVersion,
|
||||||
|
isValidHttpVersion: isValidHttpVersion
|
||||||
};
|
};
|
@ -2,6 +2,10 @@ var $ = require("jquery");
|
|||||||
var _ = require("lodash");
|
var _ = require("lodash");
|
||||||
var actions = require("./actions.js");
|
var actions = require("./actions.js");
|
||||||
|
|
||||||
|
//debug
|
||||||
|
window.$ = $;
|
||||||
|
window._ = _;
|
||||||
|
|
||||||
var Key = {
|
var Key = {
|
||||||
UP: 38,
|
UP: 38,
|
||||||
DOWN: 40,
|
DOWN: 40,
|
||||||
@ -28,17 +32,17 @@ var formatSize = function (bytes) {
|
|||||||
if (bytes === 0)
|
if (bytes === 0)
|
||||||
return "0";
|
return "0";
|
||||||
var prefix = ["b", "kb", "mb", "gb", "tb"];
|
var prefix = ["b", "kb", "mb", "gb", "tb"];
|
||||||
for (var i = 0; i < prefix.length; i++){
|
for (var i = 0; i < prefix.length; i++) {
|
||||||
if (Math.pow(1024, i + 1) > bytes){
|
if (Math.pow(1024, i + 1) > bytes) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var precision;
|
var precision;
|
||||||
if (bytes%Math.pow(1024, i) === 0)
|
if (bytes % Math.pow(1024, i) === 0)
|
||||||
precision = 0;
|
precision = 0;
|
||||||
else
|
else
|
||||||
precision = 1;
|
precision = 1;
|
||||||
return (bytes/Math.pow(1024, i)).toFixed(precision) + prefix[i];
|
return (bytes / Math.pow(1024, i)).toFixed(precision) + prefix[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -60,17 +64,16 @@ var formatTimeStamp = function (seconds) {
|
|||||||
return ts.replace("T", " ").replace("Z", "");
|
return ts.replace("T", " ").replace("Z", "");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// At some places, we need to sort strings alphabetically descending,
|
// At some places, we need to sort strings alphabetically descending,
|
||||||
// but we can only provide a key function.
|
// but we can only provide a key function.
|
||||||
// This beauty "reverses" a JS string.
|
// This beauty "reverses" a JS string.
|
||||||
var end = String.fromCharCode(0xffff);
|
var end = String.fromCharCode(0xffff);
|
||||||
function reverseString(s){
|
function reverseString(s) {
|
||||||
return String.fromCharCode.apply(String,
|
return String.fromCharCode.apply(String,
|
||||||
_.map(s.split(""), function (c) {
|
_.map(s.split(""), function (c) {
|
||||||
return 0xffff - c.charCodeAt(0);
|
return 0xffff - c.charCodeAt(0);
|
||||||
})
|
})
|
||||||
) + end;
|
) + end;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
@ -82,21 +85,22 @@ var xsrf = $.param({_xsrf: getCookie("_xsrf")});
|
|||||||
//Tornado XSRF Protection.
|
//Tornado XSRF Protection.
|
||||||
$.ajaxPrefilter(function (options) {
|
$.ajaxPrefilter(function (options) {
|
||||||
if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
|
if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
|
||||||
if (options.data) {
|
if(options.url.indexOf("?") === -1){
|
||||||
options.data += ("&" + xsrf);
|
options.url += "?" + xsrf;
|
||||||
} else {
|
} else {
|
||||||
options.data = xsrf;
|
options.url += "&" + xsrf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Log AJAX Errors
|
// Log AJAX Errors
|
||||||
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
|
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
|
||||||
if(thrownError === "abort"){
|
if (thrownError === "abort") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var message = jqXHR.responseText;
|
var message = jqXHR.responseText;
|
||||||
console.error(thrownError, message, arguments);
|
console.error(thrownError, message, arguments);
|
||||||
actions.EventLogActions.add_event(thrownError + ": " + message);
|
actions.EventLogActions.add_event(thrownError + ": " + message);
|
||||||
|
alert(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -104,5 +108,5 @@ module.exports = {
|
|||||||
formatTimeDelta: formatTimeDelta,
|
formatTimeDelta: formatTimeDelta,
|
||||||
formatTimeStamp: formatTimeStamp,
|
formatTimeStamp: formatTimeStamp,
|
||||||
reverseString: reverseString,
|
reverseString: reverseString,
|
||||||
Key: Key
|
Key: Key,
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user