diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 9db50cd76..644d46961 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -108,6 +108,12 @@ class HTTPMessage(stateobject.StateObject): ) _stateobject_long_attributes = {"content"} + def get_state(self, short=False): + ret = super(HTTPMessage, self).get_state(short) + if short: + ret["contentLength"] = len(self.content) + return ret + def get_decoded_content(self): """ Returns the decoded content based on the current Content-Encoding diff --git a/libmproxy/web/static/css/app.css b/libmproxy/web/static/css/app.css index 26ed8c3d2..ad2fe2e0d 100644 --- a/libmproxy/web/static/css/app.css +++ b/libmproxy/web/static/css/app.css @@ -180,10 +180,14 @@ header .menu { .flow-table .col-status { width: 50px; } +.flow-table .col-size { + width: 70px; +} .flow-table .col-time { width: 50px; } -.flow-table td.col-time { +.flow-table td.col-time, +.flow-table td.col-size { text-align: right; } .flow-detail { @@ -193,6 +197,29 @@ header .menu { .flow-detail nav { background-color: #F2F2F2; } +.flow-detail section { + padding: 5px; +} +.flow-detail code { + word-break: break-all; + padding-left: 0; +} +.header-table { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + width: 100%; + table-layout: fixed; + word-break: break-all; +} +.header-table tr { + border-top: 1px solid #f7f7f7; +} +.header-table td { + vertical-align: top; +} +.header-table .header-name { + width: 33%; + padding-right: 1em; +} .eventlog { flex: 0 0 auto; margin: 0; diff --git a/libmproxy/web/static/flows.json b/libmproxy/web/static/flows.json index ece178a7c..a0358db0f 100644 --- a/libmproxy/web/static/flows.json +++ b/libmproxy/web/static/flows.json @@ -1,6 +1,7 @@ [{ "id": "b5e5483c-e124-45bb-aa2e-360706e03ef4", "request": { + "contentLength": 0, "timestamp_end": 1410651311.107, "timestamp_start": 1410651311.106, "form_in": "relative", @@ -54,6 +55,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651311.055, "state": [], "timestamp_ssl_setup": 1410651311.096, @@ -77,6 +79,7 @@ "ssl_established": true }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651310.36, "address": { "use_ipv6": false, @@ -160,6 +163,7 @@ { "id": "85e9781f-d81d-43ca-a694-2cd86c76d991", "request": { + "contentLength": 42000, "timestamp_end": 1410651311.657, "timestamp_start": 1410651311.653, "form_in": "relative", @@ -217,6 +221,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651311.055, "state": [], "timestamp_ssl_setup": 1410651311.096, @@ -240,6 +245,7 @@ "ssl_established": true }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651310.36, "address": { "use_ipv6": false, @@ -323,6 +329,7 @@ { "id": "1bf281fd-e02a-423c-a69c-aa65657bc3dd", "request": { + "contentLength": 132121, "timestamp_end": 1410651312.362, "timestamp_start": 1410651312.359, "form_in": "relative", @@ -380,6 +387,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651312.303, "state": [], "timestamp_ssl_setup": 1410651312.349, @@ -403,6 +411,7 @@ "ssl_established": true }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651312.193, "address": { "use_ipv6": false, @@ -490,6 +499,7 @@ { "id": "833253a0-f7dd-48c7-893c-1f13a38a71ce", "request": { + "contentLength": 13233121, "timestamp_end": 1410651312.389, "timestamp_start": 1410651312.368, "form_in": "relative", @@ -547,6 +557,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651312.307, "state": [], "timestamp_ssl_setup": 1410651312.355, @@ -570,6 +581,7 @@ "ssl_established": true }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651312.2, "address": { "use_ipv6": false, @@ -657,6 +669,7 @@ { "id": "152d8e71-2469-4034-8d6d-11099bbb4248", "request": { + "contentLength": 132121231231, "timestamp_end": 1410651312.386, "timestamp_start": 1410651312.368, "form_in": "relative", @@ -714,6 +727,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651312.303, "state": [], "timestamp_ssl_setup": 1410651312.355, @@ -737,6 +751,7 @@ "ssl_established": true }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651312.192, "address": { "use_ipv6": false, @@ -824,6 +839,7 @@ { "id": "b3758e4d-7bae-4771-b154-e100c0722d00", "request": { + "contentLength" : 54321, "timestamp_end": 1410651373.965, "timestamp_start": 1410651373.963, "form_in": "absolute", @@ -865,6 +881,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.189, "state": [], "timestamp_ssl_setup": null, @@ -888,6 +905,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651373.958, "address": { "use_ipv6": false, @@ -955,6 +973,7 @@ { "id": "ea9e47ab-fd7b-4463-bfea-cfd64cc5f78d", "request": { + "contentLength" : 54321, "timestamp_end": 1410651374.391, "timestamp_start": 1410651374.387, "form_in": "absolute", @@ -1000,6 +1019,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.189, "state": [], "timestamp_ssl_setup": null, @@ -1023,6 +1043,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651373.958, "address": { "use_ipv6": false, @@ -1090,6 +1111,7 @@ { "id": "13ee4cd1-08e0-43ef-9bee-56fc0d9cbf3f", "request": { + "contentLength" : 54321, "timestamp_end": 1410651374.396, "timestamp_start": 1410651374.394, "form_in": "absolute", @@ -1135,6 +1157,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.567, "state": [], "timestamp_ssl_setup": null, @@ -1158,6 +1181,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651374.389, "address": { "use_ipv6": false, @@ -1221,6 +1245,7 @@ { "id": "5c50e1fc-5ac4-4748-aed1-c969ede63e4e", "request": { + "contentLength" : 54321, "timestamp_end": 1410651374.795, "timestamp_start": 1410651374.793, "form_in": "absolute", @@ -1266,6 +1291,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.99, "state": [], "timestamp_ssl_setup": null, @@ -1289,6 +1315,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651374.389, "address": { "use_ipv6": false, @@ -1372,6 +1399,7 @@ { "id": "0285a0b2-380e-43eb-a7a9-a18893950216", "request": { + "contentLength" : 54321, "timestamp_end": 1410651375.084, "timestamp_start": 1410651375.078, "form_in": "absolute", @@ -1417,6 +1445,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.99, "state": [], "timestamp_ssl_setup": null, @@ -1440,6 +1469,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651374.389, "address": { "use_ipv6": false, @@ -1519,6 +1549,7 @@ { "id": "c9af9c71-dc68-462e-8446-f3a4b2782400", "request": { + "contentLength" : 54321, "timestamp_end": 1410651374.778, "timestamp_start": 1410651374.766, "form_in": "absolute", @@ -1564,6 +1595,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.952, "state": [], "timestamp_ssl_setup": null, @@ -1587,6 +1619,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651374.39, "address": { "use_ipv6": false, @@ -1650,6 +1683,7 @@ { "id": "310386ab-3ae1-4129-9a2e-8dd2ce60ecdb", "request": { + "contentLength" : 54321, "timestamp_end": 1410651374.778, "timestamp_start": 1410651374.766, "form_in": "absolute", @@ -1695,6 +1729,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.189, "state": [], "timestamp_ssl_setup": null, @@ -1718,6 +1753,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651373.958, "address": { "use_ipv6": false, @@ -1781,6 +1817,7 @@ { "id": "b92e5f6e-bb0f-4e47-a50c-ef4072ea40b3", "request": { + "contentLength" : 54321, "timestamp_end": 1410651376.078, "timestamp_start": 1410651376.075, "form_in": "absolute", @@ -1826,6 +1863,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.189, "state": [], "timestamp_ssl_setup": null, @@ -1849,6 +1887,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651373.958, "address": { "use_ipv6": false, @@ -1904,6 +1943,7 @@ { "id": "597d086f-d836-49e3-85bb-77a983bed87f", "request": { + "contentLength" : 54321, "timestamp_end": 1410651376.282, "timestamp_start": 1410651376.279, "form_in": "absolute", @@ -1949,6 +1989,7 @@ ] }, "server_conn": { + "contentLength" : 54321, "timestamp_tcp_setup": 1410651374.189, "state": [], "timestamp_ssl_setup": null, @@ -1972,6 +2013,7 @@ "ssl_established": false }, "client_conn": { + "contentLength" : 54321, "timestamp_start": 1410651373.958, "address": { "use_ipv6": false, diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 8ee7133ee..4c47e201a 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -13,11 +13,11 @@ var AutoScrollMixin = { }; var StickyHeadMixin = { - adjustHead: function(){ + adjustHead: function () { // Abusing CSS transforms to set the element // referenced as head into some kind of position:sticky. var head = this.refs.head.getDOMNode(); - head.style.transform = "translate(0,"+this.getDOMNode().scrollTop+"px)"; + head.style.transform = "translate(0," + this.getDOMNode().scrollTop + "px)"; } }; @@ -31,6 +31,15 @@ var Key = { ENTER: 13, ESC: 27 }; + +var formatSize = function (size) { + var prefix = ["B", "KB", "MB", "GB", "TB"]; + while (size >= 1024 && prefix.length > 1) { + prefix.shift(); + size = size / 1024; + } + return (Math.floor(size * 100) / 100.0) + prefix.shift(); +}; const PayloadSources = { VIEW: "view", SERVER: "server" @@ -104,6 +113,53 @@ var EventLogActions = { }); } }; +var _MessageUtils = { + getContentType: function (message) { + return MessageUtils.getHeader(message, /Content-Type/i); + }, + get_first_header: function (message, regex) { + //FIXME: Cache Invalidation. + if (!message._headerLookups) + Object.defineProperty(message, "_headerLookups", { + value: {}, + configurable: false, + enumerable: false, + writable: false + }); + if (!(regex in message._headerLookups)) { + var header; + for (var i = 0; i < message.headers.length; i++) { + if (!!message.headers[i][0].match(regex)) { + header = message.headers[i]; + break; + } + } + message._headerLookups[regex] = header ? header[1] : undefined; + } + return message._headerLookups[regex]; + } +}; + +var defaultPorts = { + "http": 80, + "https": 443 +}; + +var RequestUtils = _.extend(_MessageUtils, { + pretty_host: function (request) { + //FIXME: Add hostheader + return request.host; + }, + pretty_url: function (request) { + var port = ""; + if (defaultPorts[request.scheme] !== request.port) { + port = ":" + request.port; + } + return request.scheme + "://" + this.pretty_host(request) + port + request.path; + } +}); + +var ResponseUtils = _.extend(_MessageUtils, {}); function EventEmitter() { this.listeners = {}; } @@ -654,6 +710,22 @@ var StatusColumn = React.createClass({displayName: 'StatusColumn', }); +var SizeColumn = React.createClass({displayName: 'SizeColumn', + statics: { + renderTitle: function(){ + return React.DOM.th({key: "size", className: "col-size"}, "Size"); + } + }, + render: function(){ + var flow = this.props.flow; + var size = formatSize( + flow.request.contentLength + + (flow.response.contentLength || 0)); + return React.DOM.td({className: "col-size"}, size); + } +}); + + var TimeColumn = React.createClass({displayName: 'TimeColumn', statics: { renderTitle: function(){ @@ -673,7 +745,14 @@ var TimeColumn = React.createClass({displayName: 'TimeColumn', }); -var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, TimeColumn]; +var all_columns = [ + TLSColumn, + IconColumn, + PathColumn, + MethodColumn, + StatusColumn, + SizeColumn, + TimeColumn]; /** @jsx React.DOM */ @@ -829,21 +908,59 @@ var FlowDetailNav = React.createClass({displayName: 'FlowDetailNav', } }); +var Headers = React.createClass({displayName: 'Headers', + render: function(){ + var rows = this.props.message.headers.map(function(header){ + return ( + React.DOM.tr(null, + React.DOM.td({className: "header-name"}, header[0]), + React.DOM.td({className: "header-value"}, header[1]) + ) + ); + }) + return ( + React.DOM.table({className: "header-table"}, + React.DOM.tbody(null, + rows + ) + ) + ); + } +}) + var FlowDetailRequest = React.createClass({displayName: 'FlowDetailRequest', render: function(){ - return React.DOM.div(null, "request"); + var flow = this.props.flow; + var url = React.DOM.code(null, RequestUtils.pretty_url(flow.request) ); + var content = null; + if(flow.request.contentLength > 0){ + content = "Request Content Size: "+ formatSize(flow.request.contentLength); + } else { + content = React.DOM.div({className: "alert alert-info"}, "No Content"); + } + + //TODO: Styling + + return ( + React.DOM.section(null, + url, + Headers({message: flow.request}), + React.DOM.hr(null), + content + ) + ); } }); var FlowDetailResponse = React.createClass({displayName: 'FlowDetailResponse', render: function(){ - return React.DOM.div(null, "response"); + return React.DOM.section(null, "response"); } }); var FlowDetailConnectionInfo = React.createClass({displayName: 'FlowDetailConnectionInfo', render: function(){ - return React.DOM.div(null, "details"); + return React.DOM.section(null, "details"); } }); @@ -860,8 +977,8 @@ var FlowDetail = React.createClass({displayName: 'FlowDetail', var Tab = tabs[this.props.active]; return ( React.DOM.div({className: "flow-detail", onScroll: this.adjustHead}, - FlowDetailNav({active: this.props.active, selectTab: this.props.selectTab}), - Tab(null) + FlowDetailNav({ref: "head", active: this.props.active, selectTab: this.props.selectTab}), + Tab({flow: this.props.flow}) ) ); } diff --git a/web/gulpfile.js b/web/gulpfile.js index 584cc7d38..d106ecd16 100644 --- a/web/gulpfile.js +++ b/web/gulpfile.js @@ -36,6 +36,7 @@ var path = { 'js/utils.js', 'js/dispatcher.js', 'js/actions.js', + 'js/flow/utils.js', 'js/stores/base.js', 'js/stores/settingstore.js', 'js/stores/eventlogstore.js', diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index 883343914..8501ce6c2 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -5,4 +5,48 @@ nav { background-color: #F2F2F2; } + + section { + padding: 5px; + } + + //FIXME: Style properly + code { + word-break: break-all; + padding-left: 0; + } +} + +//TODO: Move into some utils +.monospace(){ + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} + +.header-table { + .monospace(); + width: 100%; + table-layout: fixed; + word-break: break-all; + + tr { + //&:not(:first-child){ + border-top: 1px solid #f7f7f7; + //} + } + + td { + vertical-align: top; + //alt: + //white-space: nowrap; + //overflow: hidden; + //text-overflow: ellipsis; + } + + .header-name { + width: 33%; + padding-right: 1em; + } + .header-value { + + } } \ No newline at end of file diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less index 2a9c318bd..db4c6f126 100644 --- a/web/src/css/flowtable.less +++ b/web/src/css/flowtable.less @@ -51,10 +51,13 @@ .col-status { width: 50px; } + .col-size { + width: 70px; + } .col-time { width: 50px; } - td.col-time { + td.col-time, td.col-size { text-align: right; } } \ No newline at end of file diff --git a/web/src/js/components/flowdetail.jsx.js b/web/src/js/components/flowdetail.jsx.js index acf5e6f4f..744901be6 100644 --- a/web/src/js/components/flowdetail.jsx.js +++ b/web/src/js/components/flowdetail.jsx.js @@ -23,21 +23,59 @@ var FlowDetailNav = React.createClass({ } }); +var Headers = React.createClass({ + render: function(){ + var rows = this.props.message.headers.map(function(header){ + return ( + + {header[0]} + {header[1]} + + ); + }) + return ( + + + {rows} + +
+ ); + } +}) + var FlowDetailRequest = React.createClass({ render: function(){ - return
request
; + var flow = this.props.flow; + var url = { RequestUtils.pretty_url(flow.request) }; + var content = null; + if(flow.request.contentLength > 0){ + content = "Request Content Size: "+ formatSize(flow.request.contentLength); + } else { + content =
No Content
; + } + + //TODO: Styling + + return ( +
+ {url} + +
+ {content} +
+ ); } }); var FlowDetailResponse = React.createClass({ render: function(){ - return
response
; + return
response
; } }); var FlowDetailConnectionInfo = React.createClass({ render: function(){ - return
details
; + return
details
; } }); @@ -54,8 +92,8 @@ var FlowDetail = React.createClass({ var Tab = tabs[this.props.active]; return (
- - + +
); } diff --git a/web/src/js/components/flowtable-columns.jsx.js b/web/src/js/components/flowtable-columns.jsx.js index 0eb9966c7..01130bc14 100644 --- a/web/src/js/components/flowtable-columns.jsx.js +++ b/web/src/js/components/flowtable-columns.jsx.js @@ -77,6 +77,22 @@ var StatusColumn = React.createClass({ }); +var SizeColumn = React.createClass({ + statics: { + renderTitle: function(){ + return Size; + } + }, + render: function(){ + var flow = this.props.flow; + var size = formatSize( + flow.request.contentLength + + (flow.response.contentLength || 0)); + return {size}; + } +}); + + var TimeColumn = React.createClass({ statics: { renderTitle: function(){ @@ -96,5 +112,12 @@ var TimeColumn = React.createClass({ }); -var all_columns = [TLSColumn, IconColumn, PathColumn, MethodColumn, StatusColumn, TimeColumn]; +var all_columns = [ + TLSColumn, + IconColumn, + PathColumn, + MethodColumn, + StatusColumn, + SizeColumn, + TimeColumn]; diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js new file mode 100644 index 000000000..b621f06d6 --- /dev/null +++ b/web/src/js/flow/utils.js @@ -0,0 +1,47 @@ +var _MessageUtils = { + getContentType: function (message) { + return MessageUtils.getHeader(message, /Content-Type/i); + }, + get_first_header: function (message, regex) { + //FIXME: Cache Invalidation. + if (!message._headerLookups) + Object.defineProperty(message, "_headerLookups", { + value: {}, + configurable: false, + enumerable: false, + writable: false + }); + if (!(regex in message._headerLookups)) { + var header; + for (var i = 0; i < message.headers.length; i++) { + if (!!message.headers[i][0].match(regex)) { + header = message.headers[i]; + break; + } + } + message._headerLookups[regex] = header ? header[1] : undefined; + } + return message._headerLookups[regex]; + } +}; + +var defaultPorts = { + "http": 80, + "https": 443 +}; + +var RequestUtils = _.extend(_MessageUtils, { + pretty_host: function (request) { + //FIXME: Add hostheader + return request.host; + }, + pretty_url: function (request) { + var port = ""; + if (defaultPorts[request.scheme] !== request.port) { + port = ":" + request.port; + } + return request.scheme + "://" + this.pretty_host(request) + port + request.path; + } +}); + +var ResponseUtils = _.extend(_MessageUtils, {}); \ No newline at end of file diff --git a/web/src/js/utils.js b/web/src/js/utils.js index 947ad5c1e..e53097f80 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -13,11 +13,11 @@ var AutoScrollMixin = { }; var StickyHeadMixin = { - adjustHead: function(){ + adjustHead: function () { // Abusing CSS transforms to set the element // referenced as head into some kind of position:sticky. var head = this.refs.head.getDOMNode(); - head.style.transform = "translate(0,"+this.getDOMNode().scrollTop+"px)"; + head.style.transform = "translate(0," + this.getDOMNode().scrollTop + "px)"; } }; @@ -30,4 +30,13 @@ var Key = { RIGHT: 39, ENTER: 13, ESC: 27 +}; + +var formatSize = function (size) { + var prefix = ["B", "KB", "MB", "GB", "TB"]; + while (size >= 1024 && prefix.length > 1) { + prefix.shift(); + size = size / 1024; + } + return (Math.floor(size * 100) / 100.0) + prefix.shift(); }; \ No newline at end of file