diff options
| author | Aldo Cortesi <aldo@nullcube.com> | 2015-03-22 13:33:11 +1300 | 
|---|---|---|
| committer | Aldo Cortesi <aldo@nullcube.com> | 2015-03-22 13:33:11 +1300 | 
| commit | 3bf4feb213adf2b9829cd5452ca365e4be7ca10c (patch) | |
| tree | 572463b2e5223499a977c4da2e3ce8118d373943 /web/src | |
| parent | 89383e9c138f68caf1cc394174250c133d21aa04 (diff) | |
| parent | 89d66360d6f7caa9760fe56fa146396b1b4251dc (diff) | |
| download | mitmproxy-3bf4feb213adf2b9829cd5452ca365e4be7ca10c.tar.gz mitmproxy-3bf4feb213adf2b9829cd5452ca365e4be7ca10c.tar.bz2 mitmproxy-3bf4feb213adf2b9829cd5452ca365e4be7ca10c.zip | |
Merge branch 'master' of ssh.github.com:mitmproxy/mitmproxy
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++) { | 
