diff options
Diffstat (limited to 'libmproxy/web')
-rw-r--r-- | libmproxy/web/app.py | 51 | ||||
-rw-r--r-- | libmproxy/web/static/app.css | 17 | ||||
-rw-r--r-- | libmproxy/web/static/app.js | 401 |
3 files changed, 396 insertions, 73 deletions
diff --git a/libmproxy/web/app.py b/libmproxy/web/app.py index 8598acf5..76160dc1 100644 --- a/libmproxy/web/app.py +++ b/libmproxy/web/app.py @@ -26,6 +26,12 @@ class RequestHandler(tornado.web.RequestHandler): ) @property + def json(self): + if not self.request.headers.get("Content-Type").startswith("application/json"): + return None + return json.loads(self.request.body) + + @property def state(self): return self.application.master.state @@ -111,6 +117,35 @@ class FlowHandler(RequestHandler): self.flow.kill(self.master) 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): def post(self, flow_id): @@ -176,18 +211,12 @@ class Settings(RequestHandler): ) )) - def put(self, *update, **kwargs): + def put(self): update = {} - for k, v in self.request.arguments.iteritems(): - if len(v) != 1: - print "Warning: Unknown length for setting {}: {}".format(k, v) - continue - - if k == "_xsrf": - continue - elif k == "intercept": - self.state.set_intercept(v[0]) - update[k] = v[0] + for k, v in self.json.iteritems(): + if k == "intercept": + self.state.set_intercept(v) + update[k] = v else: print "Warning: Unknown setting {}: {}".format(k, v) diff --git a/libmproxy/web/static/app.css b/libmproxy/web/static/app.css index d767ac76..bc52381c 100644 --- a/libmproxy/web/static/app.css +++ b/libmproxy/web/static/app.css @@ -273,6 +273,10 @@ header .menu { width: 100%; overflow-x: auto; overflow-y: scroll; + /*.request .response-line, + .response .request-line { + opacity: 0.7; + }*/ } .flow-detail nav { background-color: #F2F2F2; @@ -291,9 +295,22 @@ header .menu { max-height: 100px; overflow-y: auto; } +.flow-detail .request-line { + margin-bottom: 2px; +} .flow-detail hr { 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 { margin-top: 10px; } diff --git a/libmproxy/web/static/app.js b/libmproxy/web/static/app.js index 83bb99ab..e972e298 100644 --- a/libmproxy/web/static/app.js +++ b/libmproxy/web/static/app.js @@ -303,6 +303,7 @@ function isUndefined(arg) { },{}],2:[function(require,module,exports){ var $ = require("jquery"); +var _ = require("lodash"); var AppDispatcher = require("./dispatcher.js").AppDispatcher; var ActionTypes = { @@ -348,7 +349,8 @@ var SettingsActions = { $.ajax({ type: "PUT", url: "/settings", - data: settings + contentType: 'application/json', + data: JSON.stringify(settings) }); /* @@ -399,15 +401,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); } }; @@ -427,7 +440,7 @@ module.exports = { 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 ReactRouter = require("react-router"); @@ -478,6 +491,13 @@ var StickyHeadMixin = { }; +var ChildFocus = { + contextTypes: { + returnFocus: React.PropTypes.func + } +}; + + var Navigation = _.extend({}, ReactRouter.Navigation, { setQuery: function (dict) { var q = this.context.router.getCurrentQuery(); @@ -624,6 +644,7 @@ var Splitter = React.createClass({displayName: "Splitter", }); module.exports = { + ChildFocus: ChildFocus, State: State, Navigation: Navigation, StickyHeadMixin: StickyHeadMixin, @@ -810,7 +831,7 @@ var TLSColumn = React.createClass({displayName: "TLSColumn", }, 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"; @@ -838,7 +859,7 @@ var IconColumn = React.createClass({displayName: "IconColumn", 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"; @@ -1224,7 +1245,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); @@ -1235,11 +1256,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)); @@ -1252,8 +1273,8 @@ var RawMixin = { this.requestContent(nextProps); } }, - componentWillUnmount: function(){ - if(this.state.request){ + componentWillUnmount: function () { + if (this.state.request) { this.state.request.abort(); } }, @@ -1291,7 +1312,7 @@ var ViewJSON = React.createClass({displayName: "ViewJSON", var json = this.state.content; try { json = JSON.stringify(JSON.parse(json), null, 2); - } catch(e) { + } catch (e) { } return React.createElement("pre", null, json); } @@ -1336,7 +1357,7 @@ var ContentMissing = React.createClass({displayName: "ContentMissing", var TooLarge = React.createClass({displayName: "TooLarge", statics: { - isTooLarge: function(message){ + isTooLarge: function (message) { var max_mb = ViewImage.matches(message) ? 10 : 0.2; 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("div", {className: "view-options text-center"}, 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){ 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"); @@ -1717,20 +1743,217 @@ var Headers = React.createClass({displayName: "Headers", } }); -var Request = React.createClass({displayName: "Request", +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 first_line = [ - flow.request.method, - flowutils.RequestUtils.pretty_url(flow.request), - "HTTP/" + flow.request.httpversion.join(".") - ].join(" "); + 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}) - //TODO: Styling + ); + }, + 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", + render: function () { + var flow = this.props.flow; return ( - React.createElement("section", null, - React.createElement("div", {className: "first-line"}, first_line ), + React.createElement("section", {className: "request"}, + React.createElement(RequestLine, {flow: flow}), + /*<ResponseLine flow={flow}/>*/ React.createElement(Headers, {message: flow.request}), React.createElement("hr", null), React.createElement(ContentView, {flow: flow, message: flow.request}) @@ -1742,17 +1965,10 @@ var Request = React.createClass({displayName: "Request", var Response = React.createClass({displayName: "Response", 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 ( - React.createElement("section", null, - React.createElement("div", {className: "first-line"}, first_line ), + React.createElement("section", {className: "response"}, + /*<RequestLine flow={flow}/>*/ + React.createElement(ResponseLine, {flow: flow}), React.createElement(Headers, {message: flow.response}), React.createElement("hr", null), React.createElement(ContentView, {flow: flow, message: flow.response}) @@ -1783,7 +1999,7 @@ module.exports = { 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 actions = require("../../actions.js"); @@ -2273,6 +2489,15 @@ var FlowView = require("./flowview/index.js"); var MainView = React.createClass({displayName: "MainView", 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: [], @@ -4522,9 +4747,17 @@ module.exports = (function() { 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. @@ -4557,25 +4790,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 @@ -4593,10 +4821,55 @@ 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 }; },{"jquery":"jquery","lodash":"lodash"}],22:[function(require,module,exports){ @@ -4903,6 +5176,10 @@ var $ = require("jquery"); var _ = require("lodash"); var actions = require("./actions.js"); +//debug +window.$ = $; +window._ = _; + var Key = { UP: 38, DOWN: 40, @@ -4929,17 +5206,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]; }; @@ -4961,17 +5238,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) { @@ -4983,21 +5259,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 = { @@ -5005,7 +5282,7 @@ module.exports = { formatTimeDelta: formatTimeDelta, formatTimeStamp: formatTimeStamp, reverseString: reverseString, - Key: Key + Key: Key, }; },{"./actions.js":2,"jquery":"jquery","lodash":"lodash"}]},{},[3]) |