diff options
Diffstat (limited to 'web/src')
-rw-r--r-- | web/src/css/app.less | 1 | ||||
-rw-r--r-- | web/src/css/eventlog.less | 2 | ||||
-rw-r--r-- | web/src/css/flowdetail.less | 24 | ||||
-rw-r--r-- | web/src/css/flowtable.less | 208 | ||||
-rw-r--r-- | web/src/css/flowview.less | 9 | ||||
-rw-r--r-- | web/src/css/header.less | 32 | ||||
-rw-r--r-- | web/src/css/layout.less | 2 | ||||
-rw-r--r-- | web/src/css/sprites.less | 28 | ||||
-rw-r--r-- | web/src/css/vendor-bootstrap-variables.less | 11 | ||||
-rw-r--r-- | web/src/css/vendor-bootstrap.less | 5 | ||||
-rw-r--r-- | web/src/js/actions.js | 1 | ||||
-rw-r--r-- | web/src/js/components/eventlog.js | 1 | ||||
-rw-r--r-- | web/src/js/components/flowdetail.js | 399 | ||||
-rw-r--r-- | web/src/js/components/flowview/contentview.js | 158 | ||||
-rw-r--r-- | web/src/js/components/flowview/details.js | 181 | ||||
-rw-r--r-- | web/src/js/components/flowview/index.js | 74 | ||||
-rw-r--r-- | web/src/js/components/flowview/messages.js | 91 | ||||
-rw-r--r-- | web/src/js/components/flowview/nav.js | 61 | ||||
-rw-r--r-- | web/src/js/components/header.js | 2 | ||||
-rw-r--r-- | web/src/js/components/mainview.js | 6 | ||||
-rw-r--r-- | web/src/js/flow/utils.js | 12 | ||||
-rw-r--r-- | web/src/js/store/view.js | 8 | ||||
-rw-r--r-- | web/src/js/utils.js | 1 |
23 files changed, 753 insertions, 564 deletions
diff --git a/web/src/css/app.less b/web/src/css/app.less index 26f22572..ecec3d9c 100644 --- a/web/src/css/app.less +++ b/web/src/css/app.less @@ -13,5 +13,6 @@ html { @import (less) "header.less"; @import (less) "flowtable.less"; @import (less) "flowdetail.less"; +@import (less) "flowview.less"; @import (less) "eventlog.less"; @import (less) "footer.less";
\ No newline at end of file diff --git a/web/src/css/eventlog.less b/web/src/css/eventlog.less index 8b0a7647..26dea3cc 100644 --- a/web/src/css/eventlog.less +++ b/web/src/css/eventlog.less @@ -6,7 +6,6 @@ display: flex; flex-direction: column; - > div { background-color: #F2F2F2; padding: 0 5px; @@ -23,7 +22,6 @@ background-color: #fcfcfc; } - .fa-close { cursor: pointer; float: right; diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index 7649057f..453cf425 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -1,13 +1,12 @@ //TODO: Move into some utils -.monospace(){ +.monospace() { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; } - .flow-detail { width: 100%; overflow: auto; - + nav { background-color: #F2F2F2; } @@ -27,18 +26,26 @@ max-height: 100px; overflow-y: auto; } + + hr { + margin: 0 0 5px; + } + } +.view-selector { + margin-top: 10px; +} .flow-detail table { .monospace(); width: 100%; table-layout: fixed; word-break: break-all; - + tr { - &:not(:first-child){ - border-top: 1px solid #f7f7f7; + &:not(:first-child) { + border-top: 1px solid #f7f7f7; } } @@ -59,12 +66,15 @@ } .header-table { + td { + line-height: 1.3em; + } .header-name { width: 33%; padding-right: 1em; } .header-value { - + } } diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less index d94d9370..3533983c 100644 --- a/web/src/css/flowtable.less +++ b/web/src/css/flowtable.less @@ -1,127 +1,127 @@ //TODO: move into utils .user-select (@val) { - -webkit-touch-callout: @val; - -webkit-user-select: @val; - -khtml-user-select: @val; - -moz-user-select: @val; - -ms-user-select: @val; - user-select: @val; + -webkit-touch-callout: @val; + -webkit-user-select: @val; + -khtml-user-select: @val; + -moz-user-select: @val; + -ms-user-select: @val; + user-select: @val; } .flow-table { - width: 100%; - overflow: auto; - - table { width: 100%; - table-layout: fixed; - } - - thead { - background-color: #F2F2F2; - line-height: 23px; - } - - th { - font-weight: normal; - box-shadow: 0 1px 0 #a6a6a6; - position: relative !important; - padding-left: 1px; - .user-select(none); + overflow: auto; - &.sort-asc, &.sort-desc { - background-color: lighten(#F2F2F2, 3%); - } - &.sort-asc:after, &.sort-desc:after { - font: normal normal normal 14px/1 FontAwesome; - position: absolute; - right: 3px; - top: 3px; - padding: 2px; - background-color: fadeout(lighten(#F2F2F2, 3%), 20%); - } - &.sort-asc:after { - content: "\f0de"; + table { + width: 100%; + table-layout: fixed; } - &.sort-desc:after { - content: "\f0dd"; + + thead { + background-color: #F2F2F2; + line-height: 23px; } - } + th { + font-weight: normal; + box-shadow: 0 1px 0 #a6a6a6; + position: relative !important; + padding-left: 1px; + .user-select(none); - tr { - cursor: pointer; + &.sort-asc, &.sort-desc { + background-color: lighten(#F2F2F2, 3%); + } + &.sort-asc:after, &.sort-desc:after { + font: normal normal normal 14px/1 FontAwesome; + position: absolute; + right: 3px; + top: 3px; + padding: 2px; + background-color: fadeout(lighten(#F2F2F2, 3%), 20%); + } + &.sort-asc:after { + content: "\f0de"; + } + &.sort-desc:after { + content: "\f0dd"; + } - &:nth-child(even) { - background-color: rgba(0, 0, 0, 0.05); - } - &.selected { - background-color: hsla(209, 52%, 84%, 0.5) !important; - } - &.highlighted { - background-color: hsla(48, 100%, 50%, 0.4); } - &.highlighted:nth-child(even) { - background-color: hsla(48, 100%, 50%, 0.5); + + tr { + cursor: pointer; + + &:nth-child(even) { + background-color: rgba(0, 0, 0, 0.05); + } + &.selected { + background-color: hsla(209, 52%, 84%, 0.5) !important; + } + &.highlighted { + background-color: hsla(48, 100%, 50%, 0.4); + } + &.highlighted:nth-child(even) { + background-color: hsla(48, 100%, 50%, 0.5); + } } - } - td { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } + td { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } - @interceptorange: hsl(30, 100%, 50%); + @interceptorange: hsl(30, 100%, 50%); - tr.intercepted:not(.has-response) { - .col-path, .col-method { - color: @interceptorange; + tr.intercepted:not(.has-response) { + .col-path, .col-method { + color: @interceptorange; + } } - } - tr.intercepted.has-response { - .col-status, .col-size, .col-time { - color: @interceptorange; + tr.intercepted.has-response { + .col-status, .col-size, .col-time { + color: @interceptorange; + } } - } - .fa { - line-height: inherit; - &.pull-right { - margin-left: 0; + .fa { + line-height: inherit; + &.pull-right { + margin-left: 0; + } } - } - .col-tls { - width: 10px; - } - .col-tls-https { - background-color: rgba(0, 185, 0, 0.5); - } - .col-icon { - width: 32px; - } - .col-path { - .fa-repeat { - color: green; - } - .fa-pause { - color: @interceptorange; - } - } - .col-method { - width: 60px; - } - .col-status { - width: 50px; - } - .col-size { - width: 70px; - } - .col-time { - width: 50px; - } - td.col-time, td.col-size { - text-align: right; - } + .col-tls { + width: 10px; + } + .col-tls-https { + background-color: rgba(0, 185, 0, 0.5); + } + .col-icon { + width: 32px; + } + .col-path { + .fa-repeat { + color: green; + } + .fa-pause { + color: @interceptorange; + } + } + .col-method { + width: 60px; + } + .col-status { + width: 50px; + } + .col-size { + width: 70px; + } + .col-time { + width: 50px; + } + td.col-time, td.col-size { + text-align: right; + } }
\ No newline at end of file diff --git a/web/src/css/flowview.less b/web/src/css/flowview.less new file mode 100644 index 00000000..aa8a2df2 --- /dev/null +++ b/web/src/css/flowview.less @@ -0,0 +1,9 @@ +.flowview-image { + + text-align: center; + + img { + max-width: 100%; + max-height: 100%; + } +}
\ No newline at end of file diff --git a/web/src/css/header.less b/web/src/css/header.less index 57f122e8..6e61b956 100644 --- a/web/src/css/header.less +++ b/web/src/css/header.less @@ -2,30 +2,30 @@ @import (reference) '../../node_modules/bootstrap/less/mixins/grid.less'; header { - padding-top: 0.5em; - background-color: white; - @separator-color: lighten(grey, 15%); - .menu { - padding: 10px; - border-bottom: solid @separator-color 1px; - } + padding-top: 0.5em; + background-color: white; + @separator-color: lighten(grey, 15%); + .menu { + padding: 10px; + border-bottom: solid @separator-color 1px; + } } @menu-row-gutter-width: 5px; .menu-row { - .make-row(@menu-row-gutter-width); + .make-row(@menu-row-gutter-width); } .filter-input { - .make-md-column(3, @menu-row-gutter-width); + .make-md-column(3, @menu-row-gutter-width); } .filter-input .popover { - top: 27px; - display: block; - max-width: none; - .popover-content { - max-height: 500px; - overflow-y: auto; - } + top: 27px; + display: block; + max-width: none; + .popover-content { + max-height: 500px; + overflow-y: auto; + } }
\ No newline at end of file diff --git a/web/src/css/layout.less b/web/src/css/layout.less index f6807f24..4e96609b 100644 --- a/web/src/css/layout.less +++ b/web/src/css/layout.less @@ -15,7 +15,7 @@ html, body, #container { .main-view { flex: 1 1 auto; - + display: flex; flex-direction: row; diff --git a/web/src/css/sprites.less b/web/src/css/sprites.less index 49b3600c..74131c5e 100644 --- a/web/src/css/sprites.less +++ b/web/src/css/sprites.less @@ -5,34 +5,42 @@ // From Chrome Dev Tools .resource-icon-css { - background-image: url(images/chrome-devtools/resourceCSSIcon.png); + background-image: url(images/chrome-devtools/resourceCSSIcon.png); } + .resource-icon-document { - background-image: url(images/chrome-devtools/resourceDocumentIcon.png); + background-image: url(images/chrome-devtools/resourceDocumentIcon.png); } + .resource-icon-js { - background-image: url(images/chrome-devtools/resourceJSIcon.png); + background-image: url(images/chrome-devtools/resourceJSIcon.png); } + .resource-icon-plain { - background-image: url(images/chrome-devtools/resourcePlainIcon.png); + background-image: url(images/chrome-devtools/resourcePlainIcon.png); } // Own .resource-icon-executable { - background-image: url(images/resourceExecutableIcon.png); + background-image: url(images/resourceExecutableIcon.png); } + .resource-icon-flash { - background-image: url(images/resourceFlashIcon.png); + background-image: url(images/resourceFlashIcon.png); } + .resource-icon-image { - background-image: url(images/resourceImageIcon.png); + background-image: url(images/resourceImageIcon.png); } + .resource-icon-java { - background-image: url(images/resourceJavaIcon.png); + background-image: url(images/resourceJavaIcon.png); } + .resource-icon-not-modified { - background-image: url(images/resourceNotModifiedIcon.png); + background-image: url(images/resourceNotModifiedIcon.png); } + .resource-icon-redirect { - background-image: url(images/resourceRedirectIcon.png); + background-image: url(images/resourceRedirectIcon.png); }
\ No newline at end of file diff --git a/web/src/css/vendor-bootstrap-variables.less b/web/src/css/vendor-bootstrap-variables.less index b2818993..e2c37bf5 100644 --- a/web/src/css/vendor-bootstrap-variables.less +++ b/web/src/css/vendor-bootstrap-variables.less @@ -1,6 +1,5 @@ - -@navbar-height: 32px; -@navbar-default-link-color: #303030; -@navbar-default-color: #303030; -@navbar-default-bg: #ffffff; -@navbar-default-border: #e0e0e0; +@navbar-height: 32px; +@navbar-default-link-color: #303030; +@navbar-default-color: #303030; +@navbar-default-bg: #ffffff; +@navbar-default-border: #e0e0e0; diff --git a/web/src/css/vendor-bootstrap.less b/web/src/css/vendor-bootstrap.less index 0b3252fe..35fda379 100644 --- a/web/src/css/vendor-bootstrap.less +++ b/web/src/css/vendor-bootstrap.less @@ -2,12 +2,10 @@ @import "../../node_modules/bootstrap/less/variables.less"; @import "vendor-bootstrap-variables.less"; @import "../../node_modules/bootstrap/less/mixins.less"; - // Reset and dependencies @import "../../node_modules/bootstrap/less/normalize.less"; @import "../../node_modules/bootstrap/less/print.less"; @import "../../node_modules/bootstrap/less/glyphicons.less"; - // Core CSS @import "../../node_modules/bootstrap/less/scaffolding.less"; @import "../../node_modules/bootstrap/less/type.less"; @@ -16,7 +14,6 @@ @import "../../node_modules/bootstrap/less/tables.less"; @import "../../node_modules/bootstrap/less/forms.less"; @import "../../node_modules/bootstrap/less/buttons.less"; - // Components @import "../../node_modules/bootstrap/less/component-animations.less"; @import "../../node_modules/bootstrap/less/dropdowns.less"; @@ -39,13 +36,11 @@ @import "../../node_modules/bootstrap/less/responsive-embed.less"; @import "../../node_modules/bootstrap/less/wells.less"; @import "../../node_modules/bootstrap/less/close.less"; - // Components w/ JavaScript @import "../../node_modules/bootstrap/less/modals.less"; @import "../../node_modules/bootstrap/less/tooltip.less"; @import "../../node_modules/bootstrap/less/popovers.less"; @import "../../node_modules/bootstrap/less/carousel.less"; - // Utility classes @import "../../node_modules/bootstrap/less/utilities.less"; @import "../../node_modules/bootstrap/less/responsive-utilities.less"; diff --git a/web/src/js/actions.js b/web/src/js/actions.js index 64cd68a7..1b29438c 100644 --- a/web/src/js/actions.js +++ b/web/src/js/actions.js @@ -117,5 +117,6 @@ module.exports = { ConnectionActions: ConnectionActions, FlowActions: FlowActions, StoreCmds: StoreCmds, + SettingsActions: SettingsActions, Query: Query };
\ No newline at end of file diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js index 23508275..de69462b 100644 --- a/web/src/js/components/eventlog.js +++ b/web/src/js/components/eventlog.js @@ -3,6 +3,7 @@ var common = require("./common.js"); var Query = require("../actions.js").Query; var VirtualScrollMixin = require("./virtualscroll.js"); var views = require("../store/view.js"); +var _ = require("lodash"); var LogMessage = React.createClass({ render: function () { diff --git a/web/src/js/components/flowdetail.js b/web/src/js/components/flowdetail.js deleted file mode 100644 index 1d019ffb..00000000 --- a/web/src/js/components/flowdetail.js +++ /dev/null @@ -1,399 +0,0 @@ -var React = require("react"); -var _ = require("lodash"); - -var common = require("./common.js"); -var actions = require("../actions.js"); -var flowutils = require("../flow/utils.js"); -var toputils = require("../utils.js"); - -var NavAction = React.createClass({ - onClick: function (e) { - e.preventDefault(); - this.props.onClick(); - }, - render: function () { - return ( - <a title={this.props.title} - href="#" - className="nav-action" - onClick={this.onClick}> - <i className={"fa fa-fw " + this.props.icon}></i> - </a> - ); - } -}); - -var FlowDetailNav = React.createClass({ - render: function () { - var flow = this.props.flow; - - var tabs = this.props.tabs.map(function (e) { - var str = e.charAt(0).toUpperCase() + e.slice(1); - var className = this.props.active === e ? "active" : ""; - var onClick = function (event) { - this.props.selectTab(e); - event.preventDefault(); - }.bind(this); - return <a key={e} - href="#" - className={className} - onClick={onClick}>{str}</a>; - }.bind(this)); - - var acceptButton = null; - if(flow.intercepted){ - acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={actions.FlowActions.accept.bind(null, flow)} />; - } - var revertButton = null; - if(flow.modified){ - revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={actions.FlowActions.revert.bind(null, flow)} />; - } - - return ( - <nav ref="head" className="nav-tabs nav-tabs-sm"> - {tabs} - <NavAction title="[d]elete flow" icon="fa-trash" onClick={actions.FlowActions.delete.bind(null, flow)} /> - <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={actions.FlowActions.duplicate.bind(null, flow)} /> - <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={actions.FlowActions.replay.bind(null, flow)} /> - {acceptButton} - {revertButton} - </nav> - ); - } -}); - -var Headers = React.createClass({ - render: function () { - var rows = this.props.message.headers.map(function (header, i) { - return ( - <tr key={i}> - <td className="header-name">{header[0] + ":"}</td> - <td className="header-value">{header[1]}</td> - </tr> - ); - }); - return ( - <table className="header-table"> - <tbody> - {rows} - </tbody> - </table> - ); - } -}); - -var FlowDetailRequest = 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 content = null; - if (flow.request.contentLength > 0) { - content = "Request Content Size: " + toputils.formatSize(flow.request.contentLength); - } else { - content = <div className="alert alert-info">No Content</div>; - } - - //TODO: Styling - - return ( - <section> - <div className="first-line">{ first_line }</div> - <Headers message={flow.request}/> - <hr/> - {content} - </section> - ); - } -}); - -var FlowDetailResponse = React.createClass({ - render: function () { - var flow = this.props.flow; - var first_line = [ - "HTTP/" + flow.response.httpversion.join("."), - flow.response.code, - flow.response.msg - ].join(" "); - var content = null; - if (flow.response.contentLength > 0) { - content = "Response Content Size: " + toputils.formatSize(flow.response.contentLength); - } else { - content = <div className="alert alert-info">No Content</div>; - } - - //TODO: Styling - - return ( - <section> - <div className="first-line">{ first_line }</div> - <Headers message={flow.response}/> - <hr/> - {content} - </section> - ); - } -}); - -var FlowDetailError = React.createClass({ - render: function () { - var flow = this.props.flow; - return ( - <section> - <div className="alert alert-warning"> - {flow.error.msg} - <div> - <small>{ toputils.formatTimeStamp(flow.error.timestamp) }</small> - </div> - </div> - </section> - ); - } -}); - -var TimeStamp = React.createClass({ - render: function () { - - if (!this.props.t) { - //should be return null, but that triggers a React bug. - return <tr></tr>; - } - - var ts = toputils.formatTimeStamp(this.props.t); - - var delta; - if (this.props.deltaTo) { - delta = toputils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); - delta = <span className="text-muted">{"(" + delta + ")"}</span>; - } else { - delta = null; - } - - return <tr> - <td>{this.props.title + ":"}</td> - <td>{ts} {delta}</td> - </tr>; - } -}); - -var ConnectionInfo = React.createClass({ - - render: function () { - var conn = this.props.conn; - var address = conn.address.address.join(":"); - - var sni = <tr key="sni"></tr>; //should be null, but that triggers a React bug. - if (conn.sni) { - sni = <tr key="sni"> - <td> - <abbr title="TLS Server Name Indication">TLS SNI:</abbr> - </td> - <td>{conn.sni}</td> - </tr>; - } - return ( - <table className="connection-table"> - <tbody> - <tr key="address"> - <td>Address:</td> - <td>{address}</td> - </tr> - {sni} - </tbody> - </table> - ); - } -}); - -var CertificateInfo = React.createClass({ - render: function () { - //TODO: We should fetch human-readable certificate representation - // from the server - var flow = this.props.flow; - var client_conn = flow.client_conn; - var server_conn = flow.server_conn; - - var preStyle = {maxHeight: 100}; - return ( - <div> - {client_conn.cert ? <h4>Client Certificate</h4> : null} - {client_conn.cert ? <pre style={preStyle}>{client_conn.cert}</pre> : null} - - {server_conn.cert ? <h4>Server Certificate</h4> : null} - {server_conn.cert ? <pre style={preStyle}>{server_conn.cert}</pre> : null} - </div> - ); - } -}); - -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 ( - <div> - <h4>Timing</h4> - <table className="timing-table"> - <tbody> - {rows} - </tbody> - </table> - </div> - ); - } -}); - -var FlowDetailConnectionInfo = React.createClass({ - render: function () { - var flow = this.props.flow; - var client_conn = flow.client_conn; - var server_conn = flow.server_conn; - return ( - <section> - - <h4>Client Connection</h4> - <ConnectionInfo conn={client_conn}/> - - <h4>Server Connection</h4> - <ConnectionInfo conn={server_conn}/> - - <CertificateInfo flow={flow}/> - - <Timing flow={flow}/> - - </section> - ); - } -}); - -var allTabs = { - request: FlowDetailRequest, - response: FlowDetailResponse, - error: FlowDetailError, - details: FlowDetailConnectionInfo -}; - -var FlowDetail = React.createClass({ - mixins: [common.StickyHeadMixin, common.Navigation, common.State], - getTabs: function (flow) { - var tabs = []; - ["request", "response", "error"].forEach(function (e) { - if (flow[e]) { - tabs.push(e); - } - }); - tabs.push("details"); - return tabs; - }, - nextTab: function (i) { - var tabs = this.getTabs(this.props.flow); - var currentIndex = tabs.indexOf(this.getParams().detailTab); - // JS modulo operator doesn't correct negative numbers, make sure that we are positive. - var nextIndex = (currentIndex + i + tabs.length) % tabs.length; - this.selectTab(tabs[nextIndex]); - }, - selectTab: function (panel) { - this.replaceWith( - "flow", - { - flowId: this.getParams().flowId, - detailTab: panel - } - ); - }, - render: function () { - var flow = this.props.flow; - var tabs = this.getTabs(flow); - var active = this.getParams().detailTab; - - if (!_.contains(tabs, active)) { - if (active === "response" && flow.error) { - active = "error"; - } else if (active === "error" && flow.response) { - active = "response"; - } else { - active = tabs[0]; - } - this.selectTab(active); - } - - var Tab = allTabs[active]; - return ( - <div className="flow-detail" onScroll={this.adjustHead}> - <FlowDetailNav ref="head" - flow={flow} - tabs={tabs} - active={active} - selectTab={this.selectTab}/> - <Tab flow={flow}/> - </div> - ); - } -}); - -module.exports = { - FlowDetail: FlowDetail -};
\ No newline at end of file diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js new file mode 100644 index 00000000..09a64bb2 --- /dev/null +++ b/web/src/js/components/flowview/contentview.js @@ -0,0 +1,158 @@ +var React = require("react"); +var _ = require("lodash"); + +var MessageUtils = require("../../flow/utils.js").MessageUtils; +var utils = require("../../utils.js"); + +var image_regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i; +var Image = React.createClass({ + statics: { + matches: function (message) { + return image_regex.test(MessageUtils.getContentType(message)); + } + }, + render: function () { + var message_name = this.props.flow.request === this.props.message ? "request" : "response"; + var url = "/flows/" + this.props.flow.id + "/" + message_name + "/content"; + return <div className="flowview-image"> + <img src={url} alt="preview" className="img-thumbnail"/> + </div>; + } +}); + +var Raw = React.createClass({ + statics: { + matches: function (message) { + return true; + } + }, + render: function () { + //FIXME + return <div>raw</div>; + } +}); + + +var Auto = React.createClass({ + statics: { + matches: function () { + return false; // don't match itself + }, + findView: function (message) { + for (var i = 0; i < all.length; i++) { + if (all[i].matches(message)) { + return all[i]; + } + } + return all[all.length - 1]; + } + }, + render: function () { + var View = Auto.findView(this.props.message); + return <View {...this.props}/>; + } +}); + +var all = [Auto, Image, Raw]; + + +var ContentEmpty = React.createClass({ + render: function () { + var message_name = this.props.flow.request === this.props.message ? "request" : "response"; + return <div className="alert alert-info">No {message_name} content.</div>; + } +}); + +var ContentMissing = React.createClass({ + render: function () { + var message_name = this.props.flow.request === this.props.message ? "Request" : "Response"; + return <div className="alert alert-info">{message_name} content missing.</div>; + } +}); + +var TooLarge = React.createClass({ + render: function () { + var size = utils.formatSize(this.props.message.contentLength); + return <div className="alert alert-warning"> + <button onClick={this.props.onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button> + {size} content size. + </div>; + } +}); + +var ViewSelector = React.createClass({ + render: function () { + var views = []; + for (var i = 0; i < all.length; i++) { + var view = all[i]; + var className = "btn btn-default"; + if (view === this.props.active) { + className += " active"; + } + var text; + if (view === Auto) { + text = "auto: " + Auto.findView(this.props.message).displayName.toLowerCase(); + } else { + text = view.displayName.toLowerCase(); + } + views.push( + <button + key={view.displayName} + onClick={this.props.selectView.bind(null, view)} + className={className}> + {text} + </button> + ); + } + + return <div className="view-selector btn-group btn-group-xs">{views}</div>; + } +}); + +var ContentView = React.createClass({ + getInitialState: function () { + return { + displayLarge: false, + View: Auto + }; + }, + propTypes: { + // It may seem a bit weird at the first glance: + // Every view takes the flow and the message as props, e.g. + // <Auto flow={flow} message={flow.request}/> + flow: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired, + }, + selectView: function (view) { + this.setState({ + View: view + }); + }, + displayLarge: function () { + this.setState({displayLarge: true}); + }, + componentWillReceiveProps: function (nextProps) { + if (nextProps.message !== this.props.message) { + this.setState(this.getInitialState()); + } + }, + render: function () { + var message = this.props.message; + if (message.contentLength === 0) { + return <ContentEmpty {...this.props}/>; + } else if (message.contentLength === null) { + return <ContentMissing {...this.props}/>; + } else if (message.contentLength > 1024 * 1024 * 3 && !this.state.displayLarge) { + return <TooLarge {...this.props} onClick={this.displayLarge}/>; + } + + return <div> + <this.state.View {...this.props} /> + <div className="text-center"> + <ViewSelector selectView={this.selectView} active={this.state.View} message={message}/> + </div> + </div>; + } +}); + +module.exports = ContentView;
\ No newline at end of file diff --git a/web/src/js/components/flowview/details.js b/web/src/js/components/flowview/details.js new file mode 100644 index 00000000..00e0116c --- /dev/null +++ b/web/src/js/components/flowview/details.js @@ -0,0 +1,181 @@ +var React = require("react"); +var _ = require("lodash"); + +var utils = require("../../utils.js"); + +var TimeStamp = React.createClass({ + render: function () { + + if (!this.props.t) { + //should be return null, but that triggers a React bug. + return <tr></tr>; + } + + var ts = utils.formatTimeStamp(this.props.t); + + var delta; + if (this.props.deltaTo) { + delta = utils.formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); + delta = <span className="text-muted">{"(" + delta + ")"}</span>; + } else { + delta = null; + } + + return <tr> + <td>{this.props.title + ":"}</td> + <td>{ts} {delta}</td> + </tr>; + } +}); + +var ConnectionInfo = React.createClass({ + + render: function () { + var conn = this.props.conn; + var address = conn.address.address.join(":"); + + var sni = <tr key="sni"></tr>; //should be null, but that triggers a React bug. + if (conn.sni) { + sni = <tr key="sni"> + <td> + <abbr title="TLS Server Name Indication">TLS SNI:</abbr> + </td> + <td>{conn.sni}</td> + </tr>; + } + return ( + <table className="connection-table"> + <tbody> + <tr key="address"> + <td>Address:</td> + <td>{address}</td> + </tr> + {sni} + </tbody> + </table> + ); + } +}); + +var CertificateInfo = React.createClass({ + render: function () { + //TODO: We should fetch human-readable certificate representation + // from the server + var flow = this.props.flow; + var client_conn = flow.client_conn; + var server_conn = flow.server_conn; + + var preStyle = {maxHeight: 100}; + return ( + <div> + {client_conn.cert ? <h4>Client Certificate</h4> : null} + {client_conn.cert ? <pre style={preStyle}>{client_conn.cert}</pre> : null} + + {server_conn.cert ? <h4>Server Certificate</h4> : null} + {server_conn.cert ? <pre style={preStyle}>{server_conn.cert}</pre> : null} + </div> + ); + } +}); + +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 ( + <div> + <h4>Timing</h4> + <table className="timing-table"> + <tbody> + {rows} + </tbody> + </table> + </div> + ); + } +}); + +var Details = React.createClass({ + render: function () { + var flow = this.props.flow; + var client_conn = flow.client_conn; + var server_conn = flow.server_conn; + return ( + <section> + + <h4>Client Connection</h4> + <ConnectionInfo conn={client_conn}/> + + <h4>Server Connection</h4> + <ConnectionInfo conn={server_conn}/> + + <CertificateInfo flow={flow}/> + + <Timing flow={flow}/> + + </section> + ); + } +}); + +module.exports = Details;
\ No newline at end of file diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js new file mode 100644 index 00000000..0c31aca5 --- /dev/null +++ b/web/src/js/components/flowview/index.js @@ -0,0 +1,74 @@ +var React = require("react"); +var _ = require("lodash"); + +var common = require("../common.js"); +var Nav = require("./nav.js"); +var Messages = require("./messages.js"); +var Details = require("./details.js"); + +var allTabs = { + request: Messages.Request, + response: Messages.Response, + error: Messages.Error, + details: Details +}; + +var FlowView = React.createClass({ + mixins: [common.StickyHeadMixin, common.Navigation, common.State], + getTabs: function (flow) { + var tabs = []; + ["request", "response", "error"].forEach(function (e) { + if (flow[e]) { + tabs.push(e); + } + }); + tabs.push("details"); + return tabs; + }, + nextTab: function (i) { + var tabs = this.getTabs(this.props.flow); + var currentIndex = tabs.indexOf(this.getParams().detailTab); + // JS modulo operator doesn't correct negative numbers, make sure that we are positive. + var nextIndex = (currentIndex + i + tabs.length) % tabs.length; + this.selectTab(tabs[nextIndex]); + }, + selectTab: function (panel) { + this.replaceWith( + "flow", + { + flowId: this.getParams().flowId, + detailTab: panel + } + ); + }, + render: function () { + var flow = this.props.flow; + var tabs = this.getTabs(flow); + var active = this.getParams().detailTab; + + if (!_.contains(tabs, active)) { + if (active === "response" && flow.error) { + active = "error"; + } else if (active === "error" && flow.response) { + active = "response"; + } else { + active = tabs[0]; + } + this.selectTab(active); + } + + var Tab = allTabs[active]; + return ( + <div className="flow-detail" onScroll={this.adjustHead}> + <Nav ref="head" + flow={flow} + tabs={tabs} + active={active} + selectTab={this.selectTab}/> + <Tab flow={flow}/> + </div> + ); + } +}); + +module.exports = FlowView;
\ No newline at end of file diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js new file mode 100644 index 00000000..fe8fa200 --- /dev/null +++ b/web/src/js/components/flowview/messages.js @@ -0,0 +1,91 @@ +var React = require("react"); + +var flowutils = require("../../flow/utils.js"); +var utils = require("../../utils.js"); +var ContentView = require("./contentview.js"); + +var Headers = React.createClass({ + render: function () { + var rows = this.props.message.headers.map(function (header, i) { + return ( + <tr key={i}> + <td className="header-name">{header[0] + ":"}</td> + <td className="header-value">{header[1]}</td> + </tr> + ); + }); + return ( + <table className="header-table"> + <tbody> + {rows} + </tbody> + </table> + ); + } +}); + +var Request = 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(" "); + + //TODO: Styling + + return ( + <section> + <div className="first-line">{ first_line }</div> + <Headers message={flow.request}/> + <hr/> + <ContentView flow={flow} message={flow.request}/> + </section> + ); + } +}); + +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> + <Headers message={flow.response}/> + <hr/> + <ContentView flow={flow} message={flow.response}/> + </section> + ); + } +}); + +var Error = React.createClass({ + render: function () { + var flow = this.props.flow; + return ( + <section> + <div className="alert alert-warning"> + {flow.error.msg} + <div> + <small>{ utils.formatTimeStamp(flow.error.timestamp) }</small> + </div> + </div> + </section> + ); + } +}); + +module.exports = { + Request: Request, + Response: Response, + Error: Error +};
\ No newline at end of file diff --git a/web/src/js/components/flowview/nav.js b/web/src/js/components/flowview/nav.js new file mode 100644 index 00000000..46eda707 --- /dev/null +++ b/web/src/js/components/flowview/nav.js @@ -0,0 +1,61 @@ +var React = require("react"); + +var actions = require("../../actions.js"); + +var NavAction = React.createClass({ + onClick: function (e) { + e.preventDefault(); + this.props.onClick(); + }, + render: function () { + return ( + <a title={this.props.title} + href="#" + className="nav-action" + onClick={this.onClick}> + <i className={"fa fa-fw " + this.props.icon}></i> + </a> + ); + } +}); + +var Nav = React.createClass({ + render: function () { + var flow = this.props.flow; + + var tabs = this.props.tabs.map(function (e) { + var str = e.charAt(0).toUpperCase() + e.slice(1); + var className = this.props.active === e ? "active" : ""; + var onClick = function (event) { + this.props.selectTab(e); + event.preventDefault(); + }.bind(this); + return <a key={e} + href="#" + className={className} + onClick={onClick}>{str}</a>; + }.bind(this)); + + var acceptButton = null; + if(flow.intercepted){ + acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={actions.FlowActions.accept.bind(null, flow)} />; + } + var revertButton = null; + if(flow.modified){ + revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={actions.FlowActions.revert.bind(null, flow)} />; + } + + return ( + <nav ref="head" className="nav-tabs nav-tabs-sm"> + {tabs} + <NavAction title="[d]elete flow" icon="fa-trash" onClick={actions.FlowActions.delete.bind(null, flow)} /> + <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={actions.FlowActions.duplicate.bind(null, flow)} /> + <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={actions.FlowActions.replay.bind(null, flow)} /> + {acceptButton} + {revertButton} + </nav> + ); + } +}); + +module.exports = Nav;
\ No newline at end of file diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js index d1684fb2..eca06e74 100644 --- a/web/src/js/components/header.js +++ b/web/src/js/components/header.js @@ -173,7 +173,7 @@ var MainMenu = React.createClass({ this.setQuery(d); }, onInterceptChange: function (val) { - SettingsActions.update({intercept: val}); + actions.SettingsActions.update({intercept: val}); }, render: function () { var filter = this.getQuery()[Query.FILTER] || ""; diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js index 184ef49f..81bf3b03 100644 --- a/web/src/js/components/mainview.js +++ b/web/src/js/components/mainview.js @@ -7,7 +7,7 @@ var toputils = require("../utils.js"); var views = require("../store/view.js"); var Filt = require("../filt/filt.js"); FlowTable = require("./flowtable.js"); -var flowdetail = require("./flowdetail.js"); +var FlowView = require("./flowview/index.js"); var MainView = React.createClass({ mixins: [common.Navigation, common.State], @@ -203,6 +203,8 @@ var MainView = React.createClass({ actions.FlowActions.revert(flow); } break; + case toputils.Key.SHIFT: + break; default: console.debug("keydown", e.keyCode); return; @@ -219,7 +221,7 @@ var MainView = React.createClass({ if (selected) { details = [ <common.Splitter key="splitter"/>, - <flowdetail.FlowDetail key="flowDetails" ref="flowDetails" flow={selected}/> + <FlowView key="flowDetails" ref="flowDetails" flow={selected}/> ]; } else { details = null; diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js index a95d4ffe..dd7f763b 100644 --- a/web/src/js/flow/utils.js +++ b/web/src/js/flow/utils.js @@ -1,6 +1,6 @@ var _ = require("lodash"); -var _MessageUtils = { +var MessageUtils = { getContentType: function (message) { return this.get_first_header(message, /^Content-Type$/i); }, @@ -42,7 +42,7 @@ var defaultPorts = { "https": 443 }; -var RequestUtils = _.extend(_MessageUtils, { +var RequestUtils = _.extend(MessageUtils, { pretty_host: function (request) { //FIXME: Add hostheader return request.host; @@ -56,11 +56,11 @@ var RequestUtils = _.extend(_MessageUtils, { } }); -var ResponseUtils = _.extend(_MessageUtils, {}); +var ResponseUtils = _.extend(MessageUtils, {}); module.exports = { ResponseUtils: ResponseUtils, - RequestUtils: RequestUtils - -}
\ No newline at end of file + RequestUtils: RequestUtils, + MessageUtils: MessageUtils +};
\ No newline at end of file diff --git a/web/src/js/store/view.js b/web/src/js/store/view.js index 204d22da..d13822d5 100644 --- a/web/src/js/store/view.js +++ b/web/src/js/store/view.js @@ -14,8 +14,6 @@ var default_filt = function (elem) { function StoreView(store, filt, sortfun) { EventEmitter.call(this); - filt = filt || default_filt; - sortfun = sortfun || default_sort; this.store = store; @@ -39,10 +37,10 @@ _.extend(StoreView.prototype, EventEmitter.prototype, { this.store.removeListener("recalculate", this.recalculate); }, recalculate: function (filt, sortfun) { - filt = filt || default_filt; - sortfun = sortfun || default_sort; + filt = filt || this.filt || default_filt; + sortfun = sortfun || this.sortfun || default_sort; filt = filt.bind(this); - sortfun = sortfun.bind(this) + sortfun = sortfun.bind(this); this.filt = filt; this.sortfun = sortfun; diff --git a/web/src/js/utils.js b/web/src/js/utils.js index bcd3958d..48f69880 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -15,6 +15,7 @@ var Key = { TAB: 9, SPACE: 32, BACKSPACE: 8, + SHIFT: 16 }; // Add A-Z for (var i = 65; i <= 90; i++) { |