aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2015-03-23 00:24:56 +0100
committerMaximilian Hils <git@maximilianhils.com>2015-03-23 00:24:56 +0100
commit968c7021dfef00c459899520921faf7367e923d9 (patch)
tree71df74f580e277add1f11439ea3ae32019f13789 /web
parent2acd77dea025b489d5d0ca19fc1c84901ac335d4 (diff)
downloadmitmproxy-968c7021dfef00c459899520921faf7367e923d9.tar.gz
mitmproxy-968c7021dfef00c459899520921faf7367e923d9.tar.bz2
mitmproxy-968c7021dfef00c459899520921faf7367e923d9.zip
web: add basic edit capability for first line
Diffstat (limited to 'web')
-rw-r--r--web/src/css/flowdetail.less24
-rw-r--r--web/src/js/actions.js21
-rw-r--r--web/src/js/components/common.js8
-rw-r--r--web/src/js/components/flowtable-columns.js4
-rw-r--r--web/src/js/components/flowview/contentview.js20
-rw-r--r--web/src/js/components/flowview/messages.js231
-rw-r--r--web/src/js/components/mainview.js9
-rw-r--r--web/src/js/flow/utils.js70
-rw-r--r--web/src/js/utils.js34
9 files changed, 361 insertions, 60 deletions
diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less
index cc67eeb2..edf97566 100644
--- a/web/src/css/flowdetail.less
+++ b/web/src/css/flowdetail.less
@@ -27,6 +27,13 @@
max-height: 100px;
overflow-y: auto;
}
+ .request-line {
+ margin-bottom: 2px;
+ }
+ /*.request .response-line,
+ .response .request-line {
+ opacity: 0.7;
+ }*/
hr {
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 {
margin-top: 10px;
}
diff --git a/web/src/js/actions.js b/web/src/js/actions.js
index 78fd4bf7..87c99dd2 100644
--- a/web/src/js/actions.js
+++ b/web/src/js/actions.js
@@ -1,4 +1,5 @@
var $ = require("jquery");
+var _ = require("lodash");
var AppDispatcher = require("./dispatcher.js").AppDispatcher;
var ActionTypes = {
@@ -44,7 +45,8 @@ var SettingsActions = {
$.ajax({
type: "PUT",
url: "/settings",
- data: settings
+ contentType: 'application/json',
+ data: JSON.stringify(settings)
});
/*
@@ -95,15 +97,26 @@ var FlowActions = {
revert: function(flow){
$.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({
type: ActionTypes.FLOW_STORE,
cmd: StoreCmds.UPDATE,
- data: flow
+ data: nextFlow
+ });
+ */
+ $.ajax({
+ type: "PUT",
+ url: "/flows/" + flow.id,
+ contentType: 'application/json',
+ data: JSON.stringify(nextProps)
});
},
clear: function(){
- $.post("/clear");
+ $.post("/flows/" + flow.id);
}
};
diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js
index 3ed035ee..956a46d2 100644
--- a/web/src/js/components/common.js
+++ b/web/src/js/components/common.js
@@ -30,6 +30,13 @@ var StickyHeadMixin = {
};
+var ChildFocus = {
+ contextTypes: {
+ returnFocus: React.PropTypes.func
+ }
+};
+
+
var Navigation = _.extend({}, ReactRouter.Navigation, {
setQuery: function (dict) {
var q = this.context.router.getCurrentQuery();
@@ -176,6 +183,7 @@ var Splitter = React.createClass({
});
module.exports = {
+ ChildFocus: ChildFocus,
State: State,
Navigation: Navigation,
StickyHeadMixin: StickyHeadMixin,
diff --git a/web/src/js/components/flowtable-columns.js b/web/src/js/components/flowtable-columns.js
index a82c607a..b3b47910 100644
--- a/web/src/js/components/flowtable-columns.js
+++ b/web/src/js/components/flowtable-columns.js
@@ -16,7 +16,7 @@ var TLSColumn = React.createClass({
},
render: function () {
var flow = this.props.flow;
- var ssl = (flow.request.scheme == "https");
+ var ssl = (flow.request.scheme === "https");
var classes;
if (ssl) {
classes = "col-tls col-tls-https";
@@ -44,7 +44,7 @@ var IconColumn = React.createClass({
var contentType = ResponseUtils.getContentType(flow.response);
//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";
} else if (300 <= flow.response.code && flow.response.code < 400) {
icon = "resource-icon-redirect";
diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js
index 828c6d08..63d22c1c 100644
--- a/web/src/js/components/flowview/contentview.js
+++ b/web/src/js/components/flowview/contentview.js
@@ -27,7 +27,7 @@ var RawMixin = {
}
},
requestContent: function (nextProps) {
- if(this.state.request){
+ if (this.state.request) {
this.state.request.abort();
}
var request = MessageUtils.getContent(nextProps.flow, nextProps.message);
@@ -38,11 +38,11 @@ var RawMixin = {
request.done(function (data) {
this.setState({content: data});
}.bind(this)).fail(function (jqXHR, textStatus, errorThrown) {
- if(textStatus === "abort"){
+ if (textStatus === "abort") {
return;
}
this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown});
- }.bind(this)).always(function(){
+ }.bind(this)).always(function () {
this.setState({request: undefined});
}.bind(this));
@@ -55,8 +55,8 @@ var RawMixin = {
this.requestContent(nextProps);
}
},
- componentWillUnmount: function(){
- if(this.state.request){
+ componentWillUnmount: function () {
+ if (this.state.request) {
this.state.request.abort();
}
},
@@ -94,7 +94,7 @@ var ViewJSON = React.createClass({
var json = this.state.content;
try {
json = JSON.stringify(JSON.parse(json), null, 2);
- } catch(e) {
+ } catch (e) {
}
return <pre>{json}</pre>;
}
@@ -139,7 +139,7 @@ var ContentMissing = React.createClass({
var TooLarge = React.createClass({
statics: {
- isTooLarge: function(message){
+ isTooLarge: function (message) {
var max_mb = ViewImage.matches(message) ? 10 : 0.2;
return message.contentLength > 1024 * 1024 * max_mb;
}
@@ -225,8 +225,10 @@ var ContentView = React.createClass({
<this.state.View {...this.props} />
<div className="view-options text-center">
<ViewSelector selectView={this.selectView} active={this.state.View} message={message}/>
- &nbsp;
- <a className="btn btn-default btn-xs" href={downloadUrl}><i className="fa fa-download"/></a>
+ &nbsp;
+ <a className="btn btn-default btn-xs" href={downloadUrl}>
+ <i className="fa fa-download"/>
+ </a>
</div>
</div>;
}
diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js
index fe8fa200..cb8516fc 100644
--- a/web/src/js/components/flowview/messages.js
+++ b/web/src/js/components/flowview/messages.js
@@ -1,5 +1,8 @@
var React = require("react");
+var _ = require("lodash");
+var common = require("../common.js");
+var actions = require("../../actions.js");
var flowutils = require("../../flow/utils.js");
var utils = require("../../utils.js");
var ContentView = require("./contentview.js");
@@ -24,20 +27,217 @@ var Headers = React.createClass({
}
});
-var Request = 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 first_line = [
- flow.request.method,
- flowutils.RequestUtils.pretty_url(flow.request),
- "HTTP/" + flow.request.httpversion.join(".")
- ].join(" ");
+ var url = flowutils.RequestUtils.pretty_url(flow.request);
+ var httpver = "HTTP/" + flow.request.httpversion.join(".");
- //TODO: Styling
+ return <div className="first-line request-line">
+ <ValidateInlineInput content={flow.request.method} onChange={this.onMethodChange} isValid={this.isValidMethod}/>
+ &nbsp;
+ <ValidateInlineInput content={url} onChange={this.onUrlChange} isValid={this.isValidUrl} />
+ &nbsp;
+ <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} />
+ &nbsp;
+ <ValidateInlineInput content={flow.response.code} onChange={this.onCodeChange} isValid={this.isValidCode} />
+ &nbsp;
+ <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({
+ render: function () {
+ var flow = this.props.flow;
return (
- <section>
- <div className="first-line">{ first_line }</div>
+ <section className="request">
+ <RequestLine flow={flow}/>
+ {/*<ResponseLine flow={flow}/>*/}
<Headers message={flow.request}/>
<hr/>
<ContentView flow={flow} message={flow.request}/>
@@ -49,17 +249,10 @@ var Request = React.createClass({
var Response = React.createClass({
render: function () {
var flow = this.props.flow;
- var first_line = [
- "HTTP/" + flow.response.httpversion.join("."),
- flow.response.code,
- flow.response.msg
- ].join(" ");
-
- //TODO: Styling
-
return (
- <section>
- <div className="first-line">{ first_line }</div>
+ <section className="response">
+ {/*<RequestLine flow={flow}/>*/}
+ <ResponseLine flow={flow}/>
<Headers message={flow.response}/>
<hr/>
<ContentView flow={flow} message={flow.response}/>
diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js
index 81bf3b03..9790a69e 100644
--- a/web/src/js/components/mainview.js
+++ b/web/src/js/components/mainview.js
@@ -11,6 +11,15 @@ var FlowView = require("./flowview/index.js");
var MainView = React.createClass({
mixins: [common.Navigation, common.State],
+ childContextTypes: {
+ returnFocus: React.PropTypes.func.isRequired
+ },
+ getChildContext: function() {
+ return { returnFocus: this.returnFocus };
+ },
+ returnFocus: function(){
+ this.getDOMNode().focus();
+ },
getInitialState: function () {
return {
flows: [],
diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js
index 29462a78..aa91e3b6 100644
--- a/web/src/js/flow/utils.js
+++ b/web/src/js/flow/utils.js
@@ -1,9 +1,17 @@
var _ = require("lodash");
var $ = require("jquery");
+var defaultPorts = {
+ "http": 80,
+ "https": 443
+};
+
var MessageUtils = {
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) {
//FIXME: Cache Invalidation.
@@ -36,25 +44,20 @@ var MessageUtils = {
}
return false;
},
- getContentURL: function(flow, message){
- if(message === flow.request){
+ getContentURL: function (flow, message) {
+ if (message === flow.request) {
message = "request";
- } else if (message === flow.response){
+ } else if (message === flow.response) {
message = "response";
}
return "/flows/" + flow.id + "/" + message + "/content";
},
- getContent: function(flow, message){
+ getContent: function (flow, message) {
var url = MessageUtils.getContentURL(flow, message);
return $.get(url);
}
};
-var defaultPorts = {
- "http": 80,
- "https": 443
-};
-
var RequestUtils = _.extend(MessageUtils, {
pretty_host: function (request) {
//FIXME: Add hostheader
@@ -72,8 +75,53 @@ var RequestUtils = _.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 = {
ResponseUtils: ResponseUtils,
RequestUtils: RequestUtils,
- MessageUtils: MessageUtils
+ MessageUtils: MessageUtils,
+ parseUrl: parseUrl,
+ parseHttpVersion: parseHttpVersion,
+ isValidHttpVersion: isValidHttpVersion
}; \ No newline at end of file
diff --git a/web/src/js/utils.js b/web/src/js/utils.js
index be59db96..6fc11f9e 100644
--- a/web/src/js/utils.js
+++ b/web/src/js/utils.js
@@ -2,6 +2,10 @@ var $ = require("jquery");
var _ = require("lodash");
var actions = require("./actions.js");
+//debug
+window.$ = $;
+window._ = _;
+
var Key = {
UP: 38,
DOWN: 40,
@@ -28,17 +32,17 @@ var formatSize = function (bytes) {
if (bytes === 0)
return "0";
var prefix = ["b", "kb", "mb", "gb", "tb"];
- for (var i = 0; i < prefix.length; i++){
- if (Math.pow(1024, i + 1) > bytes){
+ for (var i = 0; i < prefix.length; i++) {
+ if (Math.pow(1024, i + 1) > bytes) {
break;
}
}
var precision;
- if (bytes%Math.pow(1024, i) === 0)
+ if (bytes % Math.pow(1024, i) === 0)
precision = 0;
else
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", "");
};
-
// At some places, we need to sort strings alphabetically descending,
// but we can only provide a key function.
// This beauty "reverses" a JS string.
var end = String.fromCharCode(0xffff);
-function reverseString(s){
+function reverseString(s) {
return String.fromCharCode.apply(String,
- _.map(s.split(""), function (c) {
- return 0xffff - c.charCodeAt(0);
- })
- ) + end;
+ _.map(s.split(""), function (c) {
+ return 0xffff - c.charCodeAt(0);
+ })
+ ) + end;
}
function getCookie(name) {
@@ -82,21 +85,22 @@ var xsrf = $.param({_xsrf: getCookie("_xsrf")});
//Tornado XSRF Protection.
$.ajaxPrefilter(function (options) {
if (["post", "put", "delete"].indexOf(options.type.toLowerCase()) >= 0 && options.url[0] === "/") {
- if (options.data) {
- options.data += ("&" + xsrf);
+ if(options.url.indexOf("?") === -1){
+ options.url += "?" + xsrf;
} else {
- options.data = xsrf;
+ options.url += "&" + xsrf;
}
}
});
// Log AJAX Errors
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
- if(thrownError === "abort"){
+ if (thrownError === "abort") {
return;
}
var message = jqXHR.responseText;
console.error(thrownError, message, arguments);
actions.EventLogActions.add_event(thrownError + ": " + message);
+ alert(message);
});
module.exports = {
@@ -104,5 +108,5 @@ module.exports = {
formatTimeDelta: formatTimeDelta,
formatTimeStamp: formatTimeStamp,
reverseString: reverseString,
- Key: Key
+ Key: Key,
}; \ No newline at end of file