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", "--replay-ignore-payload-param",
action="append", dest="replay_ignore_payload_params", type=str, action="append", dest="replay_ignore_payload_params", type=str,
help=""" 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. be ignored while searching for a saved flow to replay.
Can be passed multiple times. Can be passed multiple times.
""" """

View File

@ -676,7 +676,7 @@ var LogMessage = React.createClass({displayName: "LogMessage",
} }
return ( return (
React.createElement("div", null, 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 ContentView = require("./contentview.js");
var Headers = React.createClass({displayName: "Headers", 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 () { render: function () {
var rows = this.props.message.headers.map(function (header, i) { 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 ( return (
React.createElement("tr", {key: i}, React.createElement("tr", {key: i},
React.createElement("td", {className: "header-name"}, header[0] + ":"), React.createElement("td", {className: "header-name"}, kEdit, ":"),
React.createElement("td", {className: "header-value"}, header[1]) React.createElement("td", {className: "header-value"}, vEdit)
) )
); );
}); }.bind(this));
return ( return (
React.createElement("table", {className: "header-table"}, React.createElement("table", {className: "header-table"},
React.createElement("tbody", null, React.createElement("tbody", null,
@ -1743,8 +1805,13 @@ var Headers = React.createClass({displayName: "Headers",
} }
}); });
var InlineInput = React.createClass({displayName: "InlineInput", var InlineInput = React.createClass({displayName: "InlineInput",
mixins: [common.ChildFocus], mixins: [common.ChildFocus],
propTypes: {
content: React.PropTypes.string.isRequired, //must be string to match strict equality.
onChange: React.PropTypes.func.isRequired,
},
getInitialState: function () { getInitialState: function () {
return { return {
editable: false editable: false
@ -1781,10 +1848,11 @@ var InlineInput = React.createClass({displayName: "InlineInput",
} }
break; break;
default: default:
this.props.onKeyDown && this.props.onKeyDown(e);
break; break;
} }
}, },
blur: function(){ blur: function () {
this.getDOMNode().blur(); this.getDOMNode().blur();
this.context.returnFocus && this.context.returnFocus(); 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", var ValidateInlineInput = React.createClass({displayName: "ValidateInlineInput",
propTypes: {
onChange: React.PropTypes.func.isRequired,
isValid: React.PropTypes.func.isRequired,
immediate: React.PropTypes.bool
},
getInitialState: function () { getInitialState: function () {
return { return {
content: ""+this.props.content, content: this.props.content,
originalContent: ""+this.props.content originalContent: this.props.content
}; };
}, },
onChange: function (val) { onChange: function (val) {
this.setState({ this.setState({
content: val content: val
}); });
if (this.props.immediate && val !== this.state.originalContent && this.props.isValid(val)) {
this.props.onChange(val);
}
}, },
onDone: function () { onDone: function () {
if (this.state.content === this.state.originalContent) { if (this.state.content === this.state.originalContent) {
@ -1841,8 +1941,8 @@ var ValidateInlineInput = React.createClass({displayName: "ValidateInlineInput",
componentWillReceiveProps: function (nextProps) { componentWillReceiveProps: function (nextProps) {
if (nextProps.content !== this.state.content) { if (nextProps.content !== this.state.content) {
this.setState({ this.setState({
content: ""+nextProps.content, content: nextProps.content,
originalContent: ""+nextProps.content originalContent: nextProps.content
}) })
} }
}, },
@ -1869,16 +1969,13 @@ var RequestLine = React.createClass({displayName: "RequestLine",
var httpver = "HTTP/" + flow.request.httpversion.join("."); var httpver = "HTTP/" + flow.request.httpversion.join(".");
return React.createElement("div", {className: "first-line request-line"}, 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: 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) { isValidUrl: function (url) {
var u = flowutils.parseUrl(url); var u = flowutils.parseUrl(url);
return !!u.host; return !!u.host;
@ -1911,20 +2008,17 @@ var ResponseLine = React.createClass({displayName: "ResponseLine",
var flow = this.props.flow; var flow = this.props.flow;
var httpver = "HTTP/" + flow.response.httpversion.join("."); var httpver = "HTTP/" + flow.response.httpversion.join(".");
return React.createElement("div", {className: "first-line response-line"}, 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) { isValidCode: function (code) {
return /^\d+$/.test(code); return /^\d+$/.test(code);
}, },
isValidMsg: function () {
return true;
},
onHttpVersionChange: function (nextVer) { onHttpVersionChange: function (nextVer) {
var ver = flowutils.parseHttpVersion(nextVer); var ver = flowutils.parseHttpVersion(nextVer);
actions.FlowActions.update( actions.FlowActions.update(
@ -1932,13 +2026,13 @@ var ResponseLine = React.createClass({displayName: "ResponseLine",
{response: {httpversion: ver}} {response: {httpversion: ver}}
); );
}, },
onMsgChange: function(nextMsg){ onMsgChange: function (nextMsg) {
actions.FlowActions.update( actions.FlowActions.update(
this.props.flow, this.props.flow,
{response: {msg: nextMsg}} {response: {msg: nextMsg}}
); );
}, },
onCodeChange: function(nextCode){ onCodeChange: function (nextCode) {
nextCode = parseInt(nextCode); nextCode = parseInt(nextCode);
actions.FlowActions.update( actions.FlowActions.update(
this.props.flow, this.props.flow,
@ -1954,11 +2048,18 @@ var Request = React.createClass({displayName: "Request",
React.createElement("section", {className: "request"}, React.createElement("section", {className: "request"},
React.createElement(RequestLine, {flow: flow}), React.createElement(RequestLine, {flow: flow}),
/*<ResponseLine 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("hr", null),
React.createElement(ContentView, {flow: flow, message: flow.request}) 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"}, React.createElement("section", {className: "response"},
/*<RequestLine flow={flow}/>*/ /*<RequestLine flow={flow}/>*/
React.createElement(ResponseLine, {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("hr", null),
React.createElement(ContentView, {flow: flow, message: flow.response}) 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, //there are many correct ways to parse a URL,
//however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-) //however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-)
var parts = parseUrl_regex.exec(url); var parts = parseUrl_regex.exec(url);
if(!parts){
return false;
}
var scheme = parts[1], var scheme = parts[1],
host = parts[2], 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 ContentView = require("./contentview.js");
var Headers = React.createClass({ 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 () { render: function () {
var rows = this.props.message.headers.map(function (header, i) { 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 ( return (
<tr key={i}> <tr key={i}>
<td className="header-name">{header[0] + ":"}</td> <td className="header-name">{kEdit}:</td>
<td className="header-value">{header[1]}</td> <td className="header-value">{vEdit}</td>
</tr> </tr>
); );
}); }.bind(this));
return ( return (
<table className="header-table"> <table className="header-table">
<tbody> <tbody>
@ -27,8 +89,13 @@ var Headers = React.createClass({
} }
}); });
var InlineInput = React.createClass({ var InlineInput = React.createClass({
mixins: [common.ChildFocus], mixins: [common.ChildFocus],
propTypes: {
content: React.PropTypes.string.isRequired, //must be string to match strict equality.
onChange: React.PropTypes.func.isRequired,
},
getInitialState: function () { getInitialState: function () {
return { return {
editable: false editable: false
@ -65,10 +132,11 @@ var InlineInput = React.createClass({
} }
break; break;
default: default:
this.props.onKeyDown && this.props.onKeyDown(e);
break; break;
} }
}, },
blur: function(){ blur: function () {
this.getDOMNode().blur(); this.getDOMNode().blur();
this.context.returnFocus && this.context.returnFocus(); 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({ var ValidateInlineInput = React.createClass({
propTypes: {
onChange: React.PropTypes.func.isRequired,
isValid: React.PropTypes.func.isRequired,
immediate: React.PropTypes.bool
},
getInitialState: function () { getInitialState: function () {
return { return {
content: ""+this.props.content, content: this.props.content,
originalContent: ""+this.props.content originalContent: this.props.content
}; };
}, },
onChange: function (val) { onChange: function (val) {
this.setState({ this.setState({
content: val content: val
}); });
if (this.props.immediate && val !== this.state.originalContent && this.props.isValid(val)) {
this.props.onChange(val);
}
}, },
onDone: function () { onDone: function () {
if (this.state.content === this.state.originalContent) { if (this.state.content === this.state.originalContent) {
@ -125,8 +225,8 @@ var ValidateInlineInput = React.createClass({
componentWillReceiveProps: function (nextProps) { componentWillReceiveProps: function (nextProps) {
if (nextProps.content !== this.state.content) { if (nextProps.content !== this.state.content) {
this.setState({ this.setState({
content: ""+nextProps.content, content: nextProps.content,
originalContent: ""+nextProps.content originalContent: nextProps.content
}) })
} }
}, },
@ -153,16 +253,13 @@ var RequestLine = React.createClass({
var httpver = "HTTP/" + flow.request.httpversion.join("."); var httpver = "HTTP/" + flow.request.httpversion.join(".");
return <div className="first-line request-line"> 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; &nbsp;
<ValidateInlineInput content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} /> <ValidateInlineInput content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
&nbsp; &nbsp;
<ValidateInlineInput content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} /> <ValidateInlineInput immediate content={httpver} onChange={this.onHttpVersionChange} isValid={flowutils.isValidHttpVersion} />
</div> </div>
}, },
isValidMethod: function (method) {
return true;
},
isValidUrl: function (url) { isValidUrl: function (url) {
var u = flowutils.parseUrl(url); var u = flowutils.parseUrl(url);
return !!u.host; return !!u.host;
@ -195,20 +292,17 @@ var ResponseLine = React.createClass({
var flow = this.props.flow; var flow = this.props.flow;
var httpver = "HTTP/" + flow.response.httpversion.join("."); var httpver = "HTTP/" + flow.response.httpversion.join(".");
return <div className="first-line response-line"> 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; &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; &nbsp;
<ValidateInlineInput content={flow.response.msg} onChange={this.onMsgChange} isValid={this.isValidMsg} /> <InlineInput content={flow.response.msg} onChange={this.onMsgChange}/>
</div>; </div>;
}, },
isValidCode: function (code) { isValidCode: function (code) {
return /^\d+$/.test(code); return /^\d+$/.test(code);
}, },
isValidMsg: function () {
return true;
},
onHttpVersionChange: function (nextVer) { onHttpVersionChange: function (nextVer) {
var ver = flowutils.parseHttpVersion(nextVer); var ver = flowutils.parseHttpVersion(nextVer);
actions.FlowActions.update( actions.FlowActions.update(
@ -216,13 +310,13 @@ var ResponseLine = React.createClass({
{response: {httpversion: ver}} {response: {httpversion: ver}}
); );
}, },
onMsgChange: function(nextMsg){ onMsgChange: function (nextMsg) {
actions.FlowActions.update( actions.FlowActions.update(
this.props.flow, this.props.flow,
{response: {msg: nextMsg}} {response: {msg: nextMsg}}
); );
}, },
onCodeChange: function(nextCode){ onCodeChange: function (nextCode) {
nextCode = parseInt(nextCode); nextCode = parseInt(nextCode);
actions.FlowActions.update( actions.FlowActions.update(
this.props.flow, this.props.flow,
@ -238,11 +332,18 @@ var Request = React.createClass({
<section className="request"> <section className="request">
<RequestLine flow={flow}/> <RequestLine flow={flow}/>
{/*<ResponseLine flow={flow}/>*/} {/*<ResponseLine flow={flow}/>*/}
<Headers message={flow.request}/> <Headers message={flow.request} onChange={this.onHeaderChange}/>
<hr/> <hr/>
<ContentView flow={flow} message={flow.request}/> <ContentView flow={flow} message={flow.request}/>
</section> </section>
); );
},
onHeaderChange: function (nextHeaders) {
actions.FlowActions.update(this.props.flow, {
request: {
headers: nextHeaders
}
});
} }
}); });
@ -253,11 +354,18 @@ var Response = React.createClass({
<section className="response"> <section className="response">
{/*<RequestLine flow={flow}/>*/} {/*<RequestLine flow={flow}/>*/}
<ResponseLine flow={flow}/> <ResponseLine flow={flow}/>
<Headers message={flow.response}/> <Headers message={flow.response} onChange={this.onHeaderChange}/>
<hr/> <hr/>
<ContentView flow={flow} message={flow.response}/> <ContentView flow={flow} message={flow.response}/>
</section> </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, //there are many correct ways to parse a URL,
//however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-) //however, a mitmproxy user may also wish to generate a not-so-correct URL. ;-)
var parts = parseUrl_regex.exec(url); var parts = parseUrl_regex.exec(url);
if(!parts){
return false;
}
var scheme = parts[1], var scheme = parts[1],
host = parts[2], host = parts[2],