diff --git a/libmproxy/web/static/css/app.css b/libmproxy/web/static/css/app.css index 2459f406d..f0ed95dd5 100644 --- a/libmproxy/web/static/css/app.css +++ b/libmproxy/web/static/css/app.css @@ -148,9 +148,6 @@ header .menu { font-weight: normal; box-shadow: 0 1px 0 #a6a6a6; } -.flow-table tbody { - outline: 0; -} .flow-table tr { cursor: pointer; } @@ -198,11 +195,16 @@ header .menu { background-color: #F2F2F2; } .flow-detail section { - padding: 5px; + padding: 5px 12px; } -.flow-detail code { +.flow-detail .first-line { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + background-color: #428bca; + color: white; + margin: 0 -8px; + padding: 4px 8px; + border-radius: 5px; word-break: break-all; - padding-left: 0; } .flow-detail table { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index b86e462a0..5d40f069a 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -41,7 +41,7 @@ var Key = { var formatSize = function (bytes) { var size = bytes; var prefix = ["B", "KB", "MB", "GB", "TB"]; - while (size >= 1024 && prefix.length > 1) { + while (Math.abs(size) >= 1024 && prefix.length > 1) { prefix.shift(); size = size / 1024; } @@ -50,9 +50,9 @@ var formatSize = function (bytes) { var formatTimeDelta = function (milliseconds) { var time = milliseconds; - var prefix = ["ms", "s", "m", "h"]; + var prefix = ["ms", "s", "min", "h"]; var div = [1000, 60, 60]; - while (time >= div[0] && prefix.length > 1) { + while (Math.abs(time) >= div[0] && prefix.length > 1) { prefix.shift(); time = time / div.shift(); } @@ -617,12 +617,12 @@ var Header = React.createClass({displayName: 'Header', console.log("File click"); }, render: function () { - var header = header_entries.map(function(entry){ + var header = header_entries.map(function(entry, i){ var classes = React.addons.classSet({ active: entry == this.state.active }); return ( - React.DOM.a({key: entry.title, + React.DOM.a({key: i, href: "#", className: classes, onClick: this.handleClick.bind(this, entry) @@ -685,7 +685,6 @@ var IconColumn = React.createClass({displayName: 'IconColumn', var contentType = ResponseUtils.getContentType(flow.response); //TODO: We should assign a type to the flow somewhere else. - var icon; if(flow.response.code == 304) { icon = "resource-icon-not-modified"; } else if(300 <= flow.response.code && flow.response.code < 400) { @@ -763,9 +762,10 @@ var SizeColumn = React.createClass({displayName: 'SizeColumn', }, render: function(){ var flow = this.props.flow; + var total = flow.request.contentLength; if(flow.response){ - total += flow.response.contentLength; + total += flow.response.contentLength || 0; } var size = formatSize(total); return React.DOM.td({className: "col-size"}, size); @@ -917,9 +917,9 @@ var FlowDetailNav = React.createClass({displayName: 'FlowDetailNav', var Headers = React.createClass({displayName: 'Headers', render: function(){ - var rows = this.props.message.headers.map(function(header){ + var rows = this.props.message.headers.map(function(header, i){ return ( - React.DOM.tr(null, + React.DOM.tr({key: i}, React.DOM.td({className: "header-name"}, header[0]+":"), React.DOM.td({className: "header-value"}, header[1]) ) @@ -954,7 +954,7 @@ var FlowDetailRequest = React.createClass({displayName: 'FlowDetailRequest', return ( React.DOM.section(null, - React.DOM.code(null, first_line ), + React.DOM.div({className: "first-line"}, first_line ), Headers({message: flow.request}), React.DOM.hr(null), content @@ -982,7 +982,7 @@ var FlowDetailResponse = React.createClass({displayName: 'FlowDetailResponse', return ( React.DOM.section(null, - React.DOM.code(null, first_line ), + React.DOM.div({className: "first-line"}, first_line ), Headers({message: flow.response}), React.DOM.hr(null), content @@ -993,23 +993,21 @@ var FlowDetailResponse = React.createClass({displayName: 'FlowDetailResponse', var TimeStamp = React.createClass({displayName: 'TimeStamp', render: function() { - var ts, delta; - if(!this.props.t && this.props.optional){ + if(!this.props.t){ //should be return null, but that triggers a React bug. return React.DOM.tr(null); - } else if (!this.props.t){ - ts = "active"; - } else { - ts = (new Date(this.props.t * 1000)).toISOString(); - ts = ts.replace("T", " ").replace("Z",""); + } - if(this.props.deltaTo){ - delta = Math.round((this.props.t-this.props.deltaTo)*1000) + "ms"; - delta = React.DOM.span({className: "text-muted"}, "(" + delta + ")"); - } else { - delta = null; - } + var ts = (new Date(this.props.t * 1000)).toISOString(); + ts = ts.replace("T", " ").replace("Z",""); + + var delta; + if(this.props.deltaTo){ + delta = formatTimeDelta(1000 * (this.props.t-this.props.deltaTo)); + delta = React.DOM.span({className: "text-muted"}, "(" + delta + ")"); + } else { + delta = null; } return React.DOM.tr(null, React.DOM.td(null, this.props.title + ":"), React.DOM.td(null, ts, " ", delta)); @@ -1030,24 +1028,7 @@ var ConnectionInfo = React.createClass({displayName: 'ConnectionInfo', React.DOM.table({className: "connection-table"}, React.DOM.tbody(null, React.DOM.tr({key: "address"}, React.DOM.td(null, "Address:"), React.DOM.td(null, address)), - sni, - TimeStamp({title: "Start time", - key: "start", - t: conn.timestamp_start}), - TimeStamp({title: "TCP Setup", - key: "tcpsetup", - t: conn.timestamp_tcp_setup, - deltaTo: conn.timestamp_start, - optional: true}), - TimeStamp({title: "SSL handshake", - key: "sslsetup", - t: conn.timestamp_ssl_setup, - deltaTo: conn.timestamp_start, - optional: true}), - TimeStamp({title: "End time", - key: "end", - t: conn.timestamp_end, - deltaTo: conn.timestamp_start}) + sni ) ) ); @@ -1061,13 +1042,92 @@ var CertificateInfo = React.createClass({displayName: 'CertificateInfo', var flow = this.props.flow; var client_conn = flow.client_conn; var server_conn = flow.server_conn; + + var preStyle = {maxHeight: 100}; return ( React.DOM.div(null, client_conn.cert ? React.DOM.h4(null, "Client Certificate") : null, - client_conn.cert ? React.DOM.pre(null, client_conn.cert) : null, + client_conn.cert ? React.DOM.pre({style: preStyle}, client_conn.cert) : null, server_conn.cert ? React.DOM.h4(null, "Server Certificate") : null, - server_conn.cert ? React.DOM.pre(null, server_conn.cert) : null + server_conn.cert ? React.DOM.pre({style: preStyle}, server_conn.cert) : null + ) + ); + } +}); + +var Timing = React.createClass({displayName: 'Timing', + render: function(){ + var flow = this.props.flow; + var sc = flow.server_conn; + var cc = flow.client_conn; + var req = flow.request; + var resp = flow.response; + + var timestamps = [ + { + title: "Server conn. initiated", + t: sc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Server conn. TCP handshake", + t: sc.timestamp_tcp_setup, + deltaTo: req.timestamp_start + }, { + title: "Server conn. SSL handshake", + t: sc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "Client conn. established", + t: cc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Client conn. SSL handshake", + t: cc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "First request byte", + t: req.timestamp_start, + }, { + title: "Request complete", + t: req.timestamp_end, + deltaTo: req.timestamp_start + } + ]; + + if (flow.response) { + timestamps.push( + { + title: "First response byte", + t: resp.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Response complete", + t: resp.timestamp_end, + deltaTo: req.timestamp_start + } + ); + } + + //Add unique key for each row. + timestamps.forEach(function(e){ + e.key = e.title; + }); + + timestamps = _.sortBy(timestamps, 't'); + + var rows = timestamps.map(function(e){ + return TimeStamp(e); + }); + + return ( + React.DOM.div(null, + React.DOM.h4(null, "Timing"), + React.DOM.table(null, + React.DOM.tbody(null, + rows + ) + ) ) ); } @@ -1087,7 +1147,9 @@ var FlowDetailConnectionInfo = React.createClass({displayName: 'FlowDetailConnec React.DOM.h4(null, "Server Connection"), ConnectionInfo({conn: server_conn}), - CertificateInfo({flow: flow}) + CertificateInfo({flow: flow}), + + Timing({flow: flow}) ) ); diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index 5e27e7e6c..94920caa8 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -1,3 +1,9 @@ +//TODO: Move into some utils +.monospace(){ + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} + + .flow-detail { width: 100%; overflow: auto; @@ -7,20 +13,20 @@ } section { - padding: 5px; + padding: 5px 12px; } - //FIXME: Style properly - code { + .first-line { + .monospace(); + background-color: #428bca; + color: white; + margin: 0 -8px; + padding: 4px 8px; + border-radius: 5px; word-break: break-all; - padding-left: 0; } } -//TODO: Move into some utils -.monospace(){ - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; -} .flow-detail table { .monospace(); diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less index db4c6f126..64fd86b90 100644 --- a/web/src/css/flowtable.less +++ b/web/src/css/flowtable.less @@ -17,10 +17,6 @@ box-shadow: 0 1px 0 #a6a6a6; } - tbody { - outline: 0; - } - tr { cursor: pointer; &.selected { diff --git a/web/src/js/components/flowdetail.jsx.js b/web/src/js/components/flowdetail.jsx.js index 7c9841930..ad1cfe679 100644 --- a/web/src/js/components/flowdetail.jsx.js +++ b/web/src/js/components/flowdetail.jsx.js @@ -25,9 +25,9 @@ var FlowDetailNav = React.createClass({ var Headers = React.createClass({ render: function(){ - var rows = this.props.message.headers.map(function(header){ + var rows = this.props.message.headers.map(function(header, i){ return ( - + {header[0]+":"} {header[1]} @@ -62,7 +62,7 @@ var FlowDetailRequest = React.createClass({ return (
- { first_line } +
{ first_line }

{content} @@ -90,7 +90,7 @@ var FlowDetailResponse = React.createClass({ return (
- { first_line } +
{ first_line }

{content} @@ -101,23 +101,21 @@ var FlowDetailResponse = React.createClass({ var TimeStamp = React.createClass({ render: function() { - var ts, delta; - if(!this.props.t && this.props.optional){ + if(!this.props.t){ //should be return null, but that triggers a React bug. return ; - } else if (!this.props.t){ - ts = "active"; - } else { - ts = (new Date(this.props.t * 1000)).toISOString(); - ts = ts.replace("T", " ").replace("Z",""); + } - if(this.props.deltaTo){ - delta = Math.round((this.props.t-this.props.deltaTo)*1000) + "ms"; - delta = {"(" + delta + ")"}; - } else { - delta = null; - } + var ts = (new Date(this.props.t * 1000)).toISOString(); + ts = ts.replace("T", " ").replace("Z",""); + + var delta; + if(this.props.deltaTo){ + delta = formatTimeDelta(1000 * (this.props.t-this.props.deltaTo)); + delta = {"(" + delta + ")"}; + } else { + delta = null; } return {this.props.title + ":"}{ts} {delta}; @@ -139,23 +137,6 @@ var ConnectionInfo = React.createClass({ Address:{address} {sni} - - - - ); @@ -169,13 +150,92 @@ var CertificateInfo = React.createClass({ var flow = this.props.flow; var client_conn = flow.client_conn; var server_conn = flow.server_conn; + + var preStyle = {maxHeight: 100}; return (
{client_conn.cert ?

Client Certificate

: null} - {client_conn.cert ?
{client_conn.cert}
: null} + {client_conn.cert ?
{client_conn.cert}
: null} {server_conn.cert ?

Server Certificate

: null} - {server_conn.cert ?
{server_conn.cert}
: null} + {server_conn.cert ?
{server_conn.cert}
: null} +
+ ); + } +}); + +var Timing = React.createClass({ + render: function(){ + var flow = this.props.flow; + var sc = flow.server_conn; + var cc = flow.client_conn; + var req = flow.request; + var resp = flow.response; + + var timestamps = [ + { + title: "Server conn. initiated", + t: sc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Server conn. TCP handshake", + t: sc.timestamp_tcp_setup, + deltaTo: req.timestamp_start + }, { + title: "Server conn. SSL handshake", + t: sc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "Client conn. established", + t: cc.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Client conn. SSL handshake", + t: cc.timestamp_ssl_setup, + deltaTo: req.timestamp_start + }, { + title: "First request byte", + t: req.timestamp_start, + }, { + title: "Request complete", + t: req.timestamp_end, + deltaTo: req.timestamp_start + } + ]; + + if (flow.response) { + timestamps.push( + { + title: "First response byte", + t: resp.timestamp_start, + deltaTo: req.timestamp_start + }, { + title: "Response complete", + t: resp.timestamp_end, + deltaTo: req.timestamp_start + } + ); + } + + //Add unique key for each row. + timestamps.forEach(function(e){ + e.key = e.title; + }); + + timestamps = _.sortBy(timestamps, 't'); + + var rows = timestamps.map(function(e){ + return TimeStamp(e); + }); + + return ( +
+

Timing

+ + + {rows} + +
); } @@ -197,6 +257,8 @@ var FlowDetailConnectionInfo = React.createClass({ + +
); } diff --git a/web/src/js/components/flowtable-columns.jsx.js b/web/src/js/components/flowtable-columns.jsx.js index ec63b03fc..88e0cf223 100644 --- a/web/src/js/components/flowtable-columns.jsx.js +++ b/web/src/js/components/flowtable-columns.jsx.js @@ -34,7 +34,6 @@ var IconColumn = React.createClass({ var contentType = ResponseUtils.getContentType(flow.response); //TODO: We should assign a type to the flow somewhere else. - var icon; if(flow.response.code == 304) { icon = "resource-icon-not-modified"; } else if(300 <= flow.response.code && flow.response.code < 400) { @@ -112,9 +111,10 @@ var SizeColumn = React.createClass({ }, render: function(){ var flow = this.props.flow; + var total = flow.request.contentLength; if(flow.response){ - total += flow.response.contentLength; + total += flow.response.contentLength || 0; } var size = formatSize(total); return {size}; diff --git a/web/src/js/components/header.jsx.js b/web/src/js/components/header.jsx.js index 92a58282a..994bc7593 100644 --- a/web/src/js/components/header.jsx.js +++ b/web/src/js/components/header.jsx.js @@ -62,12 +62,12 @@ var Header = React.createClass({ console.log("File click"); }, render: function () { - var header = header_entries.map(function(entry){ + var header = header_entries.map(function(entry, i){ var classes = React.addons.classSet({ active: entry == this.state.active }); return ( - = 1024 && prefix.length > 1) { + while (Math.abs(size) >= 1024 && prefix.length > 1) { prefix.shift(); size = size / 1024; } @@ -50,9 +50,9 @@ var formatSize = function (bytes) { var formatTimeDelta = function (milliseconds) { var time = milliseconds; - var prefix = ["ms", "s", "m", "h"]; + var prefix = ["ms", "s", "min", "h"]; var div = [1000, 60, 60]; - while (time >= div[0] && prefix.length > 1) { + while (Math.abs(time) >= div[0] && prefix.length > 1) { prefix.shift(); time = time / div.shift(); }