aboutsummaryrefslogtreecommitdiffstats
path: root/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'web/src')
-rw-r--r--web/src/css/app.less1
-rw-r--r--web/src/css/eventlog.less2
-rw-r--r--web/src/css/flowdetail.less27
-rw-r--r--web/src/css/flowtable.less188
-rw-r--r--web/src/css/flowview.less9
-rw-r--r--web/src/css/header.less32
-rw-r--r--web/src/css/layout.less2
-rw-r--r--web/src/css/sprites.less28
-rw-r--r--web/src/css/vendor-bootstrap-variables.less11
-rw-r--r--web/src/css/vendor-bootstrap.less5
-rw-r--r--web/src/js/actions.js8
-rw-r--r--web/src/js/app.js3
-rw-r--r--web/src/js/components/common.js48
-rw-r--r--web/src/js/components/eventlog.js2
-rw-r--r--web/src/js/components/flowdetail.js399
-rw-r--r--web/src/js/components/flowtable-columns.js83
-rw-r--r--web/src/js/components/flowtable.js78
-rw-r--r--web/src/js/components/flowview/contentview.js235
-rw-r--r--web/src/js/components/flowview/details.js181
-rw-r--r--web/src/js/components/flowview/index.js74
-rw-r--r--web/src/js/components/flowview/messages.js91
-rw-r--r--web/src/js/components/flowview/nav.js61
-rw-r--r--web/src/js/components/header.js22
-rw-r--r--web/src/js/components/mainview.js38
-rw-r--r--web/src/js/components/proxyapp.js11
-rw-r--r--web/src/js/connection.js5
-rw-r--r--web/src/js/dispatcher.js2
-rw-r--r--web/src/js/flow/utils.js27
-rw-r--r--web/src/js/store/view.js36
-rw-r--r--web/src/js/utils.js27
30 files changed, 1081 insertions, 655 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..cc67eeb2 100644
--- a/web/src/css/flowdetail.less
+++ b/web/src/css/flowdetail.less
@@ -1,13 +1,13 @@
//TODO: Move into some utils
-.monospace(){
+.monospace() {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
-
.flow-detail {
width: 100%;
- overflow: auto;
-
+ overflow-x: auto;
+ overflow-y: scroll;
+
nav {
background-color: #F2F2F2;
}
@@ -27,18 +27,26 @@
max-height: 100px;
overflow-y: auto;
}
+
+ hr {
+ margin: 0 0 5px;
+ }
+
}
+.view-options {
+ 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 +67,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 9988f1a8..3533983c 100644
--- a/web/src/css/flowtable.less
+++ b/web/src/css/flowtable.less
@@ -1,95 +1,127 @@
-.flow-table {
- width: 100%;
- overflow: auto;
+//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;
+}
- table {
+.flow-table {
width: 100%;
- table-layout: fixed;
- }
+ overflow: auto;
- thead {
- background-color: #F2F2F2;
- line-height: 23px;
- }
+ table {
+ width: 100%;
+ table-layout: fixed;
+ }
- th {
- font-weight: normal;
- box-shadow: 0 1px 0 #a6a6a6;
- }
+ thead {
+ background-color: #F2F2F2;
+ line-height: 23px;
+ }
- tr {
- cursor: pointer;
+ th {
+ font-weight: normal;
+ box-shadow: 0 1px 0 #a6a6a6;
+ position: relative !important;
+ padding-left: 1px;
+ .user-select(none);
+
+ &.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 258501a4..78fd4bf7 100644
--- a/web/src/js/actions.js
+++ b/web/src/js/actions.js
@@ -1,4 +1,5 @@
var $ = require("jquery");
+var AppDispatcher = require("./dispatcher.js").AppDispatcher;
var ActionTypes = {
// Connection
@@ -106,7 +107,7 @@ var FlowActions = {
}
};
-Query = {
+var Query = {
FILTER: "f",
HIGHLIGHT: "h",
SHOW_EVENTLOG: "e"
@@ -116,5 +117,8 @@ module.exports = {
ActionTypes: ActionTypes,
ConnectionActions: ConnectionActions,
FlowActions: FlowActions,
- StoreCmds: StoreCmds
+ StoreCmds: StoreCmds,
+ SettingsActions: SettingsActions,
+ EventLogActions: EventLogActions,
+ Query: Query
}; \ No newline at end of file
diff --git a/web/src/js/app.js b/web/src/js/app.js
index a7f3570e..63d782d4 100644
--- a/web/src/js/app.js
+++ b/web/src/js/app.js
@@ -2,14 +2,13 @@
var React = require("react");
var ReactRouter = require("react-router");
var $ = require("jquery");
-
var Connection = require("./connection");
var proxyapp = require("./components/proxyapp.js");
$(function () {
window.ws = new Connection("/updates");
- ReactRouter.run(proxyapp.routes, function (Handler) {
+ ReactRouter.run(proxyapp.routes, function (Handler, state) {
React.render(<Handler/>, document.body);
});
});
diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js
index 96262acc..3ed035ee 100644
--- a/web/src/js/components/common.js
+++ b/web/src/js/components/common.js
@@ -32,51 +32,41 @@ var StickyHeadMixin = {
var Navigation = _.extend({}, ReactRouter.Navigation, {
setQuery: function (dict) {
- var q = this.context.getCurrentQuery();
+ var q = this.context.router.getCurrentQuery();
for(var i in dict){
if(dict.hasOwnProperty(i)){
q[i] = dict[i] || undefined; //falsey values shall be removed.
}
}
- q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/599
- this.replaceWith(this.context.getCurrentPath(), this.context.getCurrentParams(), q);
+ q._ = "_"; // workaround for https://github.com/rackt/react-router/pull/957
+ this.replaceWith(this.context.router.getCurrentPath(), this.context.router.getCurrentParams(), q);
},
replaceWith: function(routeNameOrPath, params, query) {
if(routeNameOrPath === undefined){
- routeNameOrPath = this.context.getCurrentPath();
+ routeNameOrPath = this.context.router.getCurrentPath();
}
if(params === undefined){
- params = this.context.getCurrentParams();
+ params = this.context.router.getCurrentParams();
}
- if(query === undefined){
- query = this.context.getCurrentQuery();
+ if(query === undefined) {
+ query = this.context.router.getCurrentQuery();
}
- ReactRouter.Navigation.replaceWith.call(this, routeNameOrPath, params, query);
+
+ // FIXME: react-router is just broken,
+ // we hopefully just need to wait for the next release with https://github.com/rackt/react-router/pull/957.
+ this.context.router.replaceWith(routeNameOrPath, params, query);
}
});
-_.extend(Navigation.contextTypes, ReactRouter.State.contextTypes);
+// react-router is fairly good at changing its API regularly.
+// We keep the old method for now - if it should turn out that their changes are permanent,
+// we may remove this mixin and access react-router directly again.
var State = _.extend({}, ReactRouter.State, {
- getInitialState: function () {
- this._query = this.context.getCurrentQuery();
- this._queryWatches = [];
- return null;
- },
- onQueryChange: function (key, callback) {
- this._queryWatches.push({
- key: key,
- callback: callback
- });
+ getQuery: function(){
+ return this.context.router.getCurrentQuery();
},
- componentWillReceiveProps: function (nextProps, nextState) {
- var q = this.context.getCurrentQuery();
- for (var i = 0; i < this._queryWatches.length; i++) {
- var watch = this._queryWatches[i];
- if (this._query[watch.key] !== q[watch.key]) {
- watch.callback(this._query[watch.key], q[watch.key], watch.key);
- }
- }
- this._query = q;
+ getParams: function(){
+ return this.context.router.getCurrentParams();
}
});
@@ -191,4 +181,4 @@ module.exports = {
StickyHeadMixin: StickyHeadMixin,
AutoScrollMixin: AutoScrollMixin,
Splitter: Splitter
-} \ No newline at end of file
+}; \ No newline at end of file
diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js
index ae7cd093..de69462b 100644
--- a/web/src/js/components/eventlog.js
+++ b/web/src/js/components/eventlog.js
@@ -1,7 +1,9 @@
var React = require("react");
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/flowtable-columns.js b/web/src/js/components/flowtable-columns.js
index 39c4bd8d..a82c607a 100644
--- a/web/src/js/components/flowtable-columns.js
+++ b/web/src/js/components/flowtable-columns.js
@@ -1,11 +1,17 @@
var React = require("react");
-var flowutils = require("../flow/utils.js");
+var RequestUtils = require("../flow/utils.js").RequestUtils;
+var ResponseUtils = require("../flow/utils.js").ResponseUtils;
var utils = require("../utils.js");
var TLSColumn = React.createClass({
statics: {
- renderTitle: function () {
- return <th key="tls" className="col-tls"></th>;
+ Title: React.createClass({
+ render: function(){
+ return <th {...this.props} className={"col-tls " + (this.props.className || "") }></th>;
+ }
+ }),
+ sortKeyFun: function(flow){
+ return flow.request.scheme;
}
},
render: function () {
@@ -24,16 +30,18 @@ var TLSColumn = React.createClass({
var IconColumn = React.createClass({
statics: {
- renderTitle: function () {
- return <th key="icon" className="col-icon"></th>;
- }
+ Title: React.createClass({
+ render: function(){
+ return <th {...this.props} className={"col-icon " + (this.props.className || "") }></th>;
+ }
+ })
},
render: function () {
var flow = this.props.flow;
var icon;
if (flow.response) {
- var contentType = flowutils.ResponseUtils.getContentType(flow.response);
+ var contentType = ResponseUtils.getContentType(flow.response);
//TODO: We should assign a type to the flow somewhere else.
if (flow.response.code == 304) {
@@ -64,8 +72,13 @@ var IconColumn = React.createClass({
var PathColumn = React.createClass({
statics: {
- renderTitle: function () {
- return <th key="path" className="col-path">Path</th>;
+ Title: React.createClass({
+ render: function(){
+ return <th {...this.props} className={"col-path " + (this.props.className || "") }>Path</th>;
+ }
+ }),
+ sortKeyFun: function(flow){
+ return RequestUtils.pretty_url(flow.request);
}
},
render: function () {
@@ -73,7 +86,7 @@ var PathColumn = React.createClass({
return <td className="col-path">
{flow.request.is_replay ? <i className="fa fa-fw fa-repeat pull-right"></i> : null}
{flow.intercepted ? <i className="fa fa-fw fa-pause pull-right"></i> : null}
- {flow.request.scheme + "://" + flow.request.host + flow.request.path}
+ { RequestUtils.pretty_url(flow.request) }
</td>;
}
});
@@ -81,8 +94,13 @@ var PathColumn = React.createClass({
var MethodColumn = React.createClass({
statics: {
- renderTitle: function () {
- return <th key="method" className="col-method">Method</th>;
+ Title: React.createClass({
+ render: function(){
+ return <th {...this.props} className={"col-method " + (this.props.className || "") }>Method</th>;
+ }
+ }),
+ sortKeyFun: function(flow){
+ return flow.request.method;
}
},
render: function () {
@@ -94,8 +112,13 @@ var MethodColumn = React.createClass({
var StatusColumn = React.createClass({
statics: {
- renderTitle: function () {
- return <th key="status" className="col-status">Status</th>;
+ Title: React.createClass({
+ render: function(){
+ return <th {...this.props} className={"col-status " + (this.props.className || "") }>Status</th>;
+ }
+ }),
+ sortKeyFun: function(flow){
+ return flow.response ? flow.response.code : undefined;
}
},
render: function () {
@@ -113,8 +136,17 @@ var StatusColumn = React.createClass({
var SizeColumn = React.createClass({
statics: {
- renderTitle: function () {
- return <th key="size" className="col-size">Size</th>;
+ Title: React.createClass({
+ render: function(){
+ return <th {...this.props} className={"col-size " + (this.props.className || "") }>Size</th>;
+ }
+ }),
+ sortKeyFun: function(flow){
+ var total = flow.request.contentLength;
+ if (flow.response) {
+ total += flow.response.contentLength || 0;
+ }
+ return total;
}
},
render: function () {
@@ -132,8 +164,15 @@ var SizeColumn = React.createClass({
var TimeColumn = React.createClass({
statics: {
- renderTitle: function () {
- return <th key="time" className="col-time">Time</th>;
+ Title: React.createClass({
+ render: function(){
+ return <th {...this.props} className={"col-time " + (this.props.className || "") }>Time</th>;
+ }
+ }),
+ sortKeyFun: function(flow){
+ if(flow.response) {
+ return flow.response.timestamp_end - flow.request.timestamp_start;
+ }
}
},
render: function () {
@@ -156,9 +195,7 @@ var all_columns = [
MethodColumn,
StatusColumn,
SizeColumn,
- TimeColumn];
-
-
-module.exports = all_columns;
-
+ TimeColumn
+];
+module.exports = all_columns; \ No newline at end of file
diff --git a/web/src/js/components/flowtable.js b/web/src/js/components/flowtable.js
index cd50b891..4217786a 100644
--- a/web/src/js/components/flowtable.js
+++ b/web/src/js/components/flowtable.js
@@ -1,5 +1,8 @@
var React = require("react");
var common = require("./common.js");
+var utils = require("../utils.js");
+var _ = require("lodash");
+
var VirtualScrollMixin = require("./virtualscroll.js");
var flowtable_columns = require("./flowtable-columns.js");
@@ -43,9 +46,56 @@ var FlowRow = React.createClass({
});
var FlowTableHead = React.createClass({
+ getInitialState: function(){
+ return {
+ sortColumn: undefined,
+ sortDesc: false
+ };
+ },
+ onClick: function(Column){
+ var sortDesc = this.state.sortDesc;
+ var hasSort = Column.sortKeyFun;
+ if(Column === this.state.sortColumn){
+ sortDesc = !sortDesc;
+ this.setState({
+ sortDesc: sortDesc
+ });
+ } else {
+ this.setState({
+ sortColumn: hasSort && Column,
+ sortDesc: false
+ })
+ }
+ var sortKeyFun;
+ if(!sortDesc){
+ sortKeyFun = Column.sortKeyFun;
+ } else {
+ sortKeyFun = hasSort && function(){
+ var k = Column.sortKeyFun.apply(this, arguments);
+ if(_.isString(k)){
+ return utils.reverseString(""+k);
+ } else {
+ return -k;
+ }
+ }
+ }
+ this.props.setSortKeyFun(sortKeyFun);
+ },
render: function () {
- var columns = this.props.columns.map(function (column) {
- return column.renderTitle();
+ var columns = this.props.columns.map(function (Column) {
+ var onClick = this.onClick.bind(this, Column);
+ var className;
+ if(this.state.sortColumn === Column) {
+ if(this.state.sortDesc){
+ className = "sort-desc";
+ } else {
+ className = "sort-asc";
+ }
+ }
+ return <Column.Title
+ key={Column.displayName}
+ onClick={onClick}
+ className={className} />;
}.bind(this));
return <thead>
<tr>{columns}</tr>
@@ -63,13 +113,17 @@ var FlowTable = React.createClass({
columns: flowtable_columns
};
},
- componentWillMount: function () {
- if (this.props.view) {
- this.props.view.addListener("add", this.onChange);
- this.props.view.addListener("update", this.onChange);
- this.props.view.addListener("remove", this.onChange);
- this.props.view.addListener("recalculate", this.onChange);
+ _listen: function(view){
+ if(!view){
+ return;
}
+ view.addListener("add", this.onChange);
+ view.addListener("update", this.onChange);
+ view.addListener("remove", this.onChange);
+ view.addListener("recalculate", this.onChange);
+ },
+ componentWillMount: function () {
+ this._listen(this.props.view);
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.view !== this.props.view) {
@@ -79,10 +133,7 @@ var FlowTable = React.createClass({
this.props.view.removeListener("remove");
this.props.view.removeListener("recalculate");
}
- nextProps.view.addListener("add", this.onChange);
- nextProps.view.addListener("update", this.onChange);
- nextProps.view.addListener("remove", this.onChange);
- nextProps.view.addListener("recalculate", this.onChange);
+ this._listen(nextProps.view);
}
},
getDefaultProps: function () {
@@ -130,7 +181,8 @@ var FlowTable = React.createClass({
<div className="flow-table" onScroll={this.onScrollFlowTable}>
<table>
<FlowTableHead ref="head"
- columns={this.state.columns}/>
+ columns={this.state.columns}
+ setSortKeyFun={this.props.setSortKeyFun}/>
<tbody ref="body">
{ this.getPlaceholderTop(flows.length) }
{rows}
diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js
new file mode 100644
index 00000000..828c6d08
--- /dev/null
+++ b/web/src/js/components/flowview/contentview.js
@@ -0,0 +1,235 @@
+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 ViewImage = React.createClass({
+ statics: {
+ matches: function (message) {
+ return image_regex.test(MessageUtils.getContentType(message));
+ }
+ },
+ render: function () {
+ var url = MessageUtils.getContentURL(this.props.flow, this.props.message);
+ return <div className="flowview-image">
+ <img src={url} alt="preview" className="img-thumbnail"/>
+ </div>;
+ }
+});
+
+var RawMixin = {
+ getInitialState: function () {
+ return {
+ content: undefined,
+ request: undefined
+ }
+ },
+ requestContent: function (nextProps) {
+ if(this.state.request){
+ this.state.request.abort();
+ }
+ var request = MessageUtils.getContent(nextProps.flow, nextProps.message);
+ this.setState({
+ content: undefined,
+ request: request
+ });
+ request.done(function (data) {
+ this.setState({content: data});
+ }.bind(this)).fail(function (jqXHR, textStatus, errorThrown) {
+ if(textStatus === "abort"){
+ return;
+ }
+ this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown});
+ }.bind(this)).always(function(){
+ this.setState({request: undefined});
+ }.bind(this));
+
+ },
+ componentWillMount: function () {
+ this.requestContent(this.props);
+ },
+ componentWillReceiveProps: function (nextProps) {
+ if (nextProps.message !== this.props.message) {
+ this.requestContent(nextProps);
+ }
+ },
+ componentWillUnmount: function(){
+ if(this.state.request){
+ this.state.request.abort();
+ }
+ },
+ render: function () {
+ if (!this.state.content) {
+ return <div className="text-center">
+ <i className="fa fa-spinner fa-spin"></i>
+ </div>;
+ }
+ return this.renderContent();
+ }
+};
+
+var ViewRaw = React.createClass({
+ mixins: [RawMixin],
+ statics: {
+ matches: function (message) {
+ return true;
+ }
+ },
+ renderContent: function () {
+ return <pre>{this.state.content}</pre>;
+ }
+});
+
+var json_regex = /^application\/json$/i;
+var ViewJSON = React.createClass({
+ mixins: [RawMixin],
+ statics: {
+ matches: function (message) {
+ return json_regex.test(MessageUtils.getContentType(message));
+ }
+ },
+ renderContent: function () {
+ var json = this.state.content;
+ try {
+ json = JSON.stringify(JSON.parse(json), null, 2);
+ } catch(e) {
+ }
+ return <pre>{json}</pre>;
+ }
+});
+
+var ViewAuto = 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 = ViewAuto.findView(this.props.message);
+ return <View {...this.props}/>;
+ }
+});
+
+var all = [ViewAuto, ViewImage, ViewJSON, ViewRaw];
+
+
+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({
+ statics: {
+ isTooLarge: function(message){
+ var max_mb = ViewImage.matches(message) ? 10 : 0.2;
+ return message.contentLength > 1024 * 1024 * max_mb;
+ }
+ },
+ 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 === ViewAuto) {
+ text = "auto: " + ViewAuto.findView(this.props.message).displayName.toLowerCase().replace("view", "");
+ } else {
+ text = view.displayName.toLowerCase().replace("view", "");
+ }
+ 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: ViewAuto
+ };
+ },
+ 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 (!this.state.displayLarge && TooLarge.isTooLarge(message)) {
+ return <TooLarge {...this.props} onClick={this.displayLarge}/>;
+ }
+
+ var downloadUrl = MessageUtils.getContentURL(this.props.flow, message);
+
+ return <div>
+ <this.state.View {...this.props} />
+ <div className="view-options text-center">
+ <ViewSelector selectView={this.selectView} active={this.state.View} message={message}/>
+ &nbsp;
+ <a className="btn btn-default btn-xs" href={downloadUrl}><i className="fa fa-download"/></a>
+ </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 76a1a5fb..dcfdd2ae 100644
--- a/web/src/js/components/header.js
+++ b/web/src/js/components/header.js
@@ -3,9 +3,9 @@ var $ = require("jquery");
var Filt = require("../filt/filt.js");
var utils = require("../utils.js");
-
var common = require("./common.js");
var actions = require("../actions.js");
+var Query = require("../actions.js").Query;
var FilterDocs = React.createClass({
statics: {
@@ -30,12 +30,12 @@ var FilterDocs = React.createClass({
return <i className="fa fa-spinner fa-spin"></i>;
} else {
var commands = FilterDocs.doc.commands.map(function (c) {
- return <tr>
+ return <tr key={c[1]}>
<td>{c[0].replace(" ", '\u00a0')}</td>
<td>{c[1]}</td>
</tr>;
});
- commands.push(<tr>
+ commands.push(<tr key="docs-link">
<td colSpan="2">
<a href="https://mitmproxy.org/doc/features/filters.html"
target="_blank">
@@ -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] || "";
@@ -356,15 +356,17 @@ var Header = React.createClass({
},
render: function () {
var header = header_entries.map(function (entry, i) {
- var classes = React.addons.classSet({
- active: entry == this.state.active
- });
+ var className;
+ if(entry === this.state.active){
+ className = "active";
+ } else {
+ className = "";
+ }
return (
<a key={i}
href="#"
- className={classes}
- onClick={this.handleClick.bind(this, entry)}
- >
+ className={className}
+ onClick={this.handleClick.bind(this, entry)}>
{ entry.title}
</a>
);
diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js
index 550a61da..81bf3b03 100644
--- a/web/src/js/components/mainview.js
+++ b/web/src/js/components/mainview.js
@@ -2,24 +2,19 @@ var React = require("react");
var common = require("./common.js");
var actions = require("../actions.js");
+var Query = require("../actions.js").Query;
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],
getInitialState: function () {
- this.onQueryChange(Query.FILTER, function () {
- this.state.view.recalculate(this.getViewFilt(), this.getViewSort());
- }.bind(this));
- this.onQueryChange(Query.HIGHLIGHT, function () {
- this.state.view.recalculate(this.getViewFilt(), this.getViewSort());
- }.bind(this));
return {
- flows: []
+ flows: [],
+ sortKeyFun: false
};
},
getViewFilt: function () {
@@ -39,16 +34,20 @@ var MainView = React.createClass({
return filt(flow);
};
},
- getViewSort: function () {
- },
componentWillReceiveProps: function (nextProps) {
if (nextProps.flowStore !== this.props.flowStore) {
this.closeView();
this.openView(nextProps.flowStore);
}
+
+ var filterChanged = (this.props.query[Query.FILTER] !== nextProps.query[Query.FILTER]);
+ var highlightChanged = (this.props.query[Query.HIGHLIGHT] !== nextProps.query[Query.HIGHLIGHT]);
+ if (filterChanged || highlightChanged) {
+ this.state.view.recalculate(this.getViewFilt(), this.state.sortKeyFun);
+ }
},
openView: function (store) {
- var view = new views.StoreView(store, this.getViewFilt(), this.getViewSort());
+ var view = new views.StoreView(store, this.getViewFilt(), this.state.sortKeyFun);
this.setState({
view: view
});
@@ -73,7 +72,7 @@ var MainView = React.createClass({
},
onRemove: function (flow_id, index) {
if (flow_id === this.getParams().flowId) {
- var flow_to_select = this.state.view.list[Math.min(index, this.state.view.list.length -1)];
+ var flow_to_select = this.state.view.list[Math.min(index, this.state.view.list.length - 1)];
this.selectFlow(flow_to_select);
}
},
@@ -86,6 +85,12 @@ var MainView = React.createClass({
componentWillUnmount: function () {
this.closeView();
},
+ setSortKeyFun: function(sortKeyFun){
+ this.setState({
+ sortKeyFun: sortKeyFun
+ });
+ this.state.view.recalculate(this.getViewFilt(), sortKeyFun);
+ },
selectFlow: function (flow) {
if (flow) {
this.replaceWith(
@@ -194,10 +199,12 @@ var MainView = React.createClass({
}
break;
case toputils.Key.V:
- if(e.shiftKey && flow && flow.modified) {
+ if (e.shiftKey && flow && flow.modified) {
actions.FlowActions.revert(flow);
}
break;
+ case toputils.Key.SHIFT:
+ break;
default:
console.debug("keydown", e.keyCode);
return;
@@ -214,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;
@@ -225,6 +232,7 @@ var MainView = React.createClass({
<FlowTable ref="flowTable"
view={this.state.view}
selectFlow={this.selectFlow}
+ setSortKeyFun={this.setSortKeyFun}
selected={selected} />
{details}
</div>
diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js
index c5d7491d..863a9f53 100644
--- a/web/src/js/components/proxyapp.js
+++ b/web/src/js/components/proxyapp.js
@@ -8,6 +8,7 @@ var Footer = require("./footer.js");
var header = require("./header.js");
var EventLog = require("./eventlog.js");
var store = require("../store/store.js");
+var Query = require("../actions.js").Query;
//TODO: Move out of here, just a stub.
@@ -47,7 +48,6 @@ var ProxyAppMain = React.createClass({
});
},
render: function () {
-
var eventlog;
if (this.getQuery()[Query.SHOW_EVENTLOG]) {
eventlog = [
@@ -57,11 +57,13 @@ var ProxyAppMain = React.createClass({
} else {
eventlog = null;
}
-
return (
<div id="container">
<header.Header settings={this.state.settings.dict}/>
- <RouteHandler settings={this.state.settings.dict} flowStore={this.state.flowStore}/>
+ <RouteHandler
+ settings={this.state.settings.dict}
+ flowStore={this.state.flowStore}
+ query={this.getQuery()}/>
{eventlog}
<Footer settings={this.state.settings.dict}/>
</div>
@@ -88,5 +90,4 @@ var routes = (
module.exports = {
routes: routes
-};
-
+}; \ No newline at end of file
diff --git a/web/src/js/connection.js b/web/src/js/connection.js
index 85514c3c..5e229b6e 100644
--- a/web/src/js/connection.js
+++ b/web/src/js/connection.js
@@ -1,5 +1,6 @@
var actions = require("./actions.js");
+var AppDispatcher = require("./dispatcher.js").AppDispatcher;
function Connection(url) {
if (url[0] === "/") {
@@ -16,11 +17,11 @@ function Connection(url) {
};
ws.onerror = function () {
actions.ConnectionActions.error();
- EventLogActions.add_event("WebSocket connection error.");
+ actions.EventLogActions.add_event("WebSocket connection error.");
};
ws.onclose = function () {
actions.ConnectionActions.close();
- EventLogActions.add_event("WebSocket connection closed.");
+ actions.EventLogActions.add_event("WebSocket connection closed.");
};
return ws;
}
diff --git a/web/src/js/dispatcher.js b/web/src/js/dispatcher.js
index 040c34db..0c2aa202 100644
--- a/web/src/js/dispatcher.js
+++ b/web/src/js/dispatcher.js
@@ -7,7 +7,7 @@ const PayloadSources = {
};
-AppDispatcher = new flux.Dispatcher();
+var AppDispatcher = new flux.Dispatcher();
AppDispatcher.dispatchViewAction = function (action) {
action.source = PayloadSources.VIEW;
this.dispatch(action);
diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js
index a95d4ffe..29462a78 100644
--- a/web/src/js/flow/utils.js
+++ b/web/src/js/flow/utils.js
@@ -1,8 +1,9 @@
var _ = require("lodash");
+var $ = require("jquery");
-var _MessageUtils = {
+var MessageUtils = {
getContentType: function (message) {
- return this.get_first_header(message, /^Content-Type$/i);
+ return this.get_first_header(message, /^Content-Type$/i).split(";")[0].trim();
},
get_first_header: function (message, regex) {
//FIXME: Cache Invalidation.
@@ -34,6 +35,18 @@ var _MessageUtils = {
}
}
return false;
+ },
+ getContentURL: function(flow, message){
+ if(message === flow.request){
+ message = "request";
+ } else if (message === flow.response){
+ message = "response";
+ }
+ return "/flows/" + flow.id + "/" + message + "/content";
+ },
+ getContent: function(flow, message){
+ var url = MessageUtils.getContentURL(flow, message);
+ return $.get(url);
}
};
@@ -42,7 +55,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 +69,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 b5db9287..d13822d5 100644
--- a/web/src/js/store/view.js
+++ b/web/src/js/store/view.js
@@ -1,8 +1,6 @@
-
var EventEmitter = require('events').EventEmitter;
var _ = require("lodash");
-
var utils = require("../utils.js");
function SortByStoreOrder(elem) {
@@ -10,14 +8,12 @@ function SortByStoreOrder(elem) {
}
var default_sort = SortByStoreOrder;
-var default_filt = function(elem){
+var default_filt = function (elem) {
return true;
};
function StoreView(store, filt, sortfun) {
EventEmitter.call(this);
- filt = filt || default_filt;
- sortfun = sortfun || default_sort;
this.store = store;
@@ -39,19 +35,27 @@ _.extend(StoreView.prototype, EventEmitter.prototype, {
this.store.removeListener("update", this.update);
this.store.removeListener("remove", this.remove);
this.store.removeListener("recalculate", this.recalculate);
- },
- recalculate: function (filt, sortfun) {
- if (filt) {
- this.filt = filt.bind(this);
- }
- if (sortfun) {
- this.sortfun = sortfun.bind(this);
- }
+ },
+ recalculate: function (filt, sortfun) {
+ filt = filt || this.filt || default_filt;
+ sortfun = sortfun || this.sortfun || default_sort;
+ filt = filt.bind(this);
+ sortfun = sortfun.bind(this);
+ this.filt = filt;
+ this.sortfun = sortfun;
- this.list = this.store.list.filter(this.filt);
+ this.list = this.store.list.filter(filt);
this.list.sort(function (a, b) {
- return this.sortfun(a) - this.sortfun(b);
- }.bind(this));
+ var akey = sortfun(a);
+ var bkey = sortfun(b);
+ if(akey < bkey){
+ return -1;
+ } else if(akey > bkey){
+ return 1;
+ } else {
+ return 0;
+ }
+ });
this.emit("recalculate");
},
index: function (elem) {
diff --git a/web/src/js/utils.js b/web/src/js/utils.js
index 21b7a868..be59db96 100644
--- a/web/src/js/utils.js
+++ b/web/src/js/utils.js
@@ -1,5 +1,6 @@
var $ = require("jquery");
-
+var _ = require("lodash");
+var actions = require("./actions.js");
var Key = {
UP: 38,
@@ -15,6 +16,7 @@ var Key = {
TAB: 9,
SPACE: 32,
BACKSPACE: 8,
+ SHIFT: 16
};
// Add A-Z
for (var i = 65; i <= 90; i++) {
@@ -59,8 +61,20 @@ var formatTimeStamp = function (seconds) {
};
+// At some places, we need to sort strings alphabetically descending,
+// but we can only provide a key function.
+// This beauty "reverses" a JS string.
+var end = String.fromCharCode(0xffff);
+function reverseString(s){
+ return String.fromCharCode.apply(String,
+ _.map(s.split(""), function (c) {
+ return 0xffff - c.charCodeAt(0);
+ })
+ ) + end;
+}
+
function getCookie(name) {
- var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
+ var r = document.cookie.match(new RegExp("\\b" + name + "=([^;]*)\\b"));
return r ? r[1] : undefined;
}
var xsrf = $.param({_xsrf: getCookie("_xsrf")});
@@ -77,15 +91,18 @@ $.ajaxPrefilter(function (options) {
});
// Log AJAX Errors
$(document).ajaxError(function (event, jqXHR, ajaxSettings, thrownError) {
+ if(thrownError === "abort"){
+ return;
+ }
var message = jqXHR.responseText;
- console.error(message, arguments);
- EventLogActions.add_event(thrownError + ": " + message);
- window.alert(message);
+ console.error(thrownError, message, arguments);
+ actions.EventLogActions.add_event(thrownError + ": " + message);
});
module.exports = {
formatSize: formatSize,
formatTimeDelta: formatTimeDelta,
formatTimeStamp: formatTimeStamp,
+ reverseString: reverseString,
Key: Key
}; \ No newline at end of file