web: initial attempt at header editor

This commit is contained in:
Maximilian Hils 2015-03-27 15:30:19 +01:00
parent 04f2467e84
commit 42cd942b64
5 changed files with 1593 additions and 1041 deletions

View File

@ -466,7 +466,7 @@ def common_options(parser):
"--replay-ignore-payload-param",
action="append", dest="replay_ignore_payload_params", type=str,
help="""
Request's payload parameters (application/x-www-form-urlencoded) to
Request's payload parameters (application/x-www-form-urlencoded or multipart/form-data) to
be ignored while searching for a saved flow to replay.
Can be passed multiple times.
"""

View File

@ -676,7 +676,7 @@ var LogMessage = React.createClass({displayName: "LogMessage",
}
return (
React.createElement("div", null,
indicator, " ", entry.message
indicator, " ", entry.message
)
);
},
@ -1724,15 +1724,77 @@ var utils = require("../../utils.js");
var ContentView = require("./contentview.js");
var Headers = React.createClass({displayName: "Headers",
propTypes: {
onChange: React.PropTypes.func.isRequired,
message: React.PropTypes.object.isRequired
},
onChange: function (row, col, val) {
var nextHeaders = _.cloneDeep(this.props.message.headers);
nextHeaders[row][col] = val;
if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
// do not delete last row
if (nextHeaders.length === 1) {
nextHeaders[0][0] = "Name";
nextHeaders[0][1] = "Value";
} else {
nextHeaders.splice(row, 1);
// manually move selection target if this has been the last row.
if(row === nextHeaders.length){
this._nextSel = (row-1)+"-value";
}
}
}
this.props.onChange(nextHeaders);
},
onTab: function (row, col, e) {
var headers = this.props.message.headers;
if (row === headers.length - 1 && col === 1) {
e.preventDefault();
var nextHeaders = _.cloneDeep(this.props.message.headers);
nextHeaders.push(["Name", "Value"]);
this.props.onChange(nextHeaders);
this._nextSel = (row + 1) + "-key";
}
},
componentDidUpdate: function () {
if (this._nextSel && this.refs[this._nextSel]) {
this.refs[this._nextSel].focus();
this._nextSel = undefined;
}
},
onRemove: function (row, col, e) {
if (col === 1) {
e.preventDefault();
this.refs[row + "-key"].focus();
} else if (row > 0) {
e.preventDefault();
this.refs[(row - 1) + "-value"].focus();
}
},
render: function () {
var rows = this.props.message.headers.map(function (header, i) {
var kEdit = React.createElement(HeaderInlineInput, {
ref: i + "-key",
content: header[0],
onChange: this.onChange.bind(null, i, 0),
onRemove: this.onRemove.bind(null, i, 0),
onTab: this.onTab.bind(null, i, 0)});
var vEdit = React.createElement(HeaderInlineInput, {
ref: i + "-value",
content: header[1],
onChange: this.onChange.bind(null, i, 1),
onRemove: this.onRemove.bind(null, i, 1),
onTab: this.onTab.bind(null, i, 1)});
return (
React.createElement("tr", {key: i},
React.createElement("td", {className: "header-name"}, header[0] + ":"),
React.createElement("td", {className: "header-value"}, header[1])
React.createElement("td", {className: "header-name"}, kEdit, ":"),
React.createElement("td", {className: "header-value"}, vEdit)
)
);
});
}.bind(this));
return (
React.createElement("table", {className: "header-table"},
React.createElement("tbody", null,
@ -1743,8 +1805,13 @@ var Headers = React.createClass({displayName: "Headers",
}
});
var InlineInput = React.createClass({displayName: "InlineInput",
mixins: [common.ChildFocus],
propTypes: {
content: React.PropTypes.string.isRequired, //must be string to match strict equality.
onChange: React.PropTypes.func.isRequired,
},
getInitialState: function () {
return {
editable: false
@ -1781,10 +1848,11 @@ var InlineInput = React.createClass({displayName: "InlineInput",
}
break;
default:
this.props.onKeyDown && this.props.onKeyDown(e);
break;
}
},
blur: function(){
blur: function () {
this.getDOMNode().blur();
this.context.returnFocus && this.context.returnFocus();
},
@ -1814,17 +1882,49 @@ var InlineInput = React.createClass({displayName: "InlineInput",
}
});
var HeaderInlineInput = React.createClass({displayName: "HeaderInlineInput",
render: function () {
return React.createElement(InlineInput, React.__spread({ref: "input"}, this.props, {onKeyDown: this.onKeyDown}));
},
focus: function () {
this.getDOMNode().focus();
},
onKeyDown: function (e) {
switch (e.keyCode) {
case utils.Key.BACKSPACE:
var s = window.getSelection().getRangeAt(0);
if (s.startOffset === 0 && s.endOffset === 0) {
this.props.onRemove(e);
}
break;
case utils.Key.TAB:
if(!e.shiftKey){
this.props.onTab(e);
}
break;
}
}
});
var ValidateInlineInput = React.createClass({displayName: "ValidateInlineInput",
propTypes: {
onChange: React.PropTypes.func.isRequired,
isValid: React.PropTypes.func.isRequired,
immediate: React.PropTypes.bool
},
getInitialState: function () {
return {
content: ""+this.props.content,
originalContent: ""+this.props.content
content: this.props.content,
originalContent: this.props.content
};
},
onChange: function (val) {
this.setState({
content: val
});
if (this.props.immediate && val !== this.state.originalContent && this.props.isValid(val)) {
this.props.onChange(val);
}
},
onDone: function () {
if (this.state.content === this.state.originalContent) {
@ -1841,8 +1941,8 @@ var ValidateInlineInput = React.createClass({displayName: "ValidateInlineInput",
componentWillReceiveProps: function (nextProps) {
if (nextProps.content !== this.state.content) {
this.setState({
content: ""+nextProps.content,
originalContent: ""+nextProps.content
content: nextProps.content,
originalContent: nextProps.content
})
}
},
@ -1869,16 +1969,13 @@ var RequestLine = React.createClass({displayName: "RequestLine",
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(InlineInput, {content: flow.request.method, onChange: this.onMethodChange}),
" ",
React.createElement(ValidateInlineInput, {content: url, onChange: this.onUrlChange, isValid: this.isValidUrl}),
" ",
React.createElement(ValidateInlineInput, {content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion})
React.createElement(ValidateInlineInput, {immediate: true, content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion})
)
},
isValidMethod: function (method) {
return true;
},
isValidUrl: function (url) {
var u = flowutils.parseUrl(url);
return !!u.host;
@ -1911,20 +2008,17 @@ var ResponseLine = React.createClass({displayName: "ResponseLine",
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, {immediate: true, content: httpver, onChange: this.onHttpVersionChange, isValid: flowutils.isValidHttpVersion}),
" ",
React.createElement(ValidateInlineInput, {content: flow.response.code, onChange: this.onCodeChange, isValid: this.isValidCode}),
React.createElement(ValidateInlineInput, {immediate: true, content: flow.response.code + "", onChange: this.onCodeChange, isValid: this.isValidCode}),
" ",
React.createElement(ValidateInlineInput, {content: flow.response.msg, onChange: this.onMsgChange, isValid: this.isValidMsg})
React.createElement(InlineInput, {content: flow.response.msg, onChange: this.onMsgChange})
);
},
isValidCode: function (code) {
return /^\d+$/.test(code);
},
isValidMsg: function () {
return true;
},
onHttpVersionChange: function (nextVer) {
var ver = flowutils.parseHttpVersion(nextVer);
actions.FlowActions.update(
@ -1932,13 +2026,13 @@ var ResponseLine = React.createClass({displayName: "ResponseLine",
{response: {httpversion: ver}}
);
},
onMsgChange: function(nextMsg){
onMsgChange: function (nextMsg) {
actions.FlowActions.update(
this.props.flow,
{response: {msg: nextMsg}}
);
},
onCodeChange: function(nextCode){
onCodeChange: function (nextCode) {
nextCode = parseInt(nextCode);
actions.FlowActions.update(
this.props.flow,
@ -1954,11 +2048,18 @@ var Request = React.createClass({displayName: "Request",
React.createElement("section", {className: "request"},
React.createElement(RequestLine, {flow: flow}),
/*<ResponseLine flow={flow}/>*/
React.createElement(Headers, {message: flow.request}),
React.createElement(Headers, {message: flow.request, onChange: this.onHeaderChange}),
React.createElement("hr", null),
React.createElement(ContentView, {flow: flow, message: flow.request})
)
);
},
onHeaderChange: function (nextHeaders) {
actions.FlowActions.update(this.props.flow, {
request: {
headers: nextHeaders
}
});
}
});
@ -1969,11 +2070,18 @@ var Response = React.createClass({displayName: "Response",
React.createElement("section", {className: "response"},
/*<RequestLine flow={flow}/>*/
React.createElement(ResponseLine, {flow: flow}),
React.createElement(Headers, {message: flow.response}),
React.createElement(Headers, {message: flow.response, onChange: this.onHeaderChange}),
React.createElement("hr", null),
React.createElement(ContentView, {flow: flow, message: flow.response})
)
);
},
onHeaderChange: function (nextHeaders) {
actions.FlowActions.update(this.props.flow, {
response: {
headers: nextHeaders
}
});
}
});
@ -4826,6 +4934,9 @@ 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);
if(!parts){
return false;
}
var scheme = parts[1],
host = parts[2],

File diff suppressed because it is too large Load Diff

View File

@ -8,15 +8,77 @@ var utils = require("../../utils.js");
var ContentView = require("./contentview.js");
var Headers = React.createClass({
propTypes: {
onChange: React.PropTypes.func.isRequired,
message: React.PropTypes.object.isRequired
},
onChange: function (row, col, val) {
var nextHeaders = _.cloneDeep(this.props.message.headers);
nextHeaders[row][col] = val;
if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
// do not delete last row
if (nextHeaders.length === 1) {
nextHeaders[0][0] = "Name";
nextHeaders[0][1] = "Value";
} else {
nextHeaders.splice(row, 1);
// manually move selection target if this has been the last row.
if(row === nextHeaders.length){
this._nextSel = (row-1)+"-value";
}
}
}
this.props.onChange(nextHeaders);
},
onTab: function (row, col, e) {
var headers = this.props.message.headers;
if (row === headers.length - 1 && col === 1) {
e.preventDefault();
var nextHeaders = _.cloneDeep(this.props.message.headers);
nextHeaders.push(["Name", "Value"]);
this.props.onChange(nextHeaders);
this._nextSel = (row + 1) + "-key";
}
},
componentDidUpdate: function () {
if (this._nextSel && this.refs[this._nextSel]) {
this.refs[this._nextSel].focus();
this._nextSel = undefined;
}
},
onRemove: function (row, col, e) {
if (col === 1) {
e.preventDefault();
this.refs[row + "-key"].focus();
} else if (row > 0) {
e.preventDefault();
this.refs[(row - 1) + "-value"].focus();
}
},
render: function () {
var rows = this.props.message.headers.map(function (header, i) {
var kEdit = <HeaderInlineInput
ref={i + "-key"}
content={header[0]}
onChange={this.onChange.bind(null, i, 0)}
onRemove={this.onRemove.bind(null, i, 0)}
onTab={this.onTab.bind(null, i, 0)}/>;
var vEdit = <HeaderInlineInput
ref={i + "-value"}
content={header[1]}
onChange={this.onChange.bind(null, i, 1)}
onRemove={this.onRemove.bind(null, i, 1)}
onTab={this.onTab.bind(null, i, 1)}/>;
return (
<tr key={i}>
<td className="header-name">{header[0] + ":"}</td>
<td className="header-value">{header[1]}</td>
<td className="header-name">{kEdit}:</td>
<td className="header-value">{vEdit}</td>
</tr>
);
});
}.bind(this));
return (
<table className="header-table">
<tbody>
@ -27,8 +89,13 @@ var Headers = React.createClass({
}
});
var InlineInput = React.createClass({
mixins: [common.ChildFocus],
propTypes: {
content: React.PropTypes.string.isRequired, //must be string to match strict equality.
onChange: React.PropTypes.func.isRequired,
},
getInitialState: function () {
return {
editable: false
@ -65,10 +132,11 @@ var InlineInput = React.createClass({
}
break;
default:
this.props.onKeyDown && this.props.onKeyDown(e);
break;
}
},
blur: function(){
blur: function () {
this.getDOMNode().blur();
this.context.returnFocus && this.context.returnFocus();
},
@ -98,17 +166,49 @@ var InlineInput = React.createClass({
}
});
var HeaderInlineInput = React.createClass({
render: function () {
return <InlineInput ref="input" {...this.props} onKeyDown={this.onKeyDown}/>;
},
focus: function () {
this.getDOMNode().focus();
},
onKeyDown: function (e) {
switch (e.keyCode) {
case utils.Key.BACKSPACE:
var s = window.getSelection().getRangeAt(0);
if (s.startOffset === 0 && s.endOffset === 0) {
this.props.onRemove(e);
}
break;
case utils.Key.TAB:
if(!e.shiftKey){
this.props.onTab(e);
}
break;
}
}
});
var ValidateInlineInput = React.createClass({
propTypes: {
onChange: React.PropTypes.func.isRequired,
isValid: React.PropTypes.func.isRequired,
immediate: React.PropTypes.bool
},
getInitialState: function () {
return {
content: ""+this.props.content,
originalContent: ""+this.props.content
content: this.props.content,
originalContent: this.props.content
};
},
onChange: function (val) {
this.setState({
content: val
});
if (this.props.immediate && val !== this.state.originalContent && this.props.isValid(val)) {
this.props.onChange(val);
}
},
onDone: function () {
if (this.state.content === this.state.originalContent) {
@ -125,8 +225,8 @@ var ValidateInlineInput = React.createClass({
componentWillReceiveProps: function (nextProps) {
if (nextProps.content !== this.state.content) {
this.setState({
content: ""+nextProps.content,
originalContent: ""+nextProps.content
content: nextProps.content,
originalContent: nextProps.content
})
}
},
@ -153,16 +253,13 @@ var RequestLine = React.createClass({
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}/>
<InlineInput content={flow.request.method} onChange={this.onMethodChange}/>
&nbsp;
<ValidateInlineInput content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
&nbsp;
<ValidateInlineInput content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
<ValidateInlineInput immediate 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;
@ -195,20 +292,17 @@ var ResponseLine = React.createClass({
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 immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
&nbsp;
<ValidateInlineInput content={flow.response.code} onChange={this.onCodeChange} isValid={this.isValidCode} />
<ValidateInlineInput immediate content={flow.response.code + ""} onChange={this.onCodeChange} isValid={this.isValidCode} />
&nbsp;
<ValidateInlineInput content={flow.response.msg} onChange={this.onMsgChange} isValid={this.isValidMsg} />
<InlineInput content={flow.response.msg} onChange={this.onMsgChange}/>
</div>;
},
isValidCode: function (code) {
return /^\d+$/.test(code);
},
isValidMsg: function () {
return true;
},
onHttpVersionChange: function (nextVer) {
var ver = flowutils.parseHttpVersion(nextVer);
actions.FlowActions.update(
@ -216,13 +310,13 @@ var ResponseLine = React.createClass({
{response: {httpversion: ver}}
);
},
onMsgChange: function(nextMsg){
onMsgChange: function (nextMsg) {
actions.FlowActions.update(
this.props.flow,
{response: {msg: nextMsg}}
);
},
onCodeChange: function(nextCode){
onCodeChange: function (nextCode) {
nextCode = parseInt(nextCode);
actions.FlowActions.update(
this.props.flow,
@ -238,11 +332,18 @@ var Request = React.createClass({
<section className="request">
<RequestLine flow={flow}/>
{/*<ResponseLine flow={flow}/>*/}
<Headers message={flow.request}/>
<Headers message={flow.request} onChange={this.onHeaderChange}/>
<hr/>
<ContentView flow={flow} message={flow.request}/>
</section>
);
},
onHeaderChange: function (nextHeaders) {
actions.FlowActions.update(this.props.flow, {
request: {
headers: nextHeaders
}
});
}
});
@ -253,11 +354,18 @@ var Response = React.createClass({
<section className="response">
{/*<RequestLine flow={flow}/>*/}
<ResponseLine flow={flow}/>
<Headers message={flow.response}/>
<Headers message={flow.response} onChange={this.onHeaderChange}/>
<hr/>
<ContentView flow={flow} message={flow.response}/>
</section>
);
},
onHeaderChange: function (nextHeaders) {
actions.FlowActions.update(this.props.flow, {
response: {
headers: nextHeaders
}
});
}
});

View File

@ -80,6 +80,9 @@ 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);
if(!parts){
return false;
}
var scheme = parts[1],
host = parts[2],