aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.codecov.yml (renamed from codecov.yml)0
-rw-r--r--README.rst22
-rw-r--r--mitmproxy/web/static/app.css2
-rw-r--r--mitmproxy/web/static/app.js144
-rw-r--r--netlib/http/http2/__init__.py2
-rw-r--r--netlib/http/http2/connections.py432
-rw-r--r--pathod/pathoc.py16
-rw-r--r--pathod/protocols/http.py8
-rw-r--r--pathod/protocols/http2.py439
-rw-r--r--test/netlib/http/http2/test_framereader.py1
-rw-r--r--test/pathod/__init__.py1
-rw-r--r--test/pathod/test_language_actions.py19
-rw-r--r--test/pathod/test_language_base.py3
-rw-r--r--test/pathod/test_language_generators.py2
-rw-r--r--test/pathod/test_language_http.py3
-rw-r--r--test/pathod/test_language_http2.py7
-rw-r--r--test/pathod/test_language_websocket.py3
-rw-r--r--test/pathod/test_pathoc.py12
-rw-r--r--test/pathod/test_pathoc_cmdline.py6
-rw-r--r--test/pathod/test_pathod.py19
-rw-r--r--test/pathod/test_pathod_cmdline.py6
-rw-r--r--test/pathod/test_protocols_http2.py (renamed from test/netlib/http/http2/test_connections.py)97
-rw-r--r--test/pathod/test_test.py3
-rw-r--r--test/pathod/test_utils.py3
-rw-r--r--test/pathod/tutils.py2
-rw-r--r--tox.ini2
-rw-r--r--web/.editorconfig1
-rw-r--r--web/src/css/eventlog.less4
-rw-r--r--web/src/js/components/ContentView.jsx78
-rw-r--r--web/src/js/components/ContentView/ContentErrors.jsx28
-rw-r--r--web/src/js/components/ContentView/ContentLoader.jsx67
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx70
-rw-r--r--web/src/js/components/ContentView/ViewSelector.jsx28
-rw-r--r--web/src/js/components/EventLog.jsx81
-rw-r--r--web/src/js/components/FlowTable/FlowTableHead.jsx6
-rw-r--r--web/src/js/components/FlowView.jsx107
-rw-r--r--web/src/js/components/FlowView/Details.jsx133
-rw-r--r--web/src/js/components/FlowView/Headers.jsx130
-rw-r--r--web/src/js/components/FlowView/Messages.jsx168
-rw-r--r--web/src/js/components/FlowView/Nav.jsx57
-rw-r--r--web/src/js/components/Footer.jsx1
-rw-r--r--web/src/js/components/Header.jsx (renamed from web/src/js/components/Header.js)0
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx8
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx5
-rw-r--r--web/src/js/components/Header/ViewMenu.jsx2
-rw-r--r--web/src/js/components/MainView.jsx4
-rwxr-xr-xweb/src/js/components/Prompt.jsx71
-rw-r--r--web/src/js/components/ProxyApp.jsx27
-rwxr-xr-xweb/src/js/components/ValueEditor.jsx36
-rwxr-xr-xweb/src/js/components/ValueEditor/EditorBase.jsx166
-rwxr-xr-xweb/src/js/components/ValueEditor/ValidateEditor.jsx58
-rw-r--r--web/src/js/components/common.js173
-rw-r--r--web/src/js/components/common/Button.jsx16
-rw-r--r--web/src/js/components/common/Splitter.jsx99
-rw-r--r--web/src/js/components/common/ToggleButton.jsx17
-rw-r--r--web/src/js/components/common/ToggleInputButton.jsx52
-rw-r--r--web/src/js/components/editor.js238
-rw-r--r--web/src/js/components/flowview/contentview.js267
-rw-r--r--web/src/js/components/flowview/details.js181
-rw-r--r--web/src/js/components/flowview/index.js114
-rw-r--r--web/src/js/components/flowview/messages.js320
-rw-r--r--web/src/js/components/flowview/nav.js61
-rw-r--r--web/src/js/components/prompt.js102
63 files changed, 2127 insertions, 2103 deletions
diff --git a/codecov.yml b/.codecov.yml
index db247200..db247200 100644
--- a/codecov.yml
+++ b/.codecov.yml
diff --git a/README.rst b/README.rst
index 4a665360..983f50fb 100644
--- a/README.rst
+++ b/README.rst
@@ -94,6 +94,21 @@ requirements installed, and you can simply run the test suite:
Please ensure that all patches are accompanied by matching changes in the test
suite. The project tries to maintain 100% test coverage.
+You can also use `tox` to run a full suite of tests in Python 2.7 and 3.5,
+including a quick test to check documentation and code linting.
+
+The following tox environments are relevant for local testing:
+
+.. code-block:: text
+
+ tox -e py27 # runs all tests with Python 2.7
+ tox -e py35 # runs all tests with Python 3.5 (partial support only)
+ tox -e docs # runs a does-it-compile check on the documentation
+ tox -e lint # runs the linter for coding style checks
+
+We are in the middle of transitioning to Python 3, so please make sure all tests
+pass in Python 2.7 and 3.5. Running `tox` ensure all necessary tests are executed.
+
Documentation
-------------
@@ -120,6 +135,13 @@ contribute and collaborate. Please stick to the guidelines in
`PEP8`_ and the `Google Style Guide`_ unless there's a very
good reason not to.
+This is automatically enforced on every PR. If we detect a linting error, the
+PR checks will fail and block merging. We are using this command to check for style compliance:
+
+.. code-block:: text
+
+ flake8 --jobs 8 --count mitmproxy netlib pathod examples test
+
.. |mitmproxy_site| image:: https://shields.mitmproxy.org/api/https%3A%2F%2F-mitmproxy.org-blue.svg
:target: https://mitmproxy.org/
diff --git a/mitmproxy/web/static/app.css b/mitmproxy/web/static/app.css
index bd4b57be..c2dd139f 100644
--- a/mitmproxy/web/static/app.css
+++ b/mitmproxy/web/static/app.css
@@ -1,2 +1,2 @@
-html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}.resource-icon{width:32px;height:32px}.resource-icon-css{background-image:url(images/chrome-devtools/resourceCSSIcon.png)}.resource-icon-document{background-image:url(images/chrome-devtools/resourceDocumentIcon.png)}.resource-icon-js{background-image:url(images/chrome-devtools/resourceJSIcon.png)}.resource-icon-plain{background-image:url(images/chrome-devtools/resourcePlainIcon.png)}.resource-icon-executable{background-image:url(images/resourceExecutableIcon.png)}.resource-icon-flash{background-image:url(images/resourceFlashIcon.png)}.resource-icon-image{background-image:url(images/resourceImageIcon.png)}.resource-icon-java{background-image:url(images/resourceJavaIcon.png)}.resource-icon-not-modified{background-image:url(images/resourceNotModifiedIcon.png)}.resource-icon-redirect{background-image:url(images/resourceRedirectIcon.png)}#container,#mitmproxy,body,html{height:100%;margin:0;overflow:hidden}#container{display:flex;flex-direction:column;outline:0}#container>.eventlog,#container>footer,#container>header,.eventlog{flex:0 0 auto}.main-view{flex:1 1 auto;height:0;display:flex;flex-direction:row}.main-view.vertical{flex-direction:column}.main-view .flow-detail,.main-view .flow-table{flex:1 1 auto}.splitter{flex:0 0 1px;background-color:#aaa;position:relative}.splitter>div{position:absolute}.splitter.splitter-x{cursor:col-resize}.splitter.splitter-x>div{margin-left:-1px;width:4px;height:100%}.splitter.splitter-y{cursor:row-resize}.eventlog .label,.flow-table tr,.prompt-content .option{cursor:pointer}.splitter.splitter-y>div{margin-top:-1px;height:4px;width:100%}.nav-tabs{border-bottom:solid #a6a6a6 1px}.nav-tabs a{display:inline-block;border:1px solid transparent;text-decoration:none}.nav-tabs a.active{background-color:#fff;border-color:#a6a6a6 #a6a6a6 #fff}.nav-tabs a.special{color:#fff;background-color:#396cad;border-bottom-color:#396cad}.nav-tabs a.special:hover{background-color:#5386c6}.nav-tabs-lg a{padding:3px 14px;margin:0 2px -1px}.nav-tabs-sm a{padding:0 7px;margin:2px 2px -1px}.nav-tabs-sm a.nav-action{float:right;padding:0;margin:1px 0 0}header{padding-top:.5em;background-color:#fff}header .menu{padding:10px;border-bottom:solid #a6a6a6 1px}.menu-row{margin-left:-2px;margin-right:-3px}.filter-input{position:relative;min-height:1px;padding-left:2.5px;padding-right:2.5px;margin-bottom:5px}@media (min-width:768px){.filter-input{float:left;width:25%}}.filter-input .popover{top:27px;display:block;max-width:none;opacity:.9}.filter-input .popover .popover-content{max-height:500px;overflow-y:auto}.flow-table{width:100%;overflow-y:scroll;overflow-x:hidden}.flow-table table{width:100%;table-layout:fixed}.flow-table thead{background-color:#F2F2F2;line-height:23px}.flow-table th{font-weight:400;box-shadow:0 1px 0 #a6a6a6;position:relative!important;padding-left:1px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.prompt-content,footer{box-shadow:0 -1px 3px #d3d3d3}.flow-table th.sort-asc,.flow-table th.sort-desc{background-color:#fafafa}.flow-table th.sort-asc:after,.flow-table th.sort-desc:after{font:normal normal normal 14px/1 FontAwesome;position:absolute;right:3px;top:3px;padding:2px;background-color:rgba(250,250,250,.8)}.flow-detail .first-line,.flow-detail table{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;word-break:break-all}.prompt-content,.prompt-dialog{position:fixed;bottom:0;left:0;right:0}.flow-table th.sort-asc:after{content:"\f0de"}.flow-table th.sort-desc:after{content:"\f0dd"}.flow-table tr:nth-child(even){background-color:rgba(0,0,0,.05)}.flow-table tr.selected{background-color:rgba(193,215,235,.5)!important}.flow-table tr.highlighted{background-color:rgba(255,204,0,.4)}.flow-table tr.highlighted:nth-child(even){background-color:rgba(255,204,0,.5)}.flow-table td{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.flow-table tr.intercepted.has-response .col-size,.flow-table tr.intercepted.has-response .col-status,.flow-table tr.intercepted.has-response .col-time,.flow-table tr.intercepted:not(.has-response) .col-method,.flow-table tr.intercepted:not(.has-response) .col-path{color:#ff8000}.flow-table .fa{line-height:inherit}.flow-table .fa.pull-right{margin-left:0}.flow-table .col-tls{width:10px}.flow-table .col-tls-https{background-color:rgba(0,185,0,.5)}.flow-table .col-icon{width:32px}.flow-table .col-path .fa-repeat{color:green}.flow-table .col-path .fa-pause{color:#ff8000}.flow-table .col-method{width:60px}.flow-table .col-status{width:50px}.flow-table .col-size{width:70px}.flow-table .col-time{width:50px}.flow-table td.col-size,.flow-table td.col-time{text-align:right}.flow-detail{width:100%;overflow-x:auto;overflow-y:scroll}.flow-detail nav{background-color:#F2F2F2}.flow-detail section{padding:5px 12px}.flow-detail .first-line{background-color:#428bca;color:#fff;margin:0 -8px;padding:4px 8px;border-radius:5px;max-height:100px;overflow-y:auto}.flow-detail .request-line{margin-bottom:2px}.flow-detail hr{margin:0 0 5px}.inline-input{margin:0 -5px;padding:0 5px}.inline-input[contenteditable]{background-color:rgba(255,255,255,.2)}.inline-input[contenteditable].has-warning{color:#ffb8b8}.view-options{margin-top:10px}.flow-detail table{width:100%;table-layout:fixed}.flow-detail table tr:not(:first-child){border-top:1px solid #f7f7f7}.flow-detail table td{vertical-align:top}.connection-table td:first-child{width:50%;padding-right:1em}.header-table td{line-height:1.3em}.header-table .header-name{width:33%;padding-right:1em}.connection-table td,.timing-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.flowview-image{text-align:center}.flowview-image img{max-width:100%;max-height:100%}.prompt-dialog{top:0;z-index:100;background-color:rgba(0,0,0,.1)}.prompt-content{height:25px;padding:2px 5px;background-color:#fff}.prompt-content .option:not(:last-child)::after{content:", "}.eventlog{height:200px;display:flex;flex-direction:column}.eventlog>div{background-color:#F2F2F2;padding:0 5px;flex:0 0 auto}.eventlog>pre{flex:1 1 auto;margin:0;border-radius:0;overflow-x:auto;overflow-y:scroll;background-color:#fcfcfc}.eventlog .fa-close{cursor:pointer;float:right;color:grey;padding:3px 0 3px 10px}.eventlog .fa-close:hover{color:#000}.eventlog .btn-toggle{margin-top:-2px;margin-left:3px;padding:2px;font-size:10px;line-height:10px;border-radius:2px}.eventlog .label{vertical-align:middle;display:inline-block;margin-top:-2px;margin-left:3px}footer{padding:0 10px 3px}footer .label{margin-right:3px}
+html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}.resource-icon{width:32px;height:32px}.resource-icon-css{background-image:url(images/chrome-devtools/resourceCSSIcon.png)}.resource-icon-document{background-image:url(images/chrome-devtools/resourceDocumentIcon.png)}.resource-icon-js{background-image:url(images/chrome-devtools/resourceJSIcon.png)}.resource-icon-plain{background-image:url(images/chrome-devtools/resourcePlainIcon.png)}.resource-icon-executable{background-image:url(images/resourceExecutableIcon.png)}.resource-icon-flash{background-image:url(images/resourceFlashIcon.png)}.resource-icon-image{background-image:url(images/resourceImageIcon.png)}.resource-icon-java{background-image:url(images/resourceJavaIcon.png)}.resource-icon-not-modified{background-image:url(images/resourceNotModifiedIcon.png)}.resource-icon-redirect{background-image:url(images/resourceRedirectIcon.png)}#container,#mitmproxy,body,html{height:100%;margin:0;overflow:hidden}#container{display:flex;flex-direction:column;outline:0}#container>.eventlog,#container>footer,#container>header,.eventlog{flex:0 0 auto}.main-view{flex:1 1 auto;height:0;display:flex;flex-direction:row}.main-view.vertical{flex-direction:column}.main-view .flow-detail,.main-view .flow-table{flex:1 1 auto}.splitter{flex:0 0 1px;background-color:#aaa;position:relative}.splitter>div{position:absolute}.splitter.splitter-x{cursor:col-resize}.splitter.splitter-x>div{margin-left:-1px;width:4px;height:100%}.splitter.splitter-y{cursor:row-resize}.splitter.splitter-y>div{margin-top:-1px;height:4px;width:100%}.nav-tabs{border-bottom:solid #a6a6a6 1px}.nav-tabs a{display:inline-block;border:1px solid transparent;text-decoration:none}.nav-tabs a.active{background-color:#fff;border-color:#a6a6a6 #a6a6a6 #fff}.nav-tabs a.special{color:#fff;background-color:#396cad;border-bottom-color:#396cad}.nav-tabs a.special:hover{background-color:#5386c6}.nav-tabs-lg a{padding:3px 14px;margin:0 2px -1px}.nav-tabs-sm a{padding:0 7px;margin:2px 2px -1px}.nav-tabs-sm a.nav-action{float:right;padding:0;margin:1px 0 0}header{padding-top:.5em;background-color:#fff}header .menu{padding:10px;border-bottom:solid #a6a6a6 1px}.menu-row{margin-left:-2px;margin-right:-3px}.filter-input{position:relative;min-height:1px;padding-left:2.5px;padding-right:2.5px;margin-bottom:5px}@media (min-width:768px){.filter-input{float:left;width:25%}}.filter-input .popover{top:27px;display:block;max-width:none;opacity:.9}.filter-input .popover .popover-content{max-height:500px;overflow-y:auto}.flow-table{width:100%;overflow-y:scroll;overflow-x:hidden}.flow-table table{width:100%;table-layout:fixed}.flow-table thead{background-color:#F2F2F2;line-height:23px}.flow-table th{font-weight:400;box-shadow:0 1px 0 #a6a6a6;position:relative!important;padding-left:1px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.prompt-content,footer{box-shadow:0 -1px 3px #d3d3d3}.flow-table th.sort-asc,.flow-table th.sort-desc{background-color:#fafafa}.flow-table th.sort-asc:after,.flow-table th.sort-desc:after{font:normal normal normal 14px/1 FontAwesome;position:absolute;right:3px;top:3px;padding:2px;background-color:rgba(250,250,250,.8)}.flow-detail .first-line,.flow-detail table{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;word-break:break-all}.prompt-content,.prompt-dialog{position:fixed;bottom:0;left:0;right:0}.flow-table th.sort-asc:after{content:"\f0de"}.flow-table th.sort-desc:after{content:"\f0dd"}.flow-table tr{cursor:pointer}.flow-table tr:nth-child(even){background-color:rgba(0,0,0,.05)}.flow-table tr.selected{background-color:rgba(193,215,235,.5)!important}.flow-table tr.highlighted{background-color:rgba(255,204,0,.4)}.flow-table tr.highlighted:nth-child(even){background-color:rgba(255,204,0,.5)}.flow-table td{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.flow-table tr.intercepted.has-response .col-size,.flow-table tr.intercepted.has-response .col-status,.flow-table tr.intercepted.has-response .col-time,.flow-table tr.intercepted:not(.has-response) .col-method,.flow-table tr.intercepted:not(.has-response) .col-path{color:#ff8000}.flow-table .fa{line-height:inherit}.flow-table .fa.pull-right{margin-left:0}.flow-table .col-tls{width:10px}.flow-table .col-tls-https{background-color:rgba(0,185,0,.5)}.flow-table .col-icon{width:32px}.flow-table .col-path .fa-repeat{color:green}.flow-table .col-path .fa-pause{color:#ff8000}.flow-table .col-method{width:60px}.flow-table .col-status{width:50px}.flow-table .col-size{width:70px}.flow-table .col-time{width:50px}.flow-table td.col-size,.flow-table td.col-time{text-align:right}.flow-detail{width:100%;overflow-x:auto;overflow-y:scroll}.flow-detail nav{background-color:#F2F2F2}.flow-detail section{padding:5px 12px}.flow-detail .first-line{background-color:#428bca;color:#fff;margin:0 -8px;padding:4px 8px;border-radius:5px;max-height:100px;overflow-y:auto}.flow-detail .request-line{margin-bottom:2px}.flow-detail hr{margin:0 0 5px}.inline-input{margin:0 -5px;padding:0 5px}.inline-input[contenteditable]{background-color:rgba(255,255,255,.2)}.inline-input[contenteditable].has-warning{color:#ffb8b8}.view-options{margin-top:10px}.flow-detail table{width:100%;table-layout:fixed}.flow-detail table tr:not(:first-child){border-top:1px solid #f7f7f7}.flow-detail table td{vertical-align:top}.connection-table td:first-child{width:50%;padding-right:1em}.header-table td{line-height:1.3em}.header-table .header-name{width:33%;padding-right:1em}.connection-table td,.timing-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.flowview-image{text-align:center}.flowview-image img{max-width:100%;max-height:100%}.prompt-dialog{top:0;z-index:100;background-color:rgba(0,0,0,.1)}.prompt-content{height:25px;padding:2px 5px;background-color:#fff}.prompt-content .option{cursor:pointer}.prompt-content .option:not(:last-child)::after{content:", "}.eventlog{height:200px;display:flex;flex-direction:column}.eventlog>div{background-color:#F2F2F2;padding:0 5px;flex:0 0 auto;border-top:1px solid #aaa;cursor:row-resize}.eventlog>pre{flex:1 1 auto;margin:0;border-radius:0;overflow-x:auto;overflow-y:scroll;background-color:#fcfcfc}.eventlog .fa-close{cursor:pointer;float:right;color:grey;padding:3px 0 3px 10px}.eventlog .fa-close:hover{color:#000}.eventlog .btn-toggle{margin-top:-2px;margin-left:3px;padding:2px;font-size:10px;line-height:10px;border-radius:2px}.eventlog .label{cursor:pointer;vertical-align:middle;display:inline-block;margin-top:-2px;margin-left:3px}footer{padding:0 10px 3px}footer .label{margin-right:3px}
/*# sourceMappingURL=app.css.map */
diff --git a/mitmproxy/web/static/app.js b/mitmproxy/web/static/app.js
index a6833775..22706a7f 100644
--- a/mitmproxy/web/static/app.js
+++ b/mitmproxy/web/static/app.js
@@ -4,124 +4,154 @@ function EventEmitter(){this._events=this._events||{},this._maxListeners=this._m
},{}],2:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.Query=exports.FlowActions=exports.SettingsActions=exports.ConnectionActions=exports.StoreCmds=exports.ActionTypes=void 0;var _jquery=require("jquery"),_jquery2=_interopRequireDefault(_jquery),_dispatcher=require("./dispatcher.js"),_utils=require("./utils.js"),ActionTypes=exports.ActionTypes={CONNECTION_OPEN:"connection_open",CONNECTION_CLOSE:"connection_close",CONNECTION_ERROR:"connection_error",SETTINGS_STORE:"settings",EVENT_STORE:"events",FLOW_STORE:"flows"},StoreCmds=exports.StoreCmds={ADD:"add",UPDATE:"update",REMOVE:"remove",RESET:"reset"},ConnectionActions=exports.ConnectionActions={open:function(){_dispatcher.AppDispatcher.dispatchViewAction({type:ActionTypes.CONNECTION_OPEN})},close:function(){_dispatcher.AppDispatcher.dispatchViewAction({type:ActionTypes.CONNECTION_CLOSE})},error:function(){_dispatcher.AppDispatcher.dispatchViewAction({type:ActionTypes.CONNECTION_ERROR})}},SettingsActions=exports.SettingsActions={update:function(e){_jquery2["default"].ajax({type:"PUT",url:"/settings",contentType:"application/json",data:JSON.stringify(e)})}},FlowActions=exports.FlowActions={accept:function(e){_jquery2["default"].post("/flows/"+e.id+"/accept")},accept_all:function(){_jquery2["default"].post("/flows/accept")},"delete":function(e){_jquery2["default"].ajax({type:"DELETE",url:"/flows/"+e.id})},duplicate:function(e){_jquery2["default"].post("/flows/"+e.id+"/duplicate")},replay:function(e){_jquery2["default"].post("/flows/"+e.id+"/replay")},revert:function(e){_jquery2["default"].post("/flows/"+e.id+"/revert")},update:function(e,t){_jquery2["default"].ajax({type:"PUT",url:"/flows/"+e.id,contentType:"application/json",data:JSON.stringify(t)})},clear:function(){_jquery2["default"].post("/clear")},download:function(){return window.location="/flows/dump"},upload:function(e){var t=new FormData;t.append("file",e),(0,_utils.fetchApi)("/flows/dump",{method:"post",body:t})}},Query=exports.Query={SEARCH:"s",HIGHLIGHT:"h",SHOW_EVENTLOG:"e"};
-},{"./dispatcher.js":31,"./utils.js":42,"jquery":"jquery"}],3:[function(require,module,exports){
+},{"./dispatcher.js":41,"./utils.js":52,"jquery":"jquery"}],3:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}var _react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_redux=require("redux"),_reactRedux=require("react-redux"),_reduxLogger=require("redux-logger"),_reduxLogger2=_interopRequireDefault(_reduxLogger),_reduxThunk=require("redux-thunk"),_reduxThunk2=_interopRequireDefault(_reduxThunk),_reactRouter=require("react-router"),_ProxyApp=require("./components/ProxyApp"),_ProxyApp2=_interopRequireDefault(_ProxyApp),_MainView=require("./components/MainView"),_MainView2=_interopRequireDefault(_MainView),_index=require("./ducks/index"),_index2=_interopRequireDefault(_index),_eventLog=require("./ducks/eventLog"),store=(0,_redux.createStore)(_index2["default"],(0,_redux.applyMiddleware)(_reduxThunk2["default"],(0,_reduxLogger2["default"])()));window.addEventListener("error",function(e){store.dispatch((0,_eventLog.addLogEntry)(e))}),document.addEventListener("DOMContentLoaded",function(){(0,_reactDom.render)(_react2["default"].createElement(_reactRedux.Provider,{store:store},_react2["default"].createElement(_reactRouter.Router,{history:_reactRouter.hashHistory},_react2["default"].createElement(_reactRouter.Redirect,{from:"/",to:"/flows"}),_react2["default"].createElement(_reactRouter.Route,{path:"/",component:_ProxyApp2["default"]},_react2["default"].createElement(_reactRouter.Route,{path:"flows",component:_MainView2["default"]}),_react2["default"].createElement(_reactRouter.Route,{path:"flows/:flowId/:detailTab",component:_MainView2["default"]})))),document.getElementById("mitmproxy"))});
-},{"./components/MainView":19,"./components/ProxyApp":20,"./ducks/eventLog":32,"./ducks/index":34,"react":"react","react-dom":"react-dom","react-redux":"react-redux","react-router":"react-router","redux":"redux","redux-logger":"redux-logger","redux-thunk":"redux-thunk"}],4:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function EventLog(e){var t=e.filters,r=e.events,n=e.onToggleFilter,o=e.onClose;return _react2["default"].createElement("div",{className:"eventlog"},_react2["default"].createElement("div",null,"Eventlog",_react2["default"].createElement("div",{className:"pull-right"},["debug","info","web"].map(function(e){return _react2["default"].createElement(_common.ToggleButton,{key:e,text:e,checked:t[e],onToggle:function(){return n(e)}})}),_react2["default"].createElement("i",{onClick:o,className:"fa fa-close"}))),_react2["default"].createElement(_EventList2["default"],{events:r}))}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_eventLog=require("../ducks/eventLog"),_common=require("./common"),_EventList=require("./EventLog/EventList"),_EventList2=_interopRequireDefault(_EventList);EventLog.propTypes={filters:_react.PropTypes.object.isRequired,events:_react.PropTypes.array.isRequired,onToggleFilter:_react.PropTypes.func.isRequired,onClose:_react.PropTypes.func.isRequired},exports["default"]=(0,_reactRedux.connect)(function(e){return{filters:e.eventLog.filter,events:e.eventLog.filteredEvents}},{onClose:_eventLog.toggleEventLogVisibility,onToggleFilter:_eventLog.toggleEventLogFilter})(EventLog);
+},{"./components/MainView":29,"./components/ProxyApp":31,"./ducks/eventLog":42,"./ducks/index":44,"react":"react","react-dom":"react-dom","react-redux":"react-redux","react-router":"react-router","redux":"redux","redux-logger":"redux-logger","redux-thunk":"redux-thunk"}],4:[function(require,module,exports){
+"use strict";function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t["default"]=e,t}function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e},_createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,r,n){return r&&e(t.prototype,r),n&&e(t,n),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_utils=require("../flow/utils.js"),_ContentViews=require("./ContentView/ContentViews"),_ContentErrors=require("./ContentView/ContentErrors"),ContentErrors=_interopRequireWildcard(_ContentErrors),_ContentLoader=require("./ContentView/ContentLoader"),_ContentLoader2=_interopRequireDefault(_ContentLoader),_ViewSelector=require("./ContentView/ViewSelector"),_ViewSelector2=_interopRequireDefault(_ViewSelector),ContentView=function(e){function t(e,r){_classCallCheck(this,t);var n=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,r));return n.state={displayLarge:!1,View:_ContentViews.ViewAuto},n.selectView=n.selectView.bind(n),n}return _inherits(t,e),_createClass(t,[{key:"selectView",value:function(e){this.setState({View:e})}},{key:"displayLarge",value:function(){this.setState({displayLarge:!0})}},{key:"componentWillReceiveProps",value:function(e){e.message!==this.props.message&&this.setState({displayLarge:!1,View:_ContentViews.ViewAuto})}},{key:"isContentTooLarge",value:function(e){return e.contentLength>1048576*(_ContentViews.ViewImage.matches(e)?10:.2)}},{key:"render",value:function(){var e=this.props,t=e.flow,r=e.message,n=this.state,o=n.displayLarge,a=n.View;return 0===r.contentLength?_react2["default"].createElement(ContentErrors.ContentEmpty,this.props):null===r.contentLength?_react2["default"].createElement(ContentErrors.ContentMissing,this.props):!o&&this.isContentTooLarge(r)?_react2["default"].createElement(ContentErrors.ContentTooLarge,_extends({},this.props,{onClick:this.displayLarge})):_react2["default"].createElement("div",null,a.textView?_react2["default"].createElement(_ContentLoader2["default"],{flow:t,message:r},_react2["default"].createElement(this.state.View,{content:""})):_react2["default"].createElement(a,{flow:t,message:r}),_react2["default"].createElement("div",{className:"view-options text-center"},_react2["default"].createElement(_ViewSelector2["default"],{onSelectView:this.selectView,active:a,message:r})," ",_react2["default"].createElement("a",{className:"btn btn-default btn-xs",href:_utils.MessageUtils.getContentURL(t,r)},_react2["default"].createElement("i",{className:"fa fa-download"}))))}}]),t}(_react.Component);ContentView.propTypes={flow:_react2["default"].PropTypes.object.isRequired,message:_react2["default"].PropTypes.object.isRequired},exports["default"]=ContentView;
-},{"../ducks/eventLog":32,"./EventLog/EventList":5,"./common":21,"react":"react","react-redux":"react-redux"}],5:[function(require,module,exports){
+},{"../flow/utils.js":50,"./ContentView/ContentErrors":5,"./ContentView/ContentLoader":6,"./ContentView/ContentViews":7,"./ContentView/ViewSelector":8,"react":"react"}],5:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function ContentEmpty(e){var t=e.flow,n=e.message;return _react2["default"].createElement("div",{className:"alert alert-info"},"No ",t.request===n?"request":"response"," content.")}function ContentMissing(e){var t=e.flow,n=e.message;return _react2["default"].createElement("div",{className:"alert alert-info"},t.request===n?"Request":"Response"," content missing.")}function ContentTooLarge(e){var t=e.message,n=e.onClick;return _react2["default"].createElement("div",{className:"alert alert-warning"},_react2["default"].createElement("button",{onClick:n,className:"btn btn-xs btn-warning pull-right"},"Display anyway"),(0,_utils.formatSize)(t.contentLength)," content size.")}Object.defineProperty(exports,"__esModule",{value:!0}),exports.ContentEmpty=ContentEmpty,exports.ContentMissing=ContentMissing,exports.ContentTooLarge=ContentTooLarge;var _react=require("react"),_react2=_interopRequireDefault(_react),_ContentViews=require("./ContentViews"),_utils=require("../../utils.js");
+
+},{"../../utils.js":52,"./ContentViews":7,"react":"react"}],6:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_utils=require("../../flow/utils.js"),ContentLoader=function(e){function t(e,n){_classCallCheck(this,t);var r=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,n));return r.state={content:null,request:null},r}return _inherits(t,e),_createClass(t,[{key:"requestContent",value:function(e){var t=this;this.state.request&&this.state.request.abort();var n=_utils.MessageUtils.getContent(e.flow,e.message);this.setState({content:null,request:n}),n.done(function(e){t.setState({content:e})}).fail(function(e,n,r){"abort"!==n&&t.setState({content:"AJAX Error: "+n+"\r\n"+r})}).always(function(){t.setState({request:null})})}},{key:"componentWillMount",value:function(){this.requestContent(this.props)}},{key:"componentWillReceiveProps",value:function(e){e.message!==this.props.message&&this.requestContent(e)}},{key:"componentWillUnmount",value:function(){this.state.request&&this.state.request.abort()}},{key:"render",value:function(){return this.state.content?_react2["default"].cloneElement(this.props.children,{content:this.state.content}):_react2["default"].createElement("div",{className:"text-center"},_react2["default"].createElement("i",{className:"fa fa-spinner fa-spin"}))}}]),t}(_react.Component);ContentLoader.propTypes={flow:_react.PropTypes.object.isRequired,message:_react.PropTypes.object.isRequired},exports["default"]=ContentLoader;
+
+},{"../../flow/utils.js":50,"react":"react"}],7:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function ViewImage(e){var t=e.flow,i=e.message;return _react2["default"].createElement("div",{className:"flowview-image"},_react2["default"].createElement("img",{src:_utils.MessageUtils.getContentURL(t,i),alt:"preview",className:"img-thumbnail"}))}function ViewRaw(e){var t=e.content;return _react2["default"].createElement("pre",null,t)}function ViewJSON(e){var t=e.content,i=t;try{i=JSON.stringify(JSON.parse(t),null,2)}catch(r){}return _react2["default"].createElement("pre",null,i)}function ViewAuto(e){var t=e.message,i=e.flow,r=ViewAuto.findView(t);return r.textView?_react2["default"].createElement(_ContentLoader2["default"],{message:t,flow:i},_react2["default"].createElement(r,{content:""})):_react2["default"].createElement(r,{message:t,flow:i})}Object.defineProperty(exports,"__esModule",{value:!0}),exports.ViewImage=ViewImage,exports.ViewRaw=ViewRaw,exports.ViewJSON=ViewJSON,exports.ViewAuto=ViewAuto;var _react=require("react"),_react2=_interopRequireDefault(_react),_ContentLoader=require("./ContentLoader"),_ContentLoader2=_interopRequireDefault(_ContentLoader),_utils=require("../../flow/utils.js"),views=[ViewAuto,ViewImage,ViewJSON,ViewRaw];ViewImage.regex=/^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i,ViewImage.matches=function(e){return ViewImage.regex.test(_utils.MessageUtils.getContentType(e))},ViewImage.propTypes={flow:_react.PropTypes.object.isRequired,message:_react.PropTypes.object.isRequired},ViewRaw.textView=!0,ViewRaw.matches=function(){return!0},ViewRaw.propTypes={content:_react2["default"].PropTypes.string.isRequired},ViewJSON.textView=!0,ViewJSON.regex=/^application\/json$/i,ViewJSON.matches=function(e){return ViewJSON.regex.test(_utils.MessageUtils.getContentType(e))},ViewJSON.propTypes={content:_react2["default"].PropTypes.string.isRequired},ViewAuto.matches=function(){return!1},ViewAuto.findView=function(e){return views.find(function(t){return t.matches(e)})||views[views.length-1]},ViewAuto.propTypes={message:_react2["default"].PropTypes.object.isRequired,flow:_react2["default"].PropTypes.object.isRequired},exports["default"]=views;
+
+},{"../../flow/utils.js":50,"./ContentLoader":6,"react":"react"}],8:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function ViewSelector(e){var t=e.active,r=e.message,a=e.onSelectView;return _react2["default"].createElement("div",{className:"view-selector btn-group btn-group-xs"},_ContentViews2["default"].map(function(e){return _react2["default"].createElement("button",{key:e.name,onClick:function(){return a(e)},className:(0,_classnames2["default"])("btn btn-default",{active:e===t})},e===_ContentViews.ViewAuto?"auto: "+_ContentViews.ViewAuto.findView(r).name.toLowerCase().replace("view",""):e.name.toLowerCase().replace("view",""))}))}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=ViewSelector;var _react=require("react"),_react2=_interopRequireDefault(_react),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_ContentViews=require("./ContentViews"),_ContentViews2=_interopRequireDefault(_ContentViews);ViewSelector.propTypes={active:_react.PropTypes.func.isRequired,message:_react.PropTypes.object.isRequired,onSelectView:_react.PropTypes.func.isRequired};
+
+},{"./ContentViews":7,"classnames":"classnames","react":"react"}],9:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,r,n){return r&&e(t.prototype,r),n&&e(t,n),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_eventLog=require("../ducks/eventLog"),_ToggleButton=require("./common/ToggleButton"),_ToggleButton2=_interopRequireDefault(_ToggleButton),_EventList=require("./EventLog/EventList"),_EventList2=_interopRequireDefault(_EventList),EventLog=function(e){function t(e,r){_classCallCheck(this,t);var n=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,r));return n.state={height:n.props.defaultHeight},n.onDragStart=n.onDragStart.bind(n),n.onDragMove=n.onDragMove.bind(n),n.onDragStop=n.onDragStop.bind(n),n}return _inherits(t,e),_createClass(t,[{key:"onDragStart",value:function(e){e.preventDefault(),this.dragStart=this.state.height+e.pageY,window.addEventListener("mousemove",this.onDragMove),window.addEventListener("mouseup",this.onDragStop),window.addEventListener("dragend",this.onDragStop)}},{key:"onDragMove",value:function(e){e.preventDefault(),this.setState({height:this.dragStart-e.pageY})}},{key:"onDragStop",value:function(e){e.preventDefault(),window.removeEventListener("mousemove",this.onDragMove)}},{key:"render",value:function(){var e=this.state.height,t=this.props,r=t.filters,n=t.events,o=t.onToggleFilter,a=t.onClose;return _react2["default"].createElement("div",{className:"eventlog",style:{height:e}},_react2["default"].createElement("div",{onMouseDown:this.onDragStart},"Eventlog",_react2["default"].createElement("div",{className:"pull-right"},["debug","info","web"].map(function(e){return _react2["default"].createElement(_ToggleButton2["default"],{key:e,text:e,checked:r[e],onToggle:function(){return o(e)}})}),_react2["default"].createElement("i",{onClick:a,className:"fa fa-close"}))),_react2["default"].createElement(_EventList2["default"],{events:n}))}}]),t}(_react.Component);EventLog.propTypes={filters:_react.PropTypes.object.isRequired,events:_react.PropTypes.array.isRequired,onToggleFilter:_react.PropTypes.func.isRequired,onClose:_react.PropTypes.func.isRequired,defaultHeight:_react.PropTypes.number},EventLog.defaultProps={defaultHeight:200},exports["default"]=(0,_reactRedux.connect)(function(e){return{filters:e.eventLog.filter,events:e.eventLog.filteredEvents}},{onClose:_eventLog.toggleEventLogVisibility,onToggleFilter:_eventLog.toggleEventLogFilter})(EventLog);
+
+},{"../ducks/eventLog":42,"./EventLog/EventList":10,"./common/ToggleButton":37,"react":"react","react-redux":"react-redux"}],10:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function LogIcon(e){var t=e.event,r={web:"html5",debug:"bug"}[t.level]||"info";return _react2["default"].createElement("i",{className:"fa fa-fw fa-"+r})}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var o=t[r];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}return function(t,r,o){return r&&e(t.prototype,r),o&&e(t,o),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_shallowequal=require("shallowequal"),_shallowequal2=_interopRequireDefault(_shallowequal),_AutoScroll=require("../helpers/AutoScroll"),_AutoScroll2=_interopRequireDefault(_AutoScroll),_VirtualScroll=require("../helpers/VirtualScroll"),EventLogList=function(e){function t(e){_classCallCheck(this,t);var r=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e));return r.heights={},r.state={vScroll:(0,_VirtualScroll.calcVScroll)()},r.onViewportUpdate=r.onViewportUpdate.bind(r),r}return _inherits(t,e),_createClass(t,[{key:"componentDidMount",value:function(){window.addEventListener("resize",this.onViewportUpdate),this.onViewportUpdate()}},{key:"componentWillUnmount",value:function(){window.removeEventListener("resize",this.onViewportUpdate)}},{key:"componentDidUpdate",value:function(){this.onViewportUpdate()}},{key:"onViewportUpdate",value:function(){var e=this,t=_reactDom2["default"].findDOMNode(this),r=(0,_VirtualScroll.calcVScroll)({itemCount:this.props.events.length,rowHeight:this.props.rowHeight,viewportTop:t.scrollTop,viewportHeight:t.offsetHeight,itemHeights:this.props.events.map(function(t){return e.heights[t.id]})});(0,_shallowequal2["default"])(this.state.vScroll,r)||this.setState({vScroll:r})}},{key:"setHeight",value:function(e,t){if(t&&!this.heights[e]){var r=t.offsetHeight;this.heights[e]!==r&&(this.heights[e]=r,this.onViewportUpdate())}}},{key:"render",value:function(){var e=this,t=this.state.vScroll,r=this.props.events;return _react2["default"].createElement("pre",{onScroll:this.onViewportUpdate},_react2["default"].createElement("div",{style:{height:t.paddingTop}}),r.slice(t.start,t.end).map(function(t){return _react2["default"].createElement("div",{key:t.id,ref:function(r){return e.setHeight(t.id,r)}},_react2["default"].createElement(LogIcon,{event:t}),t.message)}),_react2["default"].createElement("div",{style:{height:t.paddingBottom}}))}}]),t}(_react.Component);EventLogList.propTypes={events:_react.PropTypes.array.isRequired,rowHeight:_react.PropTypes.number},EventLogList.defaultProps={rowHeight:18},exports["default"]=(0,_AutoScroll2["default"])(EventLogList);
-},{"../helpers/AutoScroll":28,"../helpers/VirtualScroll":29,"react":"react","react-dom":"react-dom","shallowequal":"shallowequal"}],6:[function(require,module,exports){
+},{"../helpers/AutoScroll":39,"../helpers/VirtualScroll":40,"react":"react","react-dom":"react-dom","shallowequal":"shallowequal"}],11:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var o=0;o<t.length;o++){var r=t[o];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,o,r){return o&&e(t.prototype,o),r&&e(t,r),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_shallowequal=require("shallowequal"),_shallowequal2=_interopRequireDefault(_shallowequal),_AutoScroll=require("./helpers/AutoScroll"),_AutoScroll2=_interopRequireDefault(_AutoScroll),_VirtualScroll=require("./helpers/VirtualScroll"),_FlowTableHead=require("./FlowTable/FlowTableHead"),_FlowTableHead2=_interopRequireDefault(_FlowTableHead),_FlowRow=require("./FlowTable/FlowRow"),_FlowRow2=_interopRequireDefault(_FlowRow),_filt=require("../filt/filt"),_filt2=_interopRequireDefault(_filt),FlowTable=function(e){function t(e,o){_classCallCheck(this,t);var r=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,o));return r.state={vScroll:(0,_VirtualScroll.calcVScroll)()},r.onViewportUpdate=r.onViewportUpdate.bind(r),r}return _inherits(t,e),_createClass(t,[{key:"componentWillMount",value:function(){window.addEventListener("resize",this.onViewportUpdate)}},{key:"componentWillUnmount",value:function(){window.removeEventListener("resize",this.onViewportUpdate)}},{key:"componentDidUpdate",value:function(){if(this.onViewportUpdate(),this.shouldScrollIntoView){this.shouldScrollIntoView=!1;var e=this.props,t=e.rowHeight,o=e.flows,r=e.selected,l=_reactDom2["default"].findDOMNode(this),a=_reactDom2["default"].findDOMNode(this.refs.head),i=a?a.offsetHeight:0,n=o.indexOf(r)*t+i,u=n+t,c=l.scrollTop,s=l.offsetHeight;c>n-i?l.scrollTop=n-i:u>c+s&&(l.scrollTop=u-s)}}},{key:"componentWillReceiveProps",value:function(e){e.selected&&e.selected!==this.props.selected&&(this.shouldScrollIntoView=!0)}},{key:"onViewportUpdate",value:function(){var e=_reactDom2["default"].findDOMNode(this),t=e.scrollTop,o=(0,_VirtualScroll.calcVScroll)({viewportTop:t,viewportHeight:e.offsetHeight,itemCount:this.props.flows.length,rowHeight:this.props.rowHeight});this.state.viewportTop===t&&(0,_shallowequal2["default"])(this.state.vScroll,o)||this.setState({vScroll:o,viewportTop:t})}},{key:"render",value:function(){var e=this,t=this.state,o=t.vScroll,r=t.viewportTop,l=this.props,a=l.flows,i=l.selected,n=l.highlight,u=n?_filt2["default"].parse(n):function(){return!1};return _react2["default"].createElement("div",{className:"flow-table",onScroll:this.onViewportUpdate},_react2["default"].createElement("table",null,_react2["default"].createElement("thead",{ref:"head",style:{transform:"translateY("+r+"px)"}},_react2["default"].createElement(_FlowTableHead2["default"],null)),_react2["default"].createElement("tbody",null,_react2["default"].createElement("tr",{style:{height:o.paddingTop}}),a.slice(o.start,o.end).map(function(t){return _react2["default"].createElement(_FlowRow2["default"],{key:t.id,flow:t,selected:t===i,highlighted:u(t),onSelect:e.props.onSelect})}),_react2["default"].createElement("tr",{style:{height:o.paddingBottom}}))))}}]),t}(_react2["default"].Component);FlowTable.propTypes={onSelect:_react.PropTypes.func.isRequired,flows:_react.PropTypes.array.isRequired,rowHeight:_react.PropTypes.number,highlight:_react.PropTypes.string,selected:_react.PropTypes.object},FlowTable.defaultProps={rowHeight:32},exports["default"]=(0,_AutoScroll2["default"])(FlowTable);
-},{"../filt/filt":39,"./FlowTable/FlowRow":8,"./FlowTable/FlowTableHead":9,"./helpers/AutoScroll":28,"./helpers/VirtualScroll":29,"react":"react","react-dom":"react-dom","shallowequal":"shallowequal"}],7:[function(require,module,exports){
+},{"../filt/filt":49,"./FlowTable/FlowRow":13,"./FlowTable/FlowTableHead":14,"./helpers/AutoScroll":39,"./helpers/VirtualScroll":40,"react":"react","react-dom":"react-dom","shallowequal":"shallowequal"}],12:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function TLSColumn(e){var t=e.flow;return _react2["default"].createElement("td",{className:(0,_classnames2["default"])("col-tls","https"===t.request.scheme?"col-tls-https":"col-tls-http")})}function IconColumn(e){var t=e.flow;return _react2["default"].createElement("td",{className:"col-icon"},_react2["default"].createElement("div",{className:(0,_classnames2["default"])("resource-icon",IconColumn.getIcon(t))}))}function PathColumn(e){var t=e.flow;return _react2["default"].createElement("td",{className:"col-path"},t.request.is_replay&&_react2["default"].createElement("i",{className:"fa fa-fw fa-repeat pull-right"}),t.intercepted&&_react2["default"].createElement("i",{className:"fa fa-fw fa-pause pull-right"}),_utils.RequestUtils.pretty_url(t.request))}function MethodColumn(e){var t=e.flow;return _react2["default"].createElement("td",{className:"col-method"},t.request.method)}function StatusColumn(e){var t=e.flow;return _react2["default"].createElement("td",{className:"col-status"},t.response&&t.response.status_code)}function SizeColumn(e){var t=e.flow;return _react2["default"].createElement("td",{className:"col-size"},(0,_utils2.formatSize)(SizeColumn.getTotalSize(t)))}function TimeColumn(e){var t=e.flow;return _react2["default"].createElement("td",{className:"col-time"},t.response?(0,_utils2.formatTimeDelta)(1e3*(t.response.timestamp_end-t.request.timestamp_start)):"...")}Object.defineProperty(exports,"__esModule",{value:!0}),exports.TLSColumn=TLSColumn,exports.IconColumn=IconColumn,exports.PathColumn=PathColumn,exports.MethodColumn=MethodColumn,exports.StatusColumn=StatusColumn,exports.SizeColumn=SizeColumn,exports.TimeColumn=TimeColumn;var _react=require("react"),_react2=_interopRequireDefault(_react),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_utils=require("../../flow/utils.js"),_utils2=require("../../utils.js");TLSColumn.sortKeyFun=function(e){return e.request.scheme},TLSColumn.headerClass="col-tls",TLSColumn.headerName="",IconColumn.headerClass="col-icon",IconColumn.headerName="",IconColumn.getIcon=function(e){if(!e.response)return"resource-icon-plain";var t=_utils.ResponseUtils.getContentType(e.response)||"";return 304===e.response.status_code?"resource-icon-not-modified":300<=e.response.status_code&&e.response.status_code<400?"resource-icon-redirect":t.indexOf("image")>=0?"resource-icon-image":t.indexOf("javascript")>=0?"resource-icon-js":t.indexOf("css")>=0?"resource-icon-css":t.indexOf("html")>=0?"resource-icon-document":"resource-icon-plain"},PathColumn.sortKeyFun=function(e){return _utils.RequestUtils.pretty_url(e.request)},PathColumn.headerClass="col-path",PathColumn.headerName="Path",MethodColumn.sortKeyFun=function(e){return e.request.method},MethodColumn.headerClass="col-method",MethodColumn.headerName="Method",StatusColumn.sortKeyFun=function(e){return e.response&&e.response.status_code},StatusColumn.headerClass="col-status",StatusColumn.headerName="Status",SizeColumn.sortKeyFun=function(e){var t=e.request.contentLength;return e.response&&(t+=e.response.contentLength||0),t},SizeColumn.getTotalSize=SizeColumn.sortKeyFun,SizeColumn.headerClass="col-size",SizeColumn.headerName="Size",TimeColumn.sortKeyFun=function(e){return e.response&&e.response.timestamp_end-e.request.timestamp_start},TimeColumn.headerClass="col-time",TimeColumn.headerName="Time",exports["default"]=[TLSColumn,IconColumn,PathColumn,MethodColumn,StatusColumn,SizeColumn,TimeColumn];
-},{"../../flow/utils.js":40,"../../utils.js":42,"classnames":"classnames","react":"react"}],8:[function(require,module,exports){
+},{"../../flow/utils.js":50,"../../utils.js":52,"classnames":"classnames","react":"react"}],13:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function FlowRow(e){var t=e.flow,r=e.selected,l=e.highlighted,o=e.onSelect,s=(0,_classnames2["default"])({selected:r,highlighted:l,intercepted:t.intercepted,"has-request":t.request,"has-response":t.response});return _react2["default"].createElement("tr",{className:s,onClick:function(){return o(t)}},_FlowColumns2["default"].map(function(e){return _react2["default"].createElement(e,{key:e.name,flow:t})}))}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=FlowRow;var _react=require("react"),_react2=_interopRequireDefault(_react),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_FlowColumns=require("./FlowColumns"),_FlowColumns2=_interopRequireDefault(_FlowColumns);FlowRow.propTypes={onSelect:_react.PropTypes.func.isRequired,flow:_react.PropTypes.object.isRequired,highlighted:_react.PropTypes.bool,selected:_react.PropTypes.bool};
-},{"./FlowColumns":7,"classnames":"classnames","react":"react"}],9:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function FlowTableHead(e){function r(e){s({sortColumn:e.name,sortDesc:e.name!==t?!1:!o})}var t=e.sortColumn,o=e.sortDesc,s=e.onSort,a=o?"sort-desc":"sort-asc";return _react2["default"].createElement("tr",null,_FlowColumns2["default"].map(function(e){return _react2["default"].createElement("th",{className:(0,_classnames2["default"])(e.headerClass,t===e.name&&a),key:e.name,onClick:function(){return r(e)}},e.headerName)}))}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_FlowColumns=require("./FlowColumns"),_FlowColumns2=_interopRequireDefault(_FlowColumns),_flows=require("../../ducks/flows");FlowTableHead.propTypes={onSort:_react.PropTypes.func.isRequired,sortDesc:_react2["default"].PropTypes.bool.isRequired,sortColumn:_react2["default"].PropTypes.string},exports["default"]=(0,_reactRedux.connect)(function(e){return{sortDesc:e.flows.sort.sortDesc,sortColumn:e.flows.sort.sortColumn}},{onSort:_flows.setSort})(FlowTableHead);
+},{"./FlowColumns":12,"classnames":"classnames","react":"react"}],14:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function FlowTableHead(e){var r=e.sortColumn,t=e.sortDesc,o=e.onSort,s=t?"sort-desc":"sort-asc";return _react2["default"].createElement("tr",null,_FlowColumns2["default"].map(function(e){return _react2["default"].createElement("th",{className:(0,_classnames2["default"])(e.headerClass,r===e.name&&s),key:e.name,onClick:function(){return o({sortColumn:e.name,sortDesc:e.name!==r?!1:!t})}},e.headerName)}))}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_FlowColumns=require("./FlowColumns"),_FlowColumns2=_interopRequireDefault(_FlowColumns),_flows=require("../../ducks/flows");FlowTableHead.propTypes={onSort:_react.PropTypes.func.isRequired,sortDesc:_react2["default"].PropTypes.bool.isRequired,sortColumn:_react2["default"].PropTypes.string},exports["default"]=(0,_reactRedux.connect)(function(e){return{sortDesc:e.flows.sort.sortDesc,sortColumn:e.flows.sort.sortColumn}},{onSort:_flows.setSort})(FlowTableHead);
+
+},{"../../ducks/flows":43,"./FlowColumns":12,"classnames":"classnames","react":"react","react-redux":"react-redux"}],15:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var o=t[r];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}return function(t,r,o){return r&&e(t.prototype,r),o&&e(t,o),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_Nav=require("./FlowView/Nav"),_Nav2=_interopRequireDefault(_Nav),_Messages=require("./FlowView/Messages"),_Details=require("./FlowView/Details"),_Details2=_interopRequireDefault(_Details),_Prompt=require("./Prompt"),_Prompt2=_interopRequireDefault(_Prompt),FlowView=function(e){function t(e,r){_classCallCheck(this,t);var o=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,r));return o.state={prompt:!1},o.closePrompt=o.closePrompt.bind(o),o.selectTab=o.selectTab.bind(o),o}return _inherits(t,e),_createClass(t,[{key:"getTabs",value:function(){var e=this;return["request","response","error"].filter(function(t){return e.props.flow[t]}).concat(["details"])}},{key:"nextTab",value:function(e){var t=this.getTabs();this.selectTab(t[(t.indexOf(this.props.tab)+e+t.length)%t.length])}},{key:"selectTab",value:function(e){this.props.updateLocation("/flows/"+this.props.flow.id+"/"+e)}},{key:"closePrompt",value:function(e){this.setState({prompt:!1}),e&&this.refs.tab.edit(e)}},{key:"promptEdit",value:function(){var e=void 0;switch(this.props.tab){case"request":e=["method","url",{text:"http version",key:"v"},"header"];break;case"response":e=[{text:"http version",key:"v"},"code","message","header"];break;case"details":return;default:throw"Unknown tab for edit: "+this.props.tab}this.setState({prompt:{options:e,done:this.closePrompt}})}},{key:"render",value:function(){var e=this.getTabs(),r=this.props,o=r.flow,a=r.tab;e.indexOf(a)<0&&(a="response"===a&&o.error?"error":"error"===a&&o.response?"response":e[0]);var s=t.allTabs[_lodash2["default"].capitalize(a)];return _react2["default"].createElement("div",{className:"flow-detail",onScroll:this.adjustHead},_react2["default"].createElement(_Nav2["default"],{flow:o,tabs:e,active:a,onSelectTab:this.selectTab}),_react2["default"].createElement(s,{ref:"tab",flow:o}),this.state.prompt&&_react2["default"].createElement(_Prompt2["default"],this.state.prompt))}}]),t}(_react.Component);FlowView.allTabs={Request:_Messages.Request,Response:_Messages.Response,Error:_Messages.Error,Details:_Details2["default"]},exports["default"]=FlowView;
-},{"../../ducks/flows":33,"./FlowColumns":7,"classnames":"classnames","react":"react","react-redux":"react-redux"}],10:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function Footer(e){var a=e.settings;return _react2["default"].createElement("footer",null,a.mode&&"regular"!=a.mode&&_react2["default"].createElement("span",{className:"label label-success"},a.mode," mode"),a.intercept&&_react2["default"].createElement("span",{className:"label label-success"},"Intercept: ",a.intercept),a.showhost&&_react2["default"].createElement("span",{className:"label label-success"},"showhost"),a.no_upstream_cert&&_react2["default"].createElement("span",{className:"label label-success"},"no-upstream-cert"),a.rawtcp&&_react2["default"].createElement("span",{className:"label label-success"},"raw-tcp"),!a.http2&&_react2["default"].createElement("span",{className:"label label-success"},"no-http2"),a.anticache&&_react2["default"].createElement("span",{className:"label label-success"},"anticache"),a.anticomp&&_react2["default"].createElement("span",{className:"label label-success"},"anticomp"),a.stickyauth&&_react2["default"].createElement("span",{className:"label label-success"},"stickyauth: ",a.stickyauth),a.stickycookie&&_react2["default"].createElement("span",{className:"label label-success"},"stickycookie: ",a.stickycookie),a.stream&&_react2["default"].createElement("span",{className:"label label-success"},"stream: ",(0,_utils.formatSize)(a.stream)))}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=Footer;var _react=require("react"),_react2=_interopRequireDefault(_react),_utils=require("../utils.js"),_common=require("./common.js");Footer.propTypes={settings:_react2["default"].PropTypes.object.isRequired};
+},{"./FlowView/Details":16,"./FlowView/Messages":18,"./FlowView/Nav":19,"./Prompt":30,"lodash":"lodash","react":"react"}],16:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function TimeStamp(e){var t=e.t,a=e.deltaTo,r=e.title;return t?_react2["default"].createElement("tr",null,_react2["default"].createElement("td",null,r,":"),_react2["default"].createElement("td",null,(0,_utils.formatTimeStamp)(t),a&&_react2["default"].createElement("span",{className:"text-muted"},"(",(0,_utils.formatTimeDelta)(1e3*(t-a)),")"))):_react2["default"].createElement("tr",null)}function ConnectionInfo(e){var t=e.conn;return _react2["default"].createElement("table",{className:"connection-table"},_react2["default"].createElement("tbody",null,_react2["default"].createElement("tr",{key:"address"},_react2["default"].createElement("td",null,"Address:"),_react2["default"].createElement("td",null,t.address.address.join(":"))),t.sni?_react2["default"].createElement("tr",{key:"sni"}):_react2["default"].createElement("tr",{key:"sni"},_react2["default"].createElement("td",null,_react2["default"].createElement("abbr",{title:"TLS Server Name Indication"},"TLS SNI:")),_react2["default"].createElement("td",null,t.sni))))}function CertificateInfo(e){var t=e.flow;return _react2["default"].createElement("div",null,t.client_conn.cert&&[_react2["default"].createElement("h4",{key:"name"},"Client Certificate"),_react2["default"].createElement("pre",{key:"value",style:{maxHeight:100}},t.client_conn.cert)],t.server_conn.cert&&[_react2["default"].createElement("h4",{key:"name"},"Server Certificate"),_react2["default"].createElement("pre",{key:"value",style:{maxHeight:100}},t.server_conn.cert)])}function Timing(e){var t=e.flow,a=t.server_conn,r=t.client_conn,n=t.request,l=t.response,c=[{title:"Server conn. initiated",t:a.timestamp_start,deltaTo:n.timestamp_start},{title:"Server conn. TCP handshake",t:a.timestamp_tcp_setup,deltaTo:n.timestamp_start},{title:"Server conn. SSL handshake",t:a.timestamp_ssl_setup,deltaTo:n.timestamp_start},{title:"Client conn. established",t:r.timestamp_start,deltaTo:n.timestamp_start},{title:"Client conn. SSL handshake",t:r.timestamp_ssl_setup,deltaTo:n.timestamp_start},{title:"First request byte",t:n.timestamp_start},{title:"Request complete",t:n.timestamp_end,deltaTo:n.timestamp_start},l&&{title:"First response byte",t:l.timestamp_start,deltaTo:n.timestamp_start},l&&{title:"Response complete",t:l.timestamp_end,deltaTo:n.timestamp_start}];return _react2["default"].createElement("div",null,_react2["default"].createElement("h4",null,"Timing"),_react2["default"].createElement("table",{className:"timing-table"},_react2["default"].createElement("tbody",null,c.filter(function(e){return e}).sort(function(e,t){return e.t-t.t}).map(function(e){return _react2["default"].createElement(TimeStamp,_extends({key:e.title},e))}))))}function Details(e){var t=e.flow;return _react2["default"].createElement("section",null,_react2["default"].createElement("h4",null,"Client Connection"),_react2["default"].createElement(ConnectionInfo,{conn:t.client_conn}),_react2["default"].createElement("h4",null,"Server Connection"),_react2["default"].createElement(ConnectionInfo,{conn:t.server_conn}),_react2["default"].createElement(CertificateInfo,{flow:t}),_react2["default"].createElement(Timing,{flow:t}))}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var r in a)Object.prototype.hasOwnProperty.call(a,r)&&(e[r]=a[r])}return e};exports.TimeStamp=TimeStamp,exports.ConnectionInfo=ConnectionInfo,exports.CertificateInfo=CertificateInfo,exports.Timing=Timing,exports["default"]=Details;var _react=require("react"),_react2=_interopRequireDefault(_react),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_utils=require("../../utils.js");
-},{"../utils.js":42,"./common.js":21,"react":"react"}],11:[function(require,module,exports){
+},{"../../utils.js":52,"lodash":"lodash","react":"react"}],17:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e},_createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,r,n){return r&&e(t.prototype,r),n&&e(t,n),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_ValueEditor=require("../ValueEditor"),_ValueEditor2=_interopRequireDefault(_ValueEditor),_utils=require("../../utils.js"),HeaderEditor=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"render",value:function(){return _react2["default"].createElement(_ValueEditor2["default"],_extends({ref:"input"},this.props,{onKeyDown:this.onKeyDown,inline:!0}))}},{key:"focus",value:function(){_reactDom2["default"].findDOMNode(this).focus()}},{key:"onKeyDown",value:function(e){switch(e.keyCode){case _utils.Key.BACKSPACE:var t=window.getSelection().getRangeAt(0);0===t.startOffset&&0===t.endOffset&&this.props.onRemove(e);break;case _utils.Key.TAB:e.shiftKey||this.props.onTab(e)}}}]),t}(_react.Component),Headers=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"onChange",value:function(e,t,r){var n=_.cloneDeep(this.props.message.headers);n[e][t]=r,n[e][0]||n[e][1]||(1===n.length?(n[0][0]="Name",n[0][1]="Value"):(n.splice(e,1),e===n.length&&(this._nextSel=e-1+"-value"))),this.props.onChange(n)}},{key:"edit",value:function(){this.refs["0-key"].focus()}},{key:"onTab",value:function(e,t,r){var n=this.props.message.headers;if(e===n.length-1&&1===t){r.preventDefault();var o=_.cloneDeep(this.props.message.headers);o.push(["Name","Value"]),this.props.onChange(o),this._nextSel=e+1+"-key"}}},{key:"componentDidUpdate",value:function(){this._nextSel&&this.refs[this._nextSel]&&(this.refs[this._nextSel].focus(),this._nextSel=void 0)}},{key:"onRemove",value:function(e,t,r){1===t?(r.preventDefault(),this.refs[e+"-key"].focus()):e>0&&(r.preventDefault(),this.refs[e-1+"-value"].focus())}},{key:"render",value:function(){var e=this,t=this.props.message;return _react2["default"].createElement("table",{className:"header-table"},_react2["default"].createElement("tbody",null,t.headers.map(function(t,r){return _react2["default"].createElement("tr",{key:r},_react2["default"].createElement("td",{className:"header-name"},_react2["default"].createElement(HeaderEditor,{ref:r+"-key",content:t[0],onDone:function(t){return e.onChange(r,0,t)},onRemove:function(t){return e.onRemove(r,0,t)},onTab:function(t){return e.onTab(r,0,t)}}),":"),_react2["default"].createElement("td",{className:"header-value"},_react2["default"].createElement(HeaderEditor,{ref:r+"-value",content:t[1],onDone:function(t){return e.onChange(r,1,t)},onRemove:function(t){return e.onRemove(r,1,t)},onTab:function(t){return e.onTab(r,1,t)}})))})))}}]),t}(_react.Component);Headers.propTypes={onChange:_react.PropTypes.func.isRequired,message:_react.PropTypes.object.isRequired},exports["default"]=Headers;
+
+},{"../../utils.js":52,"../ValueEditor":32,"react":"react","react-dom":"react-dom"}],18:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function Error(e){var t=e.flow;return _react2["default"].createElement("section",null,_react2["default"].createElement("div",{className:"alert alert-warning"},t.error.msg,_react2["default"].createElement("div",null,_react2["default"].createElement("small",null,(0,_utils2.formatTimeStamp)(t.error.timestamp)))))}Object.defineProperty(exports,"__esModule",{value:!0}),exports.Response=exports.Request=void 0;var _createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,r,n){return r&&e(t.prototype,r),n&&e(t,n),t}}();exports.Error=Error;var _react=require("react"),_react2=_interopRequireDefault(_react),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_actions=require("../../actions.js"),_utils=require("../../flow/utils.js"),_utils2=require("../../utils.js"),_ContentView=require("../ContentView"),_ContentView2=_interopRequireDefault(_ContentView),_ValueEditor=require("../ValueEditor"),_ValueEditor2=_interopRequireDefault(_ValueEditor),_Headers=require("./Headers"),_Headers2=_interopRequireDefault(_Headers),RequestLine=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"render",value:function(){var e=this.props.flow;return _react2["default"].createElement("div",{className:"first-line request-line"},_react2["default"].createElement(_ValueEditor2["default"],{ref:"method",content:e.request.method,onDone:function(t){return _actions.FlowActions.update(e,{request:{method:t}})},inline:!0})," ",_react2["default"].createElement(_ValueEditor2["default"],{ref:"url",content:_utils.RequestUtils.pretty_url(e.request),onDone:function(t){return _actions.FlowActions.update(e,{request:Object.assign({path:""},(0,_utils.parseUrl)(t))})},isValid:function(e){return!!(0,_utils.parseUrl)(e).host},inline:!0})," ",_react2["default"].createElement(_ValueEditor2["default"],{ref:"httpVersion",content:e.request.http_version,onDone:function(t){return _actions.FlowActions.update(e,{request:{http_version:(0,_utils.parseHttpVersion)(t)}})},isValid:_utils.isValidHttpVersion,inline:!0}))}}]),t}(_react.Component),ResponseLine=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"render",value:function(){var e=this.props.flow;return _react2["default"].createElement("div",{className:"first-line response-line"},_react2["default"].createElement(_ValueEditor2["default"],{ref:"httpVersion",content:e.response.http_version,onDone:function(t){return _actions.FlowActions.update(e,{response:{http_version:(0,_utils.parseHttpVersion)(t)}})},isValid:_utils.isValidHttpVersion,inline:!0})," ",_react2["default"].createElement(_ValueEditor2["default"],{ref:"code",content:e.response.status_code+"",onDone:function(t){return _actions.FlowActions.update(e,{response:{code:parseInt(t)}})},isValid:function(e){return/^\d+$/.test(e)},inline:!0})," ",_react2["default"].createElement(_ValueEditor2["default"],{ref:"msg",content:e.response.reason,onDone:function(t){return _actions.FlowActions.update(e,{response:{msg:t}})},inline:!0}))}}]),t}(_react.Component),Request=exports.Request=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"render",value:function(){var e=this.props.flow;return _react2["default"].createElement("section",{className:"request"},_react2["default"].createElement(RequestLine,{ref:"requestLine",flow:e}),_react2["default"].createElement(_Headers2["default"],{ref:"headers",message:e.request,onChange:function(t){return _actions.FlowActions.update(e,{request:{headers:t}})}}),_react2["default"].createElement("hr",null),_react2["default"].createElement(_ContentView2["default"],{flow:e,message:e.request}))}},{key:"edit",value:function(e){switch(e){case"m":this.refs.requestLine.refs.method.focus();break;case"u":this.refs.requestLine.refs.url.focus();break;case"v":this.refs.requestLine.refs.httpVersion.focus();break;case"h":this.refs.headers.edit();break;default:throw new Error("Unimplemented: "+e)}}}]),t}(_react.Component),Response=exports.Response=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"render",value:function(){var e=this.props.flow;return _react2["default"].createElement("section",{className:"response"},_react2["default"].createElement(ResponseLine,{ref:"responseLine",flow:e}),_react2["default"].createElement(_Headers2["default"],{ref:"headers",message:e.response,onChange:function(t){return _actions.FlowActions.update(e,{response:{headers:t}})}}),_react2["default"].createElement("hr",null),_react2["default"].createElement(_ContentView2["default"],{flow:e,message:e.response}))}},{key:"edit",value:function(e){switch(e){case"c":this.refs.responseLine.refs.status_code.focus();break;case"m":this.refs.responseLine.refs.msg.focus();break;case"v":this.refs.responseLine.refs.httpVersion.focus();break;case"h":this.refs.headers.edit();break;default:throw new Error("'Unimplemented: "+e)}}}]),t}(_react.Component);
+
+},{"../../actions.js":2,"../../flow/utils.js":50,"../../utils.js":52,"../ContentView":4,"../ValueEditor":32,"./Headers":17,"lodash":"lodash","react":"react"}],19:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function NavAction(e){var t=e.icon,a=e.title,c=e.onClick;return _react2["default"].createElement("a",{title:a,href:"#",className:"nav-action",onClick:function(e){e.preventDefault(),c(e)}},_react2["default"].createElement("i",{className:"fa fa-fw "+t}))}function Nav(e){var t=e.flow,a=e.active,c=e.tabs,r=e.onSelectTab;return _react2["default"].createElement("nav",{className:"nav-tabs nav-tabs-sm"},c.map(function(e){return _react2["default"].createElement("a",{key:e,href:"#",className:(0,_classnames2["default"])({active:a===e}),onClick:function(t){t.preventDefault(),r(e)}},_.capitalize(e))}),_react2["default"].createElement(NavAction,{title:"[d]elete flow",icon:"fa-trash",onClick:function(){return _actions.FlowActions["delete"](t)}}),_react2["default"].createElement(NavAction,{title:"[D]uplicate flow",icon:"fa-copy",onClick:function(){return _actions.FlowActions.duplicate(t)}}),_react2["default"].createElement(NavAction,{disabled:!0,title:"[r]eplay flow",icon:"fa-repeat",onClick:function(){return _actions.FlowActions.replay(t)}}),t.intercepted&&_react2["default"].createElement(NavAction,{title:"[a]ccept intercepted flow",icon:"fa-play",onClick:function(){return _actions.FlowActions.accept(t)}}),t.modified&&_react2["default"].createElement(NavAction,{title:"revert changes to flow [V]",icon:"fa-history",onClick:function(){return _actions.FlowActions.revert(t)}}))}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=Nav;var _react=require("react"),_react2=_interopRequireDefault(_react),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_actions=require("../../actions.js");NavAction.propTypes={icon:_react.PropTypes.string.isRequired,title:_react.PropTypes.string.isRequired,onClick:_react.PropTypes.func.isRequired},Nav.propTypes={flow:_react.PropTypes.object.isRequired,active:_react.PropTypes.string.isRequired,tabs:_react.PropTypes.array.isRequired,onSelectTab:_react.PropTypes.func.isRequired};
+
+},{"../../actions.js":2,"classnames":"classnames","react":"react"}],20:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function Footer(e){var a=e.settings;return _react2["default"].createElement("footer",null,a.mode&&"regular"!=a.mode&&_react2["default"].createElement("span",{className:"label label-success"},a.mode," mode"),a.intercept&&_react2["default"].createElement("span",{className:"label label-success"},"Intercept: ",a.intercept),a.showhost&&_react2["default"].createElement("span",{className:"label label-success"},"showhost"),a.no_upstream_cert&&_react2["default"].createElement("span",{className:"label label-success"},"no-upstream-cert"),a.rawtcp&&_react2["default"].createElement("span",{className:"label label-success"},"raw-tcp"),!a.http2&&_react2["default"].createElement("span",{className:"label label-success"},"no-http2"),a.anticache&&_react2["default"].createElement("span",{className:"label label-success"},"anticache"),a.anticomp&&_react2["default"].createElement("span",{className:"label label-success"},"anticomp"),a.stickyauth&&_react2["default"].createElement("span",{className:"label label-success"},"stickyauth: ",a.stickyauth),a.stickycookie&&_react2["default"].createElement("span",{className:"label label-success"},"stickycookie: ",a.stickycookie),a.stream&&_react2["default"].createElement("span",{className:"label label-success"},"stream: ",(0,_utils.formatSize)(a.stream)))}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=Footer;var _react=require("react"),_react2=_interopRequireDefault(_react),_utils=require("../utils.js");Footer.propTypes={settings:_react2["default"].PropTypes.object.isRequired};
+
+},{"../utils.js":52,"react":"react"}],21:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _toConsumableArray(e){if(Array.isArray(e)){for(var t=0,r=Array(e.length);t<e.length;t++)r[t]=e[t];return r}return Array.from(e)}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,r,n){return r&&e(t.prototype,r),n&&e(t,n),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_eventLog=require("../ducks/eventLog"),_MainMenu=require("./Header/MainMenu"),_MainMenu2=_interopRequireDefault(_MainMenu),_ViewMenu=require("./Header/ViewMenu"),_ViewMenu2=_interopRequireDefault(_ViewMenu),_OptionMenu=require("./Header/OptionMenu"),_OptionMenu2=_interopRequireDefault(_OptionMenu),_FileMenu=require("./Header/FileMenu"),_FileMenu2=_interopRequireDefault(_FileMenu),_FlowMenu=require("./Header/FlowMenu"),_FlowMenu2=_interopRequireDefault(_FlowMenu),_ui=require("../ducks/ui.js"),Header=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"handleClick",value:function(e,t){t.preventDefault(),this.props.setActiveMenu(e.title)}},{key:"render",value:function(){var e=this,r=this.props,n=r.settings,u=r.updateLocation,a=r.query,i=r.selectedFlow,o=r.activeMenu,l=[].concat(_toConsumableArray(t.entries));i&&l.push(_FlowMenu2["default"]);var c=_.find(l,function(e){return e.title==o});return _react2["default"].createElement("header",null,_react2["default"].createElement("nav",{className:"nav-tabs nav-tabs-lg"},_react2["default"].createElement(_FileMenu2["default"],null),l.map(function(t){return _react2["default"].createElement("a",{key:t.title,href:"#",className:(0,_classnames2["default"])({active:t===c}),onClick:function(r){return e.handleClick(t,r)}},t.title)})),_react2["default"].createElement("div",{className:"menu"},_react2["default"].createElement(c,{settings:n,updateLocation:u,query:a})))}}]),t}(_react.Component);Header.entries=[_MainMenu2["default"],_ViewMenu2["default"],_OptionMenu2["default"]],Header.propTypes={settings:_react.PropTypes.object.isRequired},exports["default"]=(0,_reactRedux.connect)(function(e){return{selectedFlow:e.flows.selected[0],activeMenu:e.ui.activeMenu}},{setActiveMenu:_ui.setActiveMenu})(Header);
-},{"../ducks/eventLog":32,"../ducks/ui.js":35,"./Header/FileMenu":12,"./Header/FlowMenu":15,"./Header/MainMenu":16,"./Header/OptionMenu":17,"./Header/ViewMenu":18,"classnames":"classnames","react":"react","react-redux":"react-redux"}],12:[function(require,module,exports){
+},{"../ducks/eventLog":42,"../ducks/ui.js":45,"./Header/FileMenu":22,"./Header/FlowMenu":25,"./Header/MainMenu":26,"./Header/OptionMenu":27,"./Header/ViewMenu":28,"classnames":"classnames","react":"react","react-redux":"react-redux"}],22:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var a=t[n];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(e,a.key,a)}}return function(t,n,a){return n&&e(t.prototype,n),a&&e(t,a),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_actions=require("../../actions.js"),FileMenu=function(e){function t(e,n){_classCallCheck(this,t);var a=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,n));return a.state={show:!1},a.close=a.close.bind(a),a.onFileClick=a.onFileClick.bind(a),a.onNewClick=a.onNewClick.bind(a),a.onOpenClick=a.onOpenClick.bind(a),a.onOpenFile=a.onOpenFile.bind(a),a.onSaveClick=a.onSaveClick.bind(a),a}return _inherits(t,e),_createClass(t,[{key:"close",value:function(){this.setState({show:!1}),document.removeEventListener("click",this.close)}},{key:"onFileClick",value:function(e){e.preventDefault(),this.state.show||(document.addEventListener("click",this.close),this.setState({show:!0}))}},{key:"onNewClick",value:function(e){e.preventDefault(),confirm("Delete all flows?")&&_actions.FlowActions.clear()}},{key:"onOpenClick",value:function(e){e.preventDefault(),this.fileInput.click()}},{key:"onOpenFile",value:function(e){e.preventDefault(),e.target.files.length>0&&(_actions.FlowActions.upload(e.target.files[0]),this.fileInput.value="")}},{key:"onSaveClick",value:function(e){e.preventDefault(),_actions.FlowActions.download()}},{key:"render",value:function(){var e=this;return _react2["default"].createElement("div",{className:(0,_classnames2["default"])("dropdown pull-left",{open:this.state.show})},_react2["default"].createElement("a",{href:"#",className:"special",onClick:this.onFileClick},"mitmproxy"),_react2["default"].createElement("ul",{className:"dropdown-menu",role:"menu"},_react2["default"].createElement("li",null,_react2["default"].createElement("a",{href:"#",onClick:this.onNewClick},_react2["default"].createElement("i",{className:"fa fa-fw fa-file"}),"New")),_react2["default"].createElement("li",null,_react2["default"].createElement("a",{href:"#",onClick:this.onOpenClick},_react2["default"].createElement("i",{className:"fa fa-fw fa-folder-open"}),"Open..."),_react2["default"].createElement("input",{ref:function(t){return e.fileInput=t},className:"hidden",type:"file",onChange:this.onOpenFile})),_react2["default"].createElement("li",null,_react2["default"].createElement("a",{href:"#",onClick:this.onSaveClick},_react2["default"].createElement("i",{className:"fa fa-fw fa-floppy-o"}),"Save...")),_react2["default"].createElement("li",{role:"presentation",className:"divider"}),_react2["default"].createElement("li",null,_react2["default"].createElement("a",{href:"http://mitm.it/",target:"_blank"},_react2["default"].createElement("i",{className:"fa fa-fw fa-external-link"}),"Install Certificates..."))))}}]),t}(_react.Component);exports["default"]=FileMenu;
-},{"../../actions.js":2,"classnames":"classnames","react":"react"}],13:[function(require,module,exports){
+},{"../../actions.js":2,"classnames":"classnames","react":"react"}],23:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,r,n){return r&&e(t.prototype,r),n&&e(t,n),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_jquery=require("jquery"),_jquery2=_interopRequireDefault(_jquery),FilterDocs=function(e){function t(e,r){_classCallCheck(this,t);var n=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,r));return n.state={doc:t.doc},n}return _inherits(t,e),_createClass(t,[{key:"componentWillMount",value:function(){var e=this;t.xhr||(t.xhr=_jquery2["default"].getJSON("/filter-help"),t.xhr.fail(function(){t.xhr=null})),this.state.doc||t.xhr.done(function(r){t.doc=r,e.setState({doc:r})})}},{key:"render",value:function(){var e=this.state.doc;return e?_react2["default"].createElement("table",{className:"table table-condensed"},_react2["default"].createElement("tbody",null,e.commands.map(function(e){return _react2["default"].createElement("tr",{key:e[1]},_react2["default"].createElement("td",null,e[0].replace(" "," ")),_react2["default"].createElement("td",null,e[1]))}),_react2["default"].createElement("tr",{key:"docs-link"},_react2["default"].createElement("td",{colSpan:"2"},_react2["default"].createElement("a",{href:"http://docs.mitmproxy.org/en/stable/features/filters.html",target:"_blank"},_react2["default"].createElement("i",{className:"fa fa-external-link"}),"&nbsp mitmproxy docs"))))):_react2["default"].createElement("i",{className:"fa fa-spinner fa-spin"})}}]),t}(_react.Component);FilterDocs.xhr=null,FilterDocs.doc=null,exports["default"]=FilterDocs;
-},{"jquery":"jquery","react":"react"}],14:[function(require,module,exports){
+},{"jquery":"jquery","react":"react"}],24:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}return function(t,n,o){return n&&e(t.prototype,n),o&&e(t,o),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_utils=require("../../utils.js"),_filt=require("../../filt/filt"),_filt2=_interopRequireDefault(_filt),_FilterDocs=require("./FilterDocs"),_FilterDocs2=_interopRequireDefault(_FilterDocs),FilterInput=function(e){function t(e,n){_classCallCheck(this,t);var o=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,n));return o.state={value:o.props.value,focus:!1,mousefocus:!1},o.onChange=o.onChange.bind(o),o.onFocus=o.onFocus.bind(o),o.onBlur=o.onBlur.bind(o),o.onKeyDown=o.onKeyDown.bind(o),o.onMouseEnter=o.onMouseEnter.bind(o),o.onMouseLeave=o.onMouseLeave.bind(o),o}return _inherits(t,e),_createClass(t,[{key:"componentWillReceiveProps",value:function(e){this.setState({value:e.value})}},{key:"isValid",value:function(e){try{var t=null==e?this.state.value:e;return t&&_filt2["default"].parse(t),!0}catch(n){return!1}}},{key:"getDesc",value:function(){if(!this.state.value)return _react2["default"].createElement(_FilterDocs2["default"],null);try{return _filt2["default"].parse(this.state.value).desc}catch(e){return""+e}}},{key:"onChange",value:function(e){var t=e.target.value;this.setState({value:t}),this.isValid(t)&&this.props.onChange(t)}},{key:"onFocus",value:function(){this.setState({focus:!0})}},{key:"onBlur",value:function(){this.setState({focus:!1})}},{key:"onMouseEnter",value:function(){this.setState({mousefocus:!0})}},{key:"onMouseLeave",value:function(){this.setState({mousefocus:!1})}},{key:"onKeyDown",value:function(e){e.keyCode!==_utils.Key.ESC&&e.keyCode!==_utils.Key.ENTER||(this.blur(),this.setState({mousefocus:!1})),e.stopPropagation()}},{key:"blur",value:function(){_reactDom2["default"].findDOMNode(this.refs.input).blur(),this.context.returnFocus()}},{key:"select",value:function(){_reactDom2["default"].findDOMNode(this.refs.input).select()}},{key:"render",value:function(){var e=this.props,t=e.type,n=e.color,o=e.placeholder,r=this.state,a=r.value,u=r.focus,s=r.mousefocus;return _react2["default"].createElement("div",{className:(0,_classnames2["default"])("filter-input input-group",{"has-error":!this.isValid()})},_react2["default"].createElement("span",{className:"input-group-addon"},_react2["default"].createElement("i",{className:"fa fa-fw fa-"+t,style:{color:n}})),_react2["default"].createElement("input",{type:"text",ref:"input",placeholder:o,className:"form-control",value:a,onChange:this.onChange,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown}),(u||s)&&_react2["default"].createElement("div",{className:"popover bottom",onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave},_react2["default"].createElement("div",{className:"arrow"}),_react2["default"].createElement("div",{className:"popover-content"},this.getDesc())))}}]),t}(_react.Component);FilterInput.contextTypes={returnFocus:_react2["default"].PropTypes.func},exports["default"]=FilterInput;
-},{"../../filt/filt":39,"../../utils.js":42,"./FilterDocs":13,"classnames":"classnames","react":"react","react-dom":"react-dom"}],15:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function FlowMenu(e){var t=e.flow;return _react2["default"].createElement("div",null,_react2["default"].createElement("div",{className:"menu-row"},_react2["default"].createElement(_common.Button,{disabled:!0,title:"[r]eplay flow",text:"Replay",icon:"fa-repeat",onClick:_actions.FlowActions.replay.bind(null,t)}),_react2["default"].createElement(_common.Button,{title:"[D]uplicate flow",text:"Duplicate",icon:"fa-copy",onClick:_actions.FlowActions.duplicate.bind(null,t)}),_react2["default"].createElement(_common.Button,{title:"[d]elete flow",text:"Delete",icon:"fa-trash",onClick:_actions.FlowActions["delete"].bind(null,t)}),_react2["default"].createElement(_common.Button,{title:"download",text:"Download",icon:"fa-download",onClick:function(){return window.location=_utils.MessageUtils.getContentURL(t,t.response)}})),_react2["default"].createElement("div",{className:"clearfix"}))}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_common=require("../common.js"),_actions=require("../../actions.js"),_utils=require("../../flow/utils.js"),_reactRedux=require("react-redux");FlowMenu.title="Flow",FlowMenu.propTypes={flow:_react.PropTypes.object.isRequired},exports["default"]=(0,_reactRedux.connect)(function(e){return{flow:e.flows.all.byId[e.flows.selected[0]]}})(FlowMenu);
+},{"../../filt/filt":49,"../../utils.js":52,"./FilterDocs":23,"classnames":"classnames","react":"react","react-dom":"react-dom"}],25:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function FlowMenu(e){var t=e.flow;return _react2["default"].createElement("div",null,_react2["default"].createElement("div",{className:"menu-row"},_react2["default"].createElement(_Button2["default"],{disabled:!0,title:"[r]eplay flow",text:"Replay",icon:"fa-repeat",onClick:_actions.FlowActions.replay.bind(null,t)}),_react2["default"].createElement(_Button2["default"],{title:"[D]uplicate flow",text:"Duplicate",icon:"fa-copy",onClick:_actions.FlowActions.duplicate.bind(null,t)}),_react2["default"].createElement(_Button2["default"],{title:"[d]elete flow",text:"Delete",icon:"fa-trash",onClick:_actions.FlowActions["delete"].bind(null,t)}),_react2["default"].createElement(_Button2["default"],{title:"download",text:"Download",icon:"fa-download",onClick:function(){return window.location=_utils.MessageUtils.getContentURL(t,t.response)}})),_react2["default"].createElement("div",{className:"clearfix"}))}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_Button=require("../common/Button"),_Button2=_interopRequireDefault(_Button),_actions=require("../../actions.js"),_utils=require("../../flow/utils.js"),_reactRedux=require("react-redux");FlowMenu.title="Flow",FlowMenu.propTypes={flow:_react.PropTypes.object.isRequired},exports["default"]=(0,_reactRedux.connect)(function(e){return{flow:e.flows.all.byId[e.flows.selected[0]]}})(FlowMenu);
-},{"../../actions.js":2,"../../flow/utils.js":40,"../common.js":21,"react":"react","react-redux":"react-redux"}],16:[function(require,module,exports){
+},{"../../actions.js":2,"../../flow/utils.js":50,"../common/Button":35,"react":"react","react-redux":"react-redux"}],26:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_actions=require("../../actions.js"),_FilterInput=require("./FilterInput"),_FilterInput2=_interopRequireDefault(_FilterInput),MainMenu=function(e){function t(e,n){_classCallCheck(this,t);var r=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,n));return r.onSearchChange=r.onSearchChange.bind(r),r.onHighlightChange=r.onHighlightChange.bind(r),r.onInterceptChange=r.onInterceptChange.bind(r),r}return _inherits(t,e),_createClass(t,[{key:"onSearchChange",value:function(e){this.props.updateLocation(void 0,_defineProperty({},_actions.Query.SEARCH,e))}},{key:"onHighlightChange",value:function(e){this.props.updateLocation(void 0,_defineProperty({},_actions.Query.HIGHLIGHT,e))}},{key:"onInterceptChange",value:function(e){_actions.SettingsActions.update({intercept:e})}},{key:"render",value:function(){var e=this.props,t=e.query,n=e.settings,r=t[_actions.Query.SEARCH]||"",a=t[_actions.Query.HIGHLIGHT]||"",o=n.intercept||"";return _react2["default"].createElement("div",null,_react2["default"].createElement("div",{className:"menu-row"},_react2["default"].createElement(_FilterInput2["default"],{ref:"search",placeholder:"Search",type:"search",color:"black",value:r,onChange:this.onSearchChange}),_react2["default"].createElement(_FilterInput2["default"],{ref:"highlight",placeholder:"Highlight",type:"tag",color:"hsl(48, 100%, 50%)",value:a,onChange:this.onHighlightChange}),_react2["default"].createElement(_FilterInput2["default"],{ref:"intercept",placeholder:"Intercept",type:"pause",color:"hsl(208, 56%, 53%)",value:o,onChange:this.onInterceptChange})),_react2["default"].createElement("div",{className:"clearfix"}))}}]),t}(_react.Component);MainMenu.title="Start",MainMenu.route="flows",MainMenu.propTypes={settings:_react2["default"].PropTypes.object.isRequired},exports["default"]=MainMenu;
-},{"../../actions.js":2,"./FilterInput":14,"react":"react"}],17:[function(require,module,exports){
-"use strict";function _interopRequireDefault(t){return t&&t.__esModule?t:{"default":t}}function OptionMenu(t){var e=t.settings;return _react2["default"].createElement("div",null,_react2["default"].createElement("div",{className:"menu-row"},_react2["default"].createElement(_common.ToggleButton,{text:"showhost",checked:e.showhost,onToggle:function(){return _actions.SettingsActions.update({showhost:!e.showhost})}}),_react2["default"].createElement(_common.ToggleButton,{text:"no_upstream_cert",checked:e.no_upstream_cert,onToggle:function(){return _actions.SettingsActions.update({no_upstream_cert:!e.no_upstream_cert})}}),_react2["default"].createElement(_common.ToggleButton,{text:"rawtcp",checked:e.rawtcp,onToggle:function(){return _actions.SettingsActions.update({rawtcp:!e.rawtcp})}}),_react2["default"].createElement(_common.ToggleButton,{text:"http2",checked:e.http2,onToggle:function(){return _actions.SettingsActions.update({http2:!e.http2})}}),_react2["default"].createElement(_common.ToggleButton,{text:"anticache",checked:e.anticache,onToggle:function(){return _actions.SettingsActions.update({anticache:!e.anticache})}}),_react2["default"].createElement(_common.ToggleButton,{text:"anticomp",checked:e.anticomp,onToggle:function(){return _actions.SettingsActions.update({anticomp:!e.anticomp})}}),_react2["default"].createElement(_common.ToggleInputButton,{name:"stickyauth",placeholder:"Sticky auth filter",checked:!!e.stickyauth,txt:e.stickyauth||"",onToggleChanged:function(t){return _actions.SettingsActions.update({stickyauth:e.stickyauth?null:t})}}),_react2["default"].createElement(_common.ToggleInputButton,{name:"stickycookie",placeholder:"Sticky cookie filter",checked:!!e.stickycookie,txt:e.stickycookie||"",onToggleChanged:function(t){return _actions.SettingsActions.update({stickycookie:e.stickycookie?null:t})}}),_react2["default"].createElement(_common.ToggleInputButton,{name:"stream",placeholder:"stream...",checked:!!e.stream,txt:e.stream||"",inputType:"number",onToggleChanged:function(t){return _actions.SettingsActions.update({stream:e.stream?null:t})}})),_react2["default"].createElement("div",{className:"clearfix"}))}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=OptionMenu;var _react=require("react"),_react2=_interopRequireDefault(_react),_common=require("../common.js"),_actions=require("../../actions.js");OptionMenu.title="Options",OptionMenu.propTypes={settings:_react.PropTypes.object.isRequired};
+},{"../../actions.js":2,"./FilterInput":24,"react":"react"}],27:[function(require,module,exports){
+"use strict";function _interopRequireDefault(t){return t&&t.__esModule?t:{"default":t}}function OptionMenu(t){var e=t.settings;return _react2["default"].createElement("div",null,_react2["default"].createElement("div",{className:"menu-row"},_react2["default"].createElement(_ToggleButton2["default"],{text:"showhost",checked:e.showhost,onToggle:function(){return _actions.SettingsActions.update({showhost:!e.showhost})}}),_react2["default"].createElement(_ToggleButton2["default"],{text:"no_upstream_cert",checked:e.no_upstream_cert,onToggle:function(){return _actions.SettingsActions.update({no_upstream_cert:!e.no_upstream_cert})}}),_react2["default"].createElement(_ToggleButton2["default"],{text:"rawtcp",checked:e.rawtcp,onToggle:function(){return _actions.SettingsActions.update({rawtcp:!e.rawtcp})}}),_react2["default"].createElement(_ToggleButton2["default"],{text:"http2",checked:e.http2,onToggle:function(){return _actions.SettingsActions.update({http2:!e.http2})}}),_react2["default"].createElement(_ToggleButton2["default"],{text:"anticache",checked:e.anticache,onToggle:function(){return _actions.SettingsActions.update({anticache:!e.anticache})}}),_react2["default"].createElement(_ToggleButton2["default"],{text:"anticomp",checked:e.anticomp,onToggle:function(){return _actions.SettingsActions.update({anticomp:!e.anticomp})}}),_react2["default"].createElement(_ToggleInputButton2["default"],{name:"stickyauth",placeholder:"Sticky auth filter",checked:!!e.stickyauth,txt:e.stickyauth||"",onToggleChanged:function(t){return _actions.SettingsActions.update({stickyauth:e.stickyauth?null:t})}}),_react2["default"].createElement(_ToggleInputButton2["default"],{name:"stickycookie",placeholder:"Sticky cookie filter",checked:!!e.stickycookie,txt:e.stickycookie||"",onToggleChanged:function(t){return _actions.SettingsActions.update({stickycookie:e.stickycookie?null:t})}}),_react2["default"].createElement(_ToggleInputButton2["default"],{name:"stream",placeholder:"stream...",checked:!!e.stream,txt:e.stream||"",inputType:"number",onToggleChanged:function(t){return _actions.SettingsActions.update({stream:e.stream?null:t})}})),_react2["default"].createElement("div",{className:"clearfix"}))}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=OptionMenu;var _react=require("react"),_react2=_interopRequireDefault(_react),_ToggleButton=require("../common/ToggleButton"),_ToggleButton2=_interopRequireDefault(_ToggleButton),_ToggleInputButton=require("../common/ToggleInputButton"),_ToggleInputButton2=_interopRequireDefault(_ToggleInputButton),_actions=require("../../actions.js");OptionMenu.title="Options",OptionMenu.propTypes={settings:_react.PropTypes.object.isRequired};
+
+},{"../../actions.js":2,"../common/ToggleButton":37,"../common/ToggleInputButton":38,"react":"react"}],28:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function ViewMenu(e){var t=e.visible,r=e.onToggle;return _react2["default"].createElement("div",null,_react2["default"].createElement("div",{className:"menu-row"},_react2["default"].createElement(_ToggleButton2["default"],{text:"Show Event Log",checked:t,onToggle:r})),_react2["default"].createElement("div",{className:"clearfix"}))}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_ToggleButton=require("../common/ToggleButton"),_ToggleButton2=_interopRequireDefault(_ToggleButton),_eventLog=require("../../ducks/eventLog");ViewMenu.title="View",ViewMenu.route="flows",ViewMenu.propTypes={visible:_react.PropTypes.bool.isRequired,onToggle:_react.PropTypes.func.isRequired},exports["default"]=(0,_reactRedux.connect)(function(e){return{visible:e.eventLog.visible}},{onToggle:_eventLog.toggleEventLogVisibility})(ViewMenu);
-},{"../../actions.js":2,"../common.js":21,"react":"react"}],18:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function ViewMenu(e){var t=e.visible,r=e.onToggle;return _react2["default"].createElement("div",null,_react2["default"].createElement("div",{className:"menu-row"},_react2["default"].createElement(_common.ToggleButton,{text:"Show Event Log",checked:t,onToggle:r})),_react2["default"].createElement("div",{className:"clearfix"}))}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_common=require("../common.js"),_eventLog=require("../../ducks/eventLog");ViewMenu.title="View",ViewMenu.route="flows",ViewMenu.propTypes={visible:_react.PropTypes.bool.isRequired,onToggle:_react.PropTypes.func.isRequired},exports["default"]=(0,_reactRedux.connect)(function(e){return{visible:e.eventLog.visible}},{onToggle:_eventLog.toggleEventLogVisibility})(ViewMenu);
+},{"../../ducks/eventLog":42,"../common/ToggleButton":37,"react":"react","react-redux":"react-redux"}],29:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var l=0;l<t.length;l++){var s=t[l];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}return function(t,l,s){return l&&e(t.prototype,l),s&&e(t,s),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_actions=require("../actions.js"),_utils=require("../utils.js"),_Splitter=require("./common/Splitter"),_Splitter2=_interopRequireDefault(_Splitter),_FlowTable=require("./FlowTable"),_FlowTable2=_interopRequireDefault(_FlowTable),_FlowView=require("./FlowView"),_FlowView2=_interopRequireDefault(_FlowView),_flows=require("../ducks/flows"),MainView=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"componentWillReceiveProps",value:function(e){e.routeParams.flowId!==(e.selectedFlow||{}).id&&this.props.selectFlow(e.routeParams.flowId),e.location.query[_actions.Query.SEARCH]!==e.filter&&this.props.setFilter(e.location.query[_actions.Query.SEARCH],!1),e.location.query[_actions.Query.HIGHLIGHT]!==e.highlight&&this.props.setHighlight(e.location.query[_actions.Query.HIGHLIGHT],!1)}},{key:"selectFlow",value:function(e){e?this.props.updateLocation("/flows/"+e.id+"/"+(this.props.routeParams.detailTab||"request")):this.props.updateLocation("/flows")}},{key:"selectFlowRelative",value:function(e){var t=this.props,l=t.flows,s=t.routeParams,i=t.selectedFlow,o=0;s.flowId?o=Math.min(Math.max(0,l.indexOf(i)+e),l.length-1):0>e&&(o=l.length-1),this.selectFlow(l[o])}},{key:"onMainKeyDown",value:function(e){var t=this.props.selectedFlow;if(!e.ctrlKey){switch(e.keyCode){case _utils.Key.K:case _utils.Key.UP:this.selectFlowRelative(-1);break;case _utils.Key.J:case _utils.Key.DOWN:this.selectFlowRelative(1);break;case _utils.Key.SPACE:case _utils.Key.PAGE_DOWN:this.selectFlowRelative(10);break;case _utils.Key.PAGE_UP:this.selectFlowRelative(-10);break;case _utils.Key.END:this.selectFlowRelative(1e10);break;case _utils.Key.HOME:this.selectFlowRelative(-1e10);break;case _utils.Key.ESC:this.selectFlow(null);break;case _utils.Key.H:case _utils.Key.LEFT:this.refs.flowDetails&&this.refs.flowDetails.nextTab(-1);break;case _utils.Key.L:case _utils.Key.TAB:case _utils.Key.RIGHT:this.refs.flowDetails&&this.refs.flowDetails.nextTab(1);break;case _utils.Key.C:e.shiftKey&&_actions.FlowActions.clear();break;case _utils.Key.D:t&&(e.shiftKey?_actions.FlowActions.duplicate(t):_actions.FlowActions["delete"](t));break;case _utils.Key.A:e.shiftKey?_actions.FlowActions.accept_all():t&&t.intercepted&&_actions.FlowActions.accept(t);break;case _utils.Key.R:!e.shiftKey&&t&&_actions.FlowActions.replay(t);break;case _utils.Key.V:e.shiftKey&&t&&t.modified&&_actions.FlowActions.revert(t);break;case _utils.Key.E:this.refs.flowDetails&&this.refs.flowDetails.promptEdit();break;case _utils.Key.SHIFT:break;default:return void console.debug("keydown",e.keyCode)}e.preventDefault()}}},{key:"render",value:function(){var e=this,t=this.props,l=t.flows,s=t.selectedFlow,i=t.highlight;t.sort;return _react2["default"].createElement("div",{className:"main-view"},_react2["default"].createElement(_FlowTable2["default"],{ref:"flowTable",flows:l,selected:s,highlight:i,onSelect:function(t){return e.selectFlow(t)}}),s&&[_react2["default"].createElement(_Splitter2["default"],{key:"splitter"}),_react2["default"].createElement(_FlowView2["default"],{key:"flowDetails",ref:"flowDetails",tab:this.props.routeParams.detailTab,query:this.props.query,updateLocation:this.props.updateLocation,flow:s})])}}]),t}(_react.Component);MainView.propTypes={highlight:_react.PropTypes.string,sort:_react.PropTypes.object},exports["default"]=(0,_reactRedux.connect)(function(e){return{flows:e.flows.view,filter:e.flows.filter,sort:e.flows.sort,highlight:e.flows.highlight,selectedFlow:e.flows.all.byId[e.flows.selected[0]]}},{selectFlow:_flows.selectFlow,setFilter:_flows.setFilter,setHighlight:_flows.setHighlight},void 0,{withRef:!0})(MainView);
-},{"../../ducks/eventLog":32,"../common.js":21,"react":"react","react-redux":"react-redux"}],19:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var s=0;s<t.length;s++){var l=t[s];l.enumerable=l.enumerable||!1,l.configurable=!0,"value"in l&&(l.writable=!0),Object.defineProperty(e,l.key,l)}}return function(t,s,l){return s&&e(t.prototype,s),l&&e(t,l),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactRedux=require("react-redux"),_actions=require("../actions.js"),_utils=require("../utils.js"),_common=require("./common.js"),_FlowTable=require("./FlowTable"),_FlowTable2=_interopRequireDefault(_FlowTable),_index=require("./flowview/index.js"),_index2=_interopRequireDefault(_index),_flows=require("../ducks/flows"),MainView=function(e){function t(){return _classCallCheck(this,t),_possibleConstructorReturn(this,Object.getPrototypeOf(t).apply(this,arguments))}return _inherits(t,e),_createClass(t,[{key:"componentWillReceiveProps",value:function(e){e.routeParams.flowId!==(e.selectedFlow||{}).id&&this.props.selectFlow(e.routeParams.flowId),e.location.query[_actions.Query.SEARCH]!==e.filter&&this.props.setFilter(e.location.query[_actions.Query.SEARCH],!1),e.location.query[_actions.Query.HIGHLIGHT]!==e.highlight&&this.props.setHighlight(e.location.query[_actions.Query.HIGHLIGHT],!1)}},{key:"selectFlow",value:function(e){e?this.props.updateLocation("/flows/"+e.id+"/"+(this.props.routeParams.detailTab||"request")):this.props.updateLocation("/flows")}},{key:"selectFlowRelative",value:function(e){var t=this.props,s=t.flows,l=t.routeParams,i=t.selectedFlow,o=0;l.flowId?o=Math.min(Math.max(0,s.indexOf(i)+e),s.length-1):0>e&&(o=s.length-1),this.selectFlow(s[o])}},{key:"onMainKeyDown",value:function(e){var t=this.props.selectedFlow;if(!e.ctrlKey){switch(e.keyCode){case _utils.Key.K:case _utils.Key.UP:this.selectFlowRelative(-1);break;case _utils.Key.J:case _utils.Key.DOWN:this.selectFlowRelative(1);break;case _utils.Key.SPACE:case _utils.Key.PAGE_DOWN:this.selectFlowRelative(10);break;case _utils.Key.PAGE_UP:this.selectFlowRelative(-10);break;case _utils.Key.END:this.selectFlowRelative(1e10);break;case _utils.Key.HOME:this.selectFlowRelative(-1e10);break;case _utils.Key.ESC:this.selectFlow(null);break;case _utils.Key.H:case _utils.Key.LEFT:this.refs.flowDetails&&this.refs.flowDetails.nextTab(-1);break;case _utils.Key.L:case _utils.Key.TAB:case _utils.Key.RIGHT:this.refs.flowDetails&&this.refs.flowDetails.nextTab(1);break;case _utils.Key.C:e.shiftKey&&_actions.FlowActions.clear();break;case _utils.Key.D:t&&(e.shiftKey?_actions.FlowActions.duplicate(t):_actions.FlowActions["delete"](t));break;case _utils.Key.A:e.shiftKey?_actions.FlowActions.accept_all():t&&t.intercepted&&_actions.FlowActions.accept(t);break;case _utils.Key.R:!e.shiftKey&&t&&_actions.FlowActions.replay(t);break;case _utils.Key.V:e.shiftKey&&t&&t.modified&&_actions.FlowActions.revert(t);break;case _utils.Key.E:this.refs.flowDetails&&this.refs.flowDetails.promptEdit();break;case _utils.Key.SHIFT:break;default:return void console.debug("keydown",e.keyCode)}e.preventDefault()}}},{key:"render",value:function(){var e=this,t=this.props,s=t.flows,l=t.selectedFlow,i=t.highlight;t.sort;return _react2["default"].createElement("div",{className:"main-view"},_react2["default"].createElement(_FlowTable2["default"],{ref:"flowTable",flows:s,selected:l,highlight:i,onSelect:function(t){return e.selectFlow(t)}}),l&&[_react2["default"].createElement(_common.Splitter,{key:"splitter"}),_react2["default"].createElement(_index2["default"],{key:"flowDetails",ref:"flowDetails",tab:this.props.routeParams.detailTab,query:this.props.query,updateLocation:this.props.updateLocation,flow:l})])}}]),t}(_react.Component);MainView.propTypes={highlight:_react.PropTypes.string,sort:_react.PropTypes.object},exports["default"]=(0,_reactRedux.connect)(function(e){return{flows:e.flows.view,filter:e.flows.filter,sort:e.flows.sort,highlight:e.flows.highlight,selectedFlow:e.flows.all.byId[e.flows.selected[0]]}},{selectFlow:_flows.selectFlow,setFilter:_flows.setFilter,setHighlight:_flows.setHighlight},void 0,{withRef:!0})(MainView);
+},{"../actions.js":2,"../ducks/flows":43,"../utils.js":52,"./FlowTable":11,"./FlowView":15,"./common/Splitter":36,"react":"react","react-redux":"react-redux"}],30:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function Prompt(e,t){function r(e){return _lodash2["default"].map(i,"key").includes(e)}function o(e){e.stopPropagation(),e.preventDefault();var r=i.find(function(t){return _utils.Key[t.key.toUpperCase()]===e.keyCode});(r||e.keyCode===_utils.Key.ESC||e.keyCode===_utils.Key.ENTER)&&(n(k||!1),t.returnFocus())}for(var a=e.prompt,n=e.done,u=e.options,i=[],s=0;s<u.length;s++){var l=u[s];if(_lodash2["default"].isString(l)){for(var p=l;p.length>0&&r(p[0]);)p=p.substr(1);l={text:l,key:p[0]}}if(!l.text||!l.key||r(l.key))throw"invalid options";i.push(l)}return _react2["default"].createElement("div",{tabIndex:"0",onKeyDown:o,onClick:onClick,className:"prompt-dialog"},_react2["default"].createElement("div",{className:"prompt-content"},a||_react2["default"].createElement("strong",null,"Select: "),i.map(function(e){function t(t){n(e.key),t.stopPropagation()}var r=e.text.indexOf(e.key);return _react2["default"].createElement("span",{key:e.key,className:"option",onClick:t},-1!==r?e.text.substring(0,r):e.text+"(",prefix,_react2["default"].createElement("strong",{className:"text-primary"},e.key),-1!==r?e.text.substring(r+1):")")})))}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=Prompt;var _react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_utils=require("../utils.js");Prompt.contextTypes={returnFocus:_react.PropTypes.func},Prompt.propTypes={options:_react.PropTypes.array.isRequired,done:_react.PropTypes.func.isRequired,prompt:_react.PropTypes.string};
-},{"../actions.js":2,"../ducks/flows":33,"../utils.js":42,"./FlowTable":6,"./common.js":21,"./flowview/index.js":25,"react":"react","react-redux":"react-redux"}],20:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_reactRedux=require("react-redux"),_common=require("./common.js"),_websocket=require("../ducks/websocket"),_Header=require("./Header"),_Header2=_interopRequireDefault(_Header),_EventLog=require("./EventLog"),_EventLog2=_interopRequireDefault(_EventLog),_Footer=require("./Footer"),_Footer2=_interopRequireDefault(_Footer),_store=require("../store/store.js"),_utils=require("../utils.js"),ProxyAppMain=function(e){function t(e,n){_classCallCheck(this,t);var r=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,n));return r.settingsStore=new _store.SettingsStore,_lodash2["default"].extend(r.settingsStore.dict,{}),r.state={settings:r.settingsStore.dict},r.focus=r.focus.bind(r),r.onKeyDown=r.onKeyDown.bind(r),r.updateLocation=r.updateLocation.bind(r),r.onSettingsChange=r.onSettingsChange.bind(r),r}return _inherits(t,e),_createClass(t,[{key:"componentWillMount",value:function(){this.props.wsConnect()}},{key:"updateLocation",value:function(e,t){void 0===e&&(e=this.props.location.pathname);var n=this.props.location.query,r=!0,o=!1,i=void 0;try{for(var a,s=Object.keys(t||{})[Symbol.iterator]();!(r=(a=s.next()).done);r=!0){var u=a.value;n[u]=t[u]||void 0}}catch(c){o=!0,i=c}finally{try{!r&&s["return"]&&s["return"]()}finally{if(o)throw i}}this.context.router.replace({pathname:e,query:n})}},{key:"getQuery",value:function(){return _lodash2["default"].clone(this.props.location.query)}},{key:"componentDidMount",value:function(){this.focus(),this.settingsStore.addListener("recalculate",this.onSettingsChange)}},{key:"componentWillUnmount",value:function(){this.settingsStore.removeListener("recalculate",this.onSettingsChange)}},{key:"onSettingsChange",value:function(){this.setState({settings:this.settingsStore.dict})}},{key:"getChildContext",value:function(){return{returnFocus:this.focus,location:this.props.location}}},{key:"focus",value:function(){document.activeElement.blur(),window.getSelection().removeAllRanges(),_reactDom2["default"].findDOMNode(this).focus()}},{key:"onKeyDown",value:function(e){var t=this,n=null;switch(e.keyCode){case _utils.Key.I:n="intercept";break;case _utils.Key.L:n="search";break;case _utils.Key.H:n="highlight";break;default:var r=this.refs.view;return this.refs.view.getWrappedInstance&&(r=this.refs.view.getWrappedInstance()),void(r.onMainKeyDown&&r.onMainKeyDown(e))}n&&!function(){var e=t.refs.header;e.setState({active:_Header2["default"].entries.MainMenu},function(){e.refs.active.refs[n].select()})}(),e.preventDefault()}},{key:"render",value:function(){var e=this.props,t=e.showEventLog,n=e.location,r=e.children,o=this.state.settings,i=this.getQuery();return _react2["default"].createElement("div",{id:"container",tabIndex:"0",onKeyDown:this.onKeyDown},_react2["default"].createElement(_Header2["default"],{ref:"header",settings:o,updateLocation:this.updateLocation,query:i}),_react2["default"].cloneElement(r,{ref:"view",location:n,query:i,updateLocation:this.updateLocation}),t&&[_react2["default"].createElement(_common.Splitter,{key:"splitter",axis:"y"}),_react2["default"].createElement(_EventLog2["default"],{key:"eventlog"})],_react2["default"].createElement(_Footer2["default"],{settings:o}))}}]),t}(_react.Component);ProxyAppMain.childContextTypes={returnFocus:_react.PropTypes.func.isRequired,location:_react.PropTypes.object.isRequired},ProxyAppMain.contextTypes={router:_react.PropTypes.object.isRequired},exports["default"]=(0,_reactRedux.connect)(function(e){return{showEventLog:e.eventLog.visible}},{wsConnect:_websocket.connect})(ProxyAppMain);
+},{"../utils.js":52,"lodash":"lodash","react":"react","react-dom":"react-dom"}],31:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_reactRedux=require("react-redux"),_websocket=require("../ducks/websocket"),_Header=require("./Header"),_Header2=_interopRequireDefault(_Header),_EventLog=require("./EventLog"),_EventLog2=_interopRequireDefault(_EventLog),_Footer=require("./Footer"),_Footer2=_interopRequireDefault(_Footer),_store=require("../store/store.js"),_utils=require("../utils.js"),ProxyAppMain=function(e){function t(e,n){_classCallCheck(this,t);var r=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,n));return r.settingsStore=new _store.SettingsStore,_lodash2["default"].extend(r.settingsStore.dict,{}),r.state={settings:r.settingsStore.dict},r.focus=r.focus.bind(r),r.onKeyDown=r.onKeyDown.bind(r),r.updateLocation=r.updateLocation.bind(r),r.onSettingsChange=r.onSettingsChange.bind(r),r}return _inherits(t,e),_createClass(t,[{key:"componentWillMount",value:function(){this.props.wsConnect()}},{key:"updateLocation",value:function(e,t){void 0===e&&(e=this.props.location.pathname);var n=this.props.location.query,r=!0,o=!1,i=void 0;try{for(var a,s=Object.keys(t||{})[Symbol.iterator]();!(r=(a=s.next()).done);r=!0){var u=a.value;n[u]=t[u]||void 0}}catch(c){o=!0,i=c}finally{try{!r&&s["return"]&&s["return"]()}finally{if(o)throw i}}this.context.router.replace({pathname:e,query:n})}},{key:"getQuery",value:function(){return _lodash2["default"].clone(this.props.location.query)}},{key:"componentDidMount",value:function(){this.focus(),this.settingsStore.addListener("recalculate",this.onSettingsChange)}},{key:"componentWillUnmount",value:function(){this.settingsStore.removeListener("recalculate",this.onSettingsChange)}},{key:"onSettingsChange",value:function(){this.setState({settings:this.settingsStore.dict})}},{key:"getChildContext",value:function(){return{returnFocus:this.focus}}},{key:"focus",value:function(){document.activeElement.blur(),window.getSelection().removeAllRanges(),_reactDom2["default"].findDOMNode(this).focus()}},{key:"onKeyDown",value:function(e){var t=this,n=null;switch(e.keyCode){case _utils.Key.I:n="intercept";break;case _utils.Key.L:n="search";break;case _utils.Key.H:n="highlight";break;default:var r=this.refs.view;return this.refs.view.getWrappedInstance&&(r=this.refs.view.getWrappedInstance()),void(r.onMainKeyDown&&r.onMainKeyDown(e))}n&&!function(){var e=t.refs.header;e.setState({active:_Header2["default"].entries[0]},function(){e.refs.active.refs[n].select()})}(),e.preventDefault()}},{key:"render",value:function(){var e=this.props,t=e.showEventLog,n=e.location,r=e.children,o=this.state.settings,i=this.getQuery();return _react2["default"].createElement("div",{id:"container",tabIndex:"0",onKeyDown:this.onKeyDown},_react2["default"].createElement(_Header2["default"],{ref:"header",settings:o,updateLocation:this.updateLocation,query:i}),_react2["default"].cloneElement(r,{ref:"view",location:n,query:i,updateLocation:this.updateLocation}),t&&_react2["default"].createElement(_EventLog2["default"],{key:"eventlog"}),_react2["default"].createElement(_Footer2["default"],{settings:o}))}}]),t}(_react.Component);ProxyAppMain.childContextTypes={returnFocus:_react.PropTypes.func.isRequired},ProxyAppMain.contextTypes={router:_react.PropTypes.object.isRequired},exports["default"]=(0,_reactRedux.connect)(function(e){return{showEventLog:e.eventLog.visible}},{wsConnect:_websocket.connect})(ProxyAppMain);
-},{"../ducks/websocket":38,"../store/store.js":41,"../utils.js":42,"./EventLog":4,"./Footer":10,"./Header":11,"./common.js":21,"lodash":"lodash","react":"react","react-dom":"react-dom","react-redux":"react-redux"}],21:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0}),exports.ToggleInputButton=exports.Button=exports.ToggleButton=exports.Splitter=void 0;var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_utils=require("../utils.js"),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),Splitter=exports.Splitter=_react2["default"].createClass({displayName:"Splitter",getDefaultProps:function(){return{axis:"x"}},getInitialState:function(){return{applied:!1,startX:!1,startY:!1}},onMouseDown:function(e){this.setState({startX:e.pageX,startY:e.pageY}),window.addEventListener("mousemove",this.onMouseMove),window.addEventListener("mouseup",this.onMouseUp),window.addEventListener("dragend",this.onDragEnd)},onDragEnd:function(){_reactDom2["default"].findDOMNode(this).style.transform="",window.removeEventListener("dragend",this.onDragEnd),window.removeEventListener("mouseup",this.onMouseUp),window.removeEventListener("mousemove",this.onMouseMove)},onMouseUp:function(e){this.onDragEnd();var t,n=_reactDom2["default"].findDOMNode(this),r=n.previousElementSibling,o=n.nextElementSibling,a=e.pageX-this.state.startX,s=e.pageY-this.state.startY;t="x"===this.props.axis?r.offsetWidth+a:r.offsetHeight+s,r.style.flex="0 0 "+Math.max(0,t)+"px",o.style.flex="1 1 auto",this.setState({applied:!0}),this.onResize()},onMouseMove:function(e){var t=0,n=0;"x"===this.props.axis?t=e.pageX-this.state.startX:n=e.pageY-this.state.startY,_reactDom2["default"].findDOMNode(this).style.transform="translate("+t+"px,"+n+"px)"},onResize:function(){window.setTimeout(function(){window.dispatchEvent(new CustomEvent("resize"))},1)},reset:function(e){if(this.state.applied){var t=_reactDom2["default"].findDOMNode(this),n=t.previousElementSibling,r=t.nextElementSibling;n.style.flex="",r.style.flex="",e||this.setState({applied:!1}),this.onResize()}},componentWillUnmount:function(){this.reset(!0)},render:function(){var e="splitter";return e+="x"===this.props.axis?" splitter-x":" splitter-y",_react2["default"].createElement("div",{className:e},_react2["default"].createElement("div",{onMouseDown:this.onMouseDown,draggable:"true"}))}}),ToggleButton=exports.ToggleButton=function(e){var t=e.checked,n=e.onToggle,r=e.text;return _react2["default"].createElement("div",{className:"btn btn-toggle "+(t?"btn-primary":"btn-default"),onClick:n},_react2["default"].createElement("i",{className:"fa fa-fw "+(t?"fa-check-square-o":"fa-square-o")})," ",r)};ToggleButton.propTypes={checked:_react2["default"].PropTypes.bool.isRequired,onToggle:_react2["default"].PropTypes.func.isRequired,text:_react2["default"].PropTypes.string.isRequired};var Button=exports.Button=function(e){var t=e.onClick,n=e.text,r=e.icon;return _react2["default"].createElement("div",{className:"btn btn-default",onClick:t},_react2["default"].createElement("i",{className:"fa fa-fw "+r})," ",n)};Button.propTypes={onClick:_react2["default"].PropTypes.func.isRequired,text:_react2["default"].PropTypes.string.isRequired};var ToggleInputButton=exports.ToggleInputButton=function(e){function t(e){_classCallCheck(this,t);var n=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e));return n.state={txt:e.txt},n}return _inherits(t,e),_createClass(t,[{key:"render",value:function(){var e=this;return _react2["default"].createElement("div",{className:"input-group toggle-input-btn"},_react2["default"].createElement("span",{className:"input-group-btn",onClick:function(){return e.props.onToggleChanged(e.state.txt)}},_react2["default"].createElement("div",{className:"btn "+(this.props.checked?"btn-primary":"btn-default")},_react2["default"].createElement("span",{className:"fa "+(this.props.checked?"fa-check-square-o":"fa-square-o")})," ",this.props.name)),_react2["default"].createElement("input",{className:"form-control",placeholder:this.props.placeholder,disabled:this.props.checked,value:this.state.txt,type:this.props.inputType,onChange:function(t){return e.setState({txt:t.target.value})},onKeyDown:function(t){t.keyCode===_utils.Key.ENTER&&e.props.onToggleChanged(e.state.txt),t.stopPropagation()}}))}}]),t}(_react2["default"].Component);ToggleInputButton.propTypes={name:_react2["default"].PropTypes.string.isRequired,txt:_react2["default"].PropTypes.string.isRequired,onToggleChanged:_react2["default"].PropTypes.func.isRequired};
+},{"../ducks/websocket":48,"../store/store.js":51,"../utils.js":52,"./EventLog":9,"./Footer":20,"./Header":21,"lodash":"lodash","react":"react","react-dom":"react-dom","react-redux":"react-redux"}],32:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var o in r)Object.prototype.hasOwnProperty.call(r,o)&&(e[o]=r[o])}return e},_createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var o=t[r];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}return function(t,r,o){return r&&e(t.prototype,r),o&&e(t,o),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_ValidateEditor=require("./ValueEditor/ValidateEditor"),_ValidateEditor2=_interopRequireDefault(_ValidateEditor),ValueEditor=function(e){function t(e){_classCallCheck(this,t);var r=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e));return r.focus=r.focus.bind(r),r}return _inherits(t,e),_createClass(t,[{key:"render",value:function(){var e=this,t=this.props.inline?"span":"div";return _react2["default"].createElement(_ValidateEditor2["default"],_extends({},this.props,{onStop:function(){return e.context.returnFocus()},tag:t}))}},{key:"focus",value:function(){_reactDom2["default"].findDOMNode(this).focus()}}]),t}(_react.Component);ValueEditor.contextTypes={returnFocus:_react.PropTypes.func},ValueEditor.propTypes={content:_react.PropTypes.string.isRequired,onDone:_react.PropTypes.func.isRequired,inline:_react.PropTypes.bool},exports["default"]=ValueEditor;
-},{"../utils.js":42,"lodash":"lodash","react":"react","react-dom":"react-dom"}],22:[function(require,module,exports){
-"use strict";function _interopRequireDefault(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.ValueEditor=void 0;var _extends=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var o=arguments[e];for(var n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t},_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_utils=require("../utils.js"),contentToHtml=function(t){return _.escape(t)},nodeToContent=function(t){return t.textContent},EditorBase=_react2["default"].createClass({displayName:"EditorBase",propTypes:{content:_react2["default"].PropTypes.string.isRequired,onDone:_react2["default"].PropTypes.func.isRequired,contentToHtml:_react2["default"].PropTypes.func,nodeToContent:_react2["default"].PropTypes.func,onStop:_react2["default"].PropTypes.func,submitOnEnter:_react2["default"].PropTypes.bool,className:_react2["default"].PropTypes.string,tag:_react2["default"].PropTypes.string},getDefaultProps:function(){return{contentToHtml:contentToHtml,nodeToContent:nodeToContent,submitOnEnter:!0,className:"",tag:"div"}},getInitialState:function(){return{editable:!1}},render:function(){var t="inline-input "+this.props.className,e={__html:this.props.contentToHtml(this.props.content)},o=this.props.tag;return _react2["default"].createElement(o,_extends({},this.props,{tabIndex:"0",className:t,contentEditable:this.state.editable||void 0,onFocus:this.onFocus,onMouseDown:this.onMouseDown,onClick:this.onClick,onBlur:this._stop,onKeyDown:this.onKeyDown,onInput:this.onInput,onPaste:this.onPaste,dangerouslySetInnerHTML:e}))},onPaste:function(t){t.preventDefault();var e=t.clipboardData.getData("text/plain");document.execCommand("insertHTML",!1,e)},onMouseDown:function(t){this._mouseDown=!0,window.addEventListener("mouseup",this.onMouseUp),this.props.onMouseDown&&this.props.onMouseDown(t)},onMouseUp:function(){this._mouseDown&&(this._mouseDown=!1,window.removeEventListener("mouseup",this.onMouseUp))},onClick:function(t){this.onMouseUp(),this.onFocus(t)},onFocus:function(t){if(console.log("onFocus",this._mouseDown,this._ignore_events,this.state.editable),!(this._mouseDown||this._ignore_events||this.state.editable)){var e,o=window.getSelection();if(o.rangeCount>0)e=o.getRangeAt(0);else if(document.caretPositionFromPoint&&t.clientX&&t.clientY){var n=document.caretPositionFromPoint(t.clientX,t.clientY);e=document.createRange(),e.setStart(n.offsetNode,n.offset)}else document.caretRangeFromPoint&&t.clientX&&t.clientY?e=document.caretRangeFromPoint(t.clientX,t.clientY):(e=document.createRange(),e.selectNodeContents(_reactDom2["default"].findDOMNode(this)));this._ignore_events=!0,this.setState({editable:!0},function(){var t=_reactDom2["default"].findDOMNode(this);t.blur(),t.focus(),this._ignore_events=!1})}},stop:function(){_reactDom2["default"].findDOMNode(this).blur(),this.props.onStop&&this.props.onStop()},_stop:function(t){if(!this._ignore_events){console.log("_stop",_.extend({},t)),window.getSelection().removeAllRanges();var e=_reactDom2["default"].findDOMNode(this),o=this.props.nodeToContent(e);this.setState({editable:!1}),this.props.onDone(o),this.props.onBlur&&this.props.onBlur(t)}},reset:function(){_reactDom2["default"].findDOMNode(this).innerHTML=this.props.contentToHtml(this.props.content)},onKeyDown:function(t){switch(t.stopPropagation(),t.keyCode){case _utils.Key.ESC:t.preventDefault(),this.reset(),this.stop();break;case _utils.Key.ENTER:this.props.submitOnEnter&&!t.shiftKey&&(t.preventDefault(),this.stop())}},onInput:function(){var t=_reactDom2["default"].findDOMNode(this),e=this.props.nodeToContent(t);this.props.onInput&&this.props.onInput(e)}}),ValidateEditor=_react2["default"].createClass({displayName:"ValidateEditor",propTypes:{content:_react2["default"].PropTypes.string.isRequired,onDone:_react2["default"].PropTypes.func.isRequired,onInput:_react2["default"].PropTypes.func,isValid:_react2["default"].PropTypes.func,className:_react2["default"].PropTypes.string},getInitialState:function(){return{currentContent:this.props.content}},componentWillReceiveProps:function(){this.setState({currentContent:this.props.content})},onInput:function(t){this.setState({currentContent:t}),this.props.onInput&&this.props.onInput(t)},render:function(){var t=this.props.className||"";return this.props.isValid&&(t+=this.props.isValid(this.state.currentContent)?" has-success":" has-warning"),_react2["default"].createElement(EditorBase,_extends({},this.props,{ref:"editor",className:t,onDone:this.onDone,onInput:this.onInput}))},onDone:function(t){this.props.isValid&&!this.props.isValid(t)&&(this.refs.editor.reset(),t=this.props.content),this.props.onDone(t)}}),ValueEditor=exports.ValueEditor=_react2["default"].createClass({displayName:"ValueEditor",contextTypes:{returnFocus:_react2["default"].PropTypes.func},propTypes:{content:_react2["default"].PropTypes.string.isRequired,onDone:_react2["default"].PropTypes.func.isRequired,inline:_react2["default"].PropTypes.bool},render:function(){var t=this.props.inline?"span":"div";return _react2["default"].createElement(ValidateEditor,_extends({},this.props,{onStop:this.onStop,tag:t}))},focus:function(){_reactDom2["default"].findDOMNode(this).focus()},onStop:function(){this.context.returnFocus()}});
+},{"./ValueEditor/ValidateEditor":34,"react":"react","react-dom":"react-dom"}],33:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(e[o]=n[o])}return e},_createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}return function(t,n,o){return n&&e(t.prototype,n),o&&e(t,o),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_utils=require("../../utils.js"),EditorBase=function(e){function t(e){_classCallCheck(this,t);var n=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e));return n.state={editable:!1},n.onPaste=n.onPaste.bind(n),n.onMouseDown=n.onMouseDown.bind(n),n.onMouseUp=n.onMouseUp.bind(n),n.onFocus=n.onFocus.bind(n),n.onClick=n.onClick.bind(n),n.stop=n.stop.bind(n),n.onBlur=n.onBlur.bind(n),n.reset=n.reset.bind(n),n.onKeyDown=n.onKeyDown.bind(n),n.onInput=n.onInput.bind(n),n}return _inherits(t,e),_createClass(t,[{key:"stop",value:function(){_reactDom2["default"].findDOMNode(this).blur(),this.props.onStop()}},{key:"render",value:function(){return _react2["default"].createElement(this.props.tag,_extends({},this.props,{tabIndex:"0",className:"inline-input "+this.props.className,contentEditable:this.state.editable||void 0,onFocus:this.onFocus,onMouseDown:this.onMouseDown,onClick:this.onClick,onBlur:this.onBlur,onKeyDown:this.onKeyDown,onInput:this.onInput,onPaste:this.onPaste,dangerouslySetInnerHTML:{__html:this.props.contentToHtml(this.props.content)}}))}},{key:"onPaste",value:function(e){e.preventDefault();var t=e.clipboardData.getData("text/plain");document.execCommand("insertHTML",!1,t)}},{key:"onMouseDown",value:function(e){this._mouseDown=!0,window.addEventListener("mouseup",this.onMouseUp),this.props.onMouseDown(e)}},{key:"onMouseUp",value:function(){this._mouseDown&&(this._mouseDown=!1,window.removeEventListener("mouseup",this.onMouseUp))}},{key:"onClick",value:function(e){this.onMouseUp(),this.onFocus(e)}},{key:"onFocus",value:function(e){var t=this;if(!(this._mouseDown||this._ignore_events||this.state.editable)){var n=window.getSelection(),o=void 0;if(n.rangeCount>0)o=n.getRangeAt(0);else if(document.caretPositionFromPoint&&e.clientX&&e.clientY){var s=document.caretPositionFromPoint(e.clientX,e.clientY);o=document.createRange(),o.setStart(s.offsetNode,s.offset)}else document.caretRangeFromPoint&&e.clientX&&e.clientY?o=document.caretRangeFromPoint(e.clientX,e.clientY):(o=document.createRange(),o.selectNodeContents(_reactDom2["default"].findDOMNode(this)));this._ignore_events=!0,this.setState({editable:!0},function(){var e=_reactDom2["default"].findDOMNode(t);e.blur(),e.focus(),t._ignore_events=!1})}}},{key:"onBlur",value:function(e){this._ignore_events||(window.getSelection().removeAllRanges(),this.setState({editable:!1}),this.props.onDone(this.props.nodeToContent(_reactDom2["default"].findDOMNode(this))),this.props.onBlur(e))}},{key:"reset",value:function(){_reactDom2["default"].findDOMNode(this).innerHTML=this.props.contentToHtml(this.props.content)}},{key:"onKeyDown",value:function(e){switch(e.stopPropagation(),e.keyCode){case _utils.Key.ESC:e.preventDefault(),this.reset(),this.stop();break;case _utils.Key.ENTER:this.props.submitOnEnter&&!e.shiftKey&&(e.preventDefault(),this.stop())}}},{key:"onInput",value:function(){this.props.onInput(this.props.nodeToContent(_reactDom2["default"].findDOMNode(this)))}}]),t}(_react.Component);EditorBase.propTypes={content:_react.PropTypes.string.isRequired,onDone:_react.PropTypes.func.isRequired,contentToHtml:_react.PropTypes.func,nodeToContent:_react.PropTypes.func,onStop:_react.PropTypes.func,submitOnEnter:_react.PropTypes.bool,className:_react.PropTypes.string,tag:_react.PropTypes.string},EditorBase.defaultProps={contentToHtml:function(e){return _.escape(e)},nodeToContent:function(e){return e.textContent},submitOnEnter:!0,className:"",tag:"div",onStop:_.noop,onMouseDown:_.noop,onBlur:_.noop,onInput:_.noop},exports["default"]=EditorBase;
-},{"../utils.js":42,"react":"react","react-dom":"react-dom"}],23:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var s in a)Object.prototype.hasOwnProperty.call(a,s)&&(e[s]=a[s])}return e},_react=require("react"),_react2=_interopRequireDefault(_react),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_utils=require("../../flow/utils.js"),_utils2=require("../../utils.js"),ViewImage=_react2["default"].createClass({displayName:"ViewImage",propTypes:{flow:_react2["default"].PropTypes.object.isRequired,message:_react2["default"].PropTypes.object.isRequired},statics:{regex:/^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i,matches:function(e){return ViewImage.regex.test(_utils.MessageUtils.getContentType(e))}},render:function(){var e=_utils.MessageUtils.getContentURL(this.props.flow,this.props.message);return _react2["default"].createElement("div",{className:"flowview-image"},_react2["default"].createElement("img",{src:e,alt:"preview",className:"img-thumbnail"}))}}),ContentLoader=_react2["default"].createClass({displayName:"ContentLoader",propTypes:{flow:_react2["default"].PropTypes.object.isRequired,message:_react2["default"].PropTypes.object.isRequired},getInitialState:function(){return{content:void 0,request:void 0}},requestContent:function(e){this.state.request&&this.state.request.abort();var t=_utils.MessageUtils.getContent(e.flow,e.message);this.setState({content:void 0,request:t}),t.done(function(e){this.setState({content:e})}.bind(this)).fail(function(e,t,a){"abort"!==t&&this.setState({content:"AJAX Error: "+t+"\r\n"+a})}.bind(this)).always(function(){this.setState({request:void 0})}.bind(this))},componentWillMount:function(){this.requestContent(this.props)},componentWillReceiveProps:function(e){e.message!==this.props.message&&this.requestContent(e)},componentWillUnmount:function(){this.state.request&&this.state.request.abort()},render:function(){return this.state.content?_react2["default"].cloneElement(this.props.children,{content:this.state.content}):_react2["default"].createElement("div",{className:"text-center"},_react2["default"].createElement("i",{className:"fa fa-spinner fa-spin"}))}}),ViewRaw=_react2["default"].createClass({displayName:"ViewRaw",propTypes:{content:_react2["default"].PropTypes.string.isRequired},statics:{textView:!0,matches:function(e){return!0}},render:function(){return _react2["default"].createElement("pre",null,this.props.content)}}),ViewJSON=_react2["default"].createClass({displayName:"ViewJSON",propTypes:{content:_react2["default"].PropTypes.string.isRequired},statics:{textView:!0,regex:/^application\/json$/i,matches:function(e){return ViewJSON.regex.test(_utils.MessageUtils.getContentType(e))}},render:function(){var e=this.props.content;try{e=JSON.stringify(JSON.parse(e),null,2)}catch(t){}return _react2["default"].createElement("pre",null,e)}}),ViewAuto=_react2["default"].createClass({displayName:"ViewAuto",propTypes:{message:_react2["default"].PropTypes.object.isRequired,flow:_react2["default"].PropTypes.object.isRequired},statics:{matches:function(){return!1},findView:function(e){for(var t=0;t<all.length;t++)if(all[t].matches(e))return all[t];return all[all.length-1]}},render:function(){var e=this.props,t=e.message,a=e.flow,s=ViewAuto.findView(this.props.message);return s.textView?_react2["default"].createElement(ContentLoader,{message:t,flow:a},_react2["default"].createElement(s,{content:""})):_react2["default"].createElement(s,{message:t,flow:a})}}),all=[ViewAuto,ViewImage,ViewJSON,ViewRaw],ContentEmpty=_react2["default"].createClass({displayName:"ContentEmpty",render:function(){var e=this.props.flow.request===this.props.message?"request":"response";return _react2["default"].createElement("div",{className:"alert alert-info"},"No ",e," content.")}}),ContentMissing=_react2["default"].createClass({displayName:"ContentMissing",render:function(){var e=this.props.flow.request===this.props.message?"Request":"Response";return _react2["default"].createElement("div",{className:"alert alert-info"},e," content missing.")}}),TooLarge=_react2["default"].createClass({displayName:"TooLarge",statics:{isTooLarge:function(e){var t=ViewImage.matches(e)?10:.2;return e.contentLength>1048576*t}},render:function(){var e=(0,_utils2.formatSize)(this.props.message.contentLength);return _react2["default"].createElement("div",{className:"alert alert-warning"},_react2["default"].createElement("button",{onClick:this.props.onClick,className:"btn btn-xs btn-warning pull-right"},"Display anyway"),e," content size.")}}),ViewSelector=_react2["default"].createClass({displayName:"ViewSelector",render:function(){for(var e=[],t=0;t<all.length;t++){var a=all[t],s="btn btn-default";a===this.props.active&&(s+=" active");var r;r=a===ViewAuto?"auto: "+ViewAuto.findView(this.props.message).displayName.toLowerCase().replace("view",""):a.displayName.toLowerCase().replace("view",""),e.push(_react2["default"].createElement("button",{key:a.displayName,onClick:this.props.selectView.bind(null,a),className:s},r))}return _react2["default"].createElement("div",{className:"view-selector btn-group btn-group-xs"},e)}}),ContentView=_react2["default"].createClass({displayName:"ContentView",getInitialState:function(){return{displayLarge:!1,View:ViewAuto}},propTypes:{flow:_react2["default"].PropTypes.object.isRequired,message:_react2["default"].PropTypes.object.isRequired},selectView:function(e){this.setState({View:e})},displayLarge:function(){this.setState({displayLarge:!0})},componentWillReceiveProps:function(e){e.message!==this.props.message&&this.setState(this.getInitialState())},render:function(){var e=this.props,t=e.flow,a=e.message,a=this.props.message;if(0===a.contentLength)return _react2["default"].createElement(ContentEmpty,this.props);if(null===a.contentLength)return _react2["default"].createElement(ContentMissing,this.props);if(!this.state.displayLarge&&TooLarge.isTooLarge(a))return _react2["default"].createElement(TooLarge,_extends({},this.props,{onClick:this.displayLarge}));var s=_utils.MessageUtils.getContentURL(this.props.flow,a);return _react2["default"].createElement("div",null,this.state.View.textView?_react2["default"].createElement(ContentLoader,{flow:t,message:a},_react2["default"].createElement(this.state.View,{content:""})):_react2["default"].createElement(this.state.View,{flow:t,message:a}),_react2["default"].createElement("div",{className:"view-options text-center"},_react2["default"].createElement(ViewSelector,{selectView:this.selectView,active:this.state.View,message:a})," ",_react2["default"].createElement("a",{className:"btn btn-default btn-xs",href:s},_react2["default"].createElement("i",{className:"fa fa-download"}))))}});exports["default"]=ContentView;
+},{"../../utils.js":52,"react":"react","react-dom":"react-dom"}],34:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e},_createClass=function(){function e(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,r,n){return r&&e(t.prototype,r),n&&e(t,n),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_EditorBase=require("./EditorBase"),_EditorBase2=_interopRequireDefault(_EditorBase),ValidateEditor=function(e){function t(e){_classCallCheck(this,t);var r=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e));return r.state={currentContent:e.content},r.onInput=r.onInput.bind(r),r.onDone=r.onDone.bind(r),r}return _inherits(t,e),_createClass(t,[{key:"componentWillReceiveProps",value:function(e){this.setState({currentContent:e.content})}},{key:"onInput",value:function(e){this.setState({currentContent:e}),this.props.onInput&&this.props.onInput(e)}},{key:"onDone",value:function(e){this.props.isValid&&!this.props.isValid(e)&&(this.refs.editor.reset(),e=this.props.content),this.props.onDone(e)}},{key:"render",value:function(){var e=this.props.className||"";return this.props.isValid&&(e+=this.props.isValid(this.state.currentContent)?" has-success":" has-warning"),_react2["default"].createElement(_EditorBase2["default"],_extends({},this.props,{ref:"editor",className:e,onDone:this.onDone,onInput:this.onInput}))}}]),t}(_react.Component);ValidateEditor.propTypes={content:_react.PropTypes.string.isRequired,onDone:_react.PropTypes.func.isRequired,onInput:_react.PropTypes.func,isValid:_react.PropTypes.func,className:_react.PropTypes.string},exports["default"]=ValidateEditor;
-},{"../../flow/utils.js":40,"../../utils.js":42,"lodash":"lodash","react":"react"}],24:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_utils=require("../../utils.js"),TimeStamp=_react2["default"].createClass({displayName:"TimeStamp",render:function(){if(!this.props.t)return _react2["default"].createElement("tr",null);var e,t=(0,_utils.formatTimeStamp)(this.props.t);return this.props.deltaTo?(e=(0,_utils.formatTimeDelta)(1e3*(this.props.t-this.props.deltaTo)),e=_react2["default"].createElement("span",{className:"text-muted"},"("+e+")")):e=null,_react2["default"].createElement("tr",null,_react2["default"].createElement("td",null,this.props.title+":"),_react2["default"].createElement("td",null,t," ",e))}}),ConnectionInfo=_react2["default"].createClass({displayName:"ConnectionInfo",render:function(){var e=this.props.conn,t=e.address.address.join(":"),a=_react2["default"].createElement("tr",{key:"sni"});return e.sni&&(a=_react2["default"].createElement("tr",{key:"sni"},_react2["default"].createElement("td",null,_react2["default"].createElement("abbr",{title:"TLS Server Name Indication"},"TLS SNI:")),_react2["default"].createElement("td",null,e.sni))),_react2["default"].createElement("table",{className:"connection-table"},_react2["default"].createElement("tbody",null,_react2["default"].createElement("tr",{key:"address"},_react2["default"].createElement("td",null,"Address:"),_react2["default"].createElement("td",null,t)),a))}}),CertificateInfo=_react2["default"].createClass({displayName:"CertificateInfo",render:function(){var e=this.props.flow,t=e.client_conn,a=e.server_conn,r={maxHeight:100};return _react2["default"].createElement("div",null,t.cert?_react2["default"].createElement("h4",null,"Client Certificate"):null,t.cert?_react2["default"].createElement("pre",{style:r},t.cert):null,a.cert?_react2["default"].createElement("h4",null,"Server Certificate"):null,a.cert?_react2["default"].createElement("pre",{style:r},a.cert):null)}}),Timing=_react2["default"].createClass({displayName:"Timing",render:function(){var e=this.props.flow,t=e.server_conn,a=e.client_conn,r=e.request,l=e.response,n=[{title:"Server conn. initiated",t:t.timestamp_start,deltaTo:r.timestamp_start},{title:"Server conn. TCP handshake",t:t.timestamp_tcp_setup,deltaTo:r.timestamp_start},{title:"Server conn. SSL handshake",t:t.timestamp_ssl_setup,deltaTo:r.timestamp_start},{title:"Client conn. established",t:a.timestamp_start,deltaTo:r.timestamp_start},{title:"Client conn. SSL handshake",t:a.timestamp_ssl_setup,deltaTo:r.timestamp_start},{title:"First request byte",t:r.timestamp_start},{title:"Request complete",t:r.timestamp_end,deltaTo:r.timestamp_start}];e.response&&n.push({title:"First response byte",t:l.timestamp_start,deltaTo:r.timestamp_start},{title:"Response complete",t:l.timestamp_end,deltaTo:r.timestamp_start}),n.forEach(function(e){e.key=e.title}),n=_lodash2["default"].sortBy(n,"t");var s=n.map(function(e){return _react2["default"].createElement(TimeStamp,e)});return _react2["default"].createElement("div",null,_react2["default"].createElement("h4",null,"Timing"),_react2["default"].createElement("table",{className:"timing-table"},_react2["default"].createElement("tbody",null,s)))}}),Details=_react2["default"].createClass({displayName:"Details",render:function(){var e=this.props.flow,t=e.client_conn,a=e.server_conn;return _react2["default"].createElement("section",null,_react2["default"].createElement("h4",null,"Client Connection"),_react2["default"].createElement(ConnectionInfo,{conn:t}),_react2["default"].createElement("h4",null,"Server Connection"),_react2["default"].createElement(ConnectionInfo,{conn:a}),_react2["default"].createElement(CertificateInfo,{flow:e}),_react2["default"].createElement(Timing,{flow:e}))}});exports["default"]=Details;
+},{"./EditorBase":33,"react":"react","react-dom":"react-dom"}],35:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function Button(e){var t=e.onClick,r=e.text,a=e.icon;return _react2["default"].createElement("div",{className:"btn btn-default",onClick:t},_react2["default"].createElement("i",{className:"fa fa-fw "+a})," ",r)}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=Button;var _react=require("react"),_react2=_interopRequireDefault(_react);Button.propTypes={onClick:_react.PropTypes.func.isRequired,text:_react.PropTypes.string.isRequired};
-},{"../../utils.js":42,"lodash":"lodash","react":"react"}],25:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_nav=require("./nav.js"),_nav2=_interopRequireDefault(_nav),_messages=require("./messages.js"),_details=require("./details.js"),_details2=_interopRequireDefault(_details),_prompt=require("../prompt.js"),_prompt2=_interopRequireDefault(_prompt),allTabs={request:_messages.Request,response:_messages.Response,error:_messages.Error,details:_details2["default"]},FlowView=_react2["default"].createClass({displayName:"FlowView",getInitialState:function(){return{prompt:!1}},getTabs:function(e){var t=[];return["request","response","error"].forEach(function(r){e[r]&&t.push(r)}),t.push("details"),t},nextTab:function(e){var t=this.getTabs(this.props.flow),r=t.indexOf(this.props.tab),s=(r+e+t.length)%t.length;this.selectTab(t[s])},selectTab:function(e){this.props.updateLocation("/flows/"+this.props.flow.id+"/"+e)},promptEdit:function(){var e;switch(this.props.tab){case"request":e=["method","url",{text:"http version",key:"v"},"header"];break;case"response":e=[{text:"http version",key:"v"},"code","message","header"];break;case"details":return;default:throw"Unknown tab for edit: "+this.props.tab}this.setState({prompt:{done:function(e){this.setState({prompt:!1}),e&&this.refs.tab.edit(e)}.bind(this),options:e}})},render:function(){var e=this.props.flow,t=this.getTabs(e),r=this.props.tab;t.indexOf(r)<0&&(r="response"===r&&e.error?"error":"error"===r&&e.response?"response":t[0]);var s=null;this.state.prompt&&(s=_react2["default"].createElement(_prompt2["default"],this.state.prompt));var a=allTabs[r];return _react2["default"].createElement("div",{className:"flow-detail",onScroll:this.adjustHead},_react2["default"].createElement(_nav2["default"],{ref:"head",flow:e,tabs:t,active:r,selectTab:this.selectTab}),_react2["default"].createElement(a,{ref:"tab",flow:e}),s)}});exports["default"]=FlowView;
+},{"react":"react"}],36:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}return function(t,n,o){return n&&e(t.prototype,n),o&&e(t,o),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),Splitter=function(e){function t(e,n){_classCallCheck(this,t);var o=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e,n));return o.state={applied:!1,startX:!1,startY:!1},o.onMouseMove=o.onMouseMove.bind(o),o.onMouseUp=o.onMouseUp.bind(o),o.onDragEnd=o.onDragEnd.bind(o),o}return _inherits(t,e),_createClass(t,[{key:"onMouseDown",value:function(e){this.setState({startX:e.pageX,startY:e.pageY}),window.addEventListener("mousemove",this.onMouseMove),window.addEventListener("mouseup",this.onMouseUp),window.addEventListener("dragend",this.onDragEnd)}},{key:"onDragEnd",value:function(){_reactDom2["default"].findDOMNode(this).style.transform="",window.removeEventListener("dragend",this.onDragEnd),window.removeEventListener("mouseup",this.onMouseUp),window.removeEventListener("mousemove",this.onMouseMove)}},{key:"onMouseUp",value:function(e){this.onDragEnd();var t=_reactDom2["default"].findDOMNode(this),n=t.previousElementSibling,o=n.offsetHeight+e.pageY-this.state.startY;"x"===this.props.axis&&(o=n.offsetWidth+e.pageX-this.state.startX),n.style.flex="0 0 "+Math.max(0,o)+"px",t.nextElementSibling.style.flex="1 1 auto",this.setState({applied:!0}),this.onResize()}},{key:"onMouseMove",value:function(e){var t=0,n=0;"x"===this.props.axis?t=e.pageX-this.state.startX:n=e.pageY-this.state.startY,_reactDom2["default"].findDOMNode(this).style.transform="translate("+t+"px, "+n+"px)"}},{key:"onResize",value:function(){window.setTimeout(function(){return window.dispatchEvent(new CustomEvent("resize"))},1)}},{key:"reset",value:function(e){if(this.state.applied){var t=_reactDom2["default"].findDOMNode(this);t.previousElementSibling.style.flex="",t.nextElementSibling.style.flex="",e||this.setState({applied:!1}),this.onResize()}}},{key:"componentWillUnmount",value:function(){this.reset(!0)}},{key:"render",value:function(){return _react2["default"].createElement("div",{className:(0,_classnames2["default"])("splitter","x"===this.props.axis?"splitter-x":"splitter-y")},_react2["default"].createElement("div",{onMouseDown:this.onMouseDown,draggable:"true"}))}}]),t}(_react.Component);Splitter.defaultProps={axis:"x"},exports["default"]=Splitter;
-},{"../prompt.js":30,"./details.js":24,"./messages.js":26,"./nav.js":27,"react":"react"}],26:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.Error=exports.Response=exports.Request=void 0;var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var s in r)Object.prototype.hasOwnProperty.call(r,s)&&(e[s]=r[s])}return e},_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_actions=require("../../actions.js"),_utils=require("../../flow/utils.js"),_utils2=require("../../utils.js"),_contentview=require("./contentview.js"),_contentview2=_interopRequireDefault(_contentview),_editor=require("../editor.js"),Headers=_react2["default"].createClass({displayName:"Headers",propTypes:{onChange:_react2["default"].PropTypes.func.isRequired,message:_react2["default"].PropTypes.object.isRequired},onChange:function(e,t,r){var s=_lodash2["default"].cloneDeep(this.props.message.headers);s[e][t]=r,s[e][0]||s[e][1]||(1===s.length?(s[0][0]="Name",s[0][1]="Value"):(s.splice(e,1),e===s.length&&(this._nextSel=e-1+"-value"))),this.props.onChange(s)},edit:function(){this.refs["0-key"].focus()},onTab:function(e,t,r){var s=this.props.message.headers;if(e===s.length-1&&1===t){r.preventDefault();var n=_lodash2["default"].cloneDeep(this.props.message.headers);n.push(["Name","Value"]),this.props.onChange(n),this._nextSel=e+1+"-key"}},componentDidUpdate:function(){this._nextSel&&this.refs[this._nextSel]&&(this.refs[this._nextSel].focus(),this._nextSel=void 0)},onRemove:function(e,t,r){1===t?(r.preventDefault(),this.refs[e+"-key"].focus()):e>0&&(r.preventDefault(),this.refs[e-1+"-value"].focus())},render:function(){var e=this.props.message.headers.map(function(e,t){var r=_react2["default"].createElement(HeaderEditor,{ref:t+"-key",content:e[0],onDone:this.onChange.bind(null,t,0),onRemove:this.onRemove.bind(null,t,0),onTab:this.onTab.bind(null,t,0)}),s=_react2["default"].createElement(HeaderEditor,{ref:t+"-value",content:e[1],onDone:this.onChange.bind(null,t,1),onRemove:this.onRemove.bind(null,t,1),onTab:this.onTab.bind(null,t,1)});return _react2["default"].createElement("tr",{key:t},_react2["default"].createElement("td",{className:"header-name"},r,":"),_react2["default"].createElement("td",{className:"header-value"},s))}.bind(this));return _react2["default"].createElement("table",{className:"header-table"},_react2["default"].createElement("tbody",null,e))}}),HeaderEditor=_react2["default"].createClass({displayName:"HeaderEditor",render:function(){return _react2["default"].createElement(_editor.ValueEditor,_extends({ref:"input"},this.props,{onKeyDown:this.onKeyDown,inline:!0}))},focus:function(){_reactDom2["default"].findDOMNode(this).focus()},onKeyDown:function(e){switch(e.keyCode){case _utils2.Key.BACKSPACE:var t=window.getSelection().getRangeAt(0);0===t.startOffset&&0===t.endOffset&&this.props.onRemove(e);break;case _utils2.Key.TAB:e.shiftKey||this.props.onTab(e)}}}),RequestLine=_react2["default"].createClass({displayName:"RequestLine",render:function(){var e=this.props.flow,t=_utils.RequestUtils.pretty_url(e.request),r=e.request.http_version;return _react2["default"].createElement("div",{className:"first-line request-line"},_react2["default"].createElement(_editor.ValueEditor,{ref:"method",content:e.request.method,onDone:this.onMethodChange,inline:!0})," ",_react2["default"].createElement(_editor.ValueEditor,{ref:"url",content:t,onDone:this.onUrlChange,isValid:this.isValidUrl,inline:!0})," ",_react2["default"].createElement(_editor.ValueEditor,{ref:"httpVersion",content:r,onDone:this.onHttpVersionChange,isValid:_utils.isValidHttpVersion,inline:!0}))},isValidUrl:function(e){var t=(0,_utils.parseUrl)(e);return!!t.host},onMethodChange:function(e){_actions.FlowActions.update(this.props.flow,{request:{method:e}})},onUrlChange:function(e){var t=(0,_utils.parseUrl)(e);t.path=t.path||"",_actions.FlowActions.update(this.props.flow,{request:t})},onHttpVersionChange:function(e){var t=(0,_utils.parseHttpVersion)(e);_actions.FlowActions.update(this.props.flow,{request:{http_version:t}})}}),ResponseLine=_react2["default"].createClass({displayName:"ResponseLine",render:function(){var e=this.props.flow,t=e.response.http_version;return _react2["default"].createElement("div",{className:"first-line response-line"},_react2["default"].createElement(_editor.ValueEditor,{ref:"httpVersion",content:t,onDone:this.onHttpVersionChange,isValid:_utils.isValidHttpVersion,inline:!0})," ",_react2["default"].createElement(_editor.ValueEditor,{ref:"code",content:e.response.status_code+"",onDone:this.onCodeChange,isValid:this.isValidCode,inline:!0})," ",_react2["default"].createElement(_editor.ValueEditor,{ref:"msg",content:e.response.reason,onDone:this.onMsgChange,inline:!0}))},isValidCode:function(e){return/^\d+$/.test(e)},onHttpVersionChange:function(e){var t=(0,_utils.parseHttpVersion)(e);_actions.FlowActions.update(this.props.flow,{response:{http_version:t}})},onMsgChange:function(e){_actions.FlowActions.update(this.props.flow,{response:{msg:e}})},onCodeChange:function(e){e=parseInt(e),_actions.FlowActions.update(this.props.flow,{response:{code:e}})}}),Request=exports.Request=_react2["default"].createClass({displayName:"Request",render:function(){var e=this.props.flow;return _react2["default"].createElement("section",{className:"request"},_react2["default"].createElement(RequestLine,{ref:"requestLine",flow:e}),_react2["default"].createElement(Headers,{ref:"headers",message:e.request,onChange:this.onHeaderChange}),_react2["default"].createElement("hr",null),_react2["default"].createElement(_contentview2["default"],{flow:e,message:e.request}))},edit:function(e){switch(e){case"m":this.refs.requestLine.refs.method.focus();break;case"u":this.refs.requestLine.refs.url.focus();break;case"v":this.refs.requestLine.refs.httpVersion.focus();break;case"h":this.refs.headers.edit();break;default:throw"Unimplemented: "+e}},onHeaderChange:function(e){_actions.FlowActions.update(this.props.flow,{request:{headers:e}})}}),Response=exports.Response=_react2["default"].createClass({displayName:"Response",render:function(){var e=this.props.flow;return _react2["default"].createElement("section",{className:"response"},_react2["default"].createElement(ResponseLine,{ref:"responseLine",flow:e}),_react2["default"].createElement(Headers,{ref:"headers",message:e.response,onChange:this.onHeaderChange}),_react2["default"].createElement("hr",null),_react2["default"].createElement(_contentview2["default"],{flow:e,message:e.response}))},edit:function(e){switch(e){case"c":this.refs.responseLine.refs.status_code.focus();break;case"m":this.refs.responseLine.refs.msg.focus();break;case"v":this.refs.responseLine.refs.httpVersion.focus();break;case"h":this.refs.headers.edit();break;default:throw"Unimplemented: "+e}},onHeaderChange:function(e){_actions.FlowActions.update(this.props.flow,{response:{headers:e}})}}),Error=exports.Error=_react2["default"].createClass({displayName:"Error",render:function(){var e=this.props.flow;return _react2["default"].createElement("section",null,_react2["default"].createElement("div",{className:"alert alert-warning"},e.error.msg,_react2["default"].createElement("div",null,_react2["default"].createElement("small",null,(0,_utils2.formatTimeStamp)(e.error.timestamp)))))}});
+},{"classnames":"classnames","react":"react","react-dom":"react-dom"}],37:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function ToggleButton(e){var t=e.checked,r=e.onToggle,a=e.text;return _react2["default"].createElement("div",{className:"btn btn-toggle "+(t?"btn-primary":"btn-default"),onClick:r},_react2["default"].createElement("i",{className:"fa fa-fw "+(t?"fa-check-square-o":"fa-square-o")})," ",a)}Object.defineProperty(exports,"__esModule",{value:!0}),exports["default"]=ToggleButton;var _react=require("react"),_react2=_interopRequireDefault(_react);ToggleButton.propTypes={checked:_react.PropTypes.bool.isRequired,onToggle:_react.PropTypes.func.isRequired,text:_react.PropTypes.string.isRequired};
-},{"../../actions.js":2,"../../flow/utils.js":40,"../../utils.js":42,"../editor.js":22,"./contentview.js":23,"lodash":"lodash","react":"react","react-dom":"react-dom"}],27:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_actions=require("../../actions.js"),NavAction=_react2["default"].createClass({displayName:"NavAction",onClick:function(e){e.preventDefault(),this.props.onClick()},render:function(){return _react2["default"].createElement("a",{title:this.props.title,href:"#",className:"nav-action",onClick:this.onClick},_react2["default"].createElement("i",{className:"fa fa-fw "+this.props.icon}))}}),Nav=_react2["default"].createClass({displayName:"Nav",render:function(){var e=this.props.flow,t=this.props.tabs.map(function(e){var t=e.charAt(0).toUpperCase()+e.slice(1),a=this.props.active===e?"active":"",c=function(t){this.props.selectTab(e),t.preventDefault()}.bind(this);return _react2["default"].createElement("a",{key:e,href:"#",className:a,onClick:c},t)}.bind(this)),a=null;e.intercepted&&(a=_react2["default"].createElement(NavAction,{title:"[a]ccept intercepted flow",icon:"fa-play",onClick:_actions.FlowActions.accept.bind(null,e)}));var c=null;return e.modified&&(c=_react2["default"].createElement(NavAction,{title:"revert changes to flow [V]",icon:"fa-history",onClick:_actions.FlowActions.revert.bind(null,e)})),_react2["default"].createElement("nav",{ref:"head",className:"nav-tabs nav-tabs-sm"},t,_react2["default"].createElement(NavAction,{title:"[d]elete flow",icon:"fa-trash",onClick:_actions.FlowActions["delete"].bind(null,e)}),_react2["default"].createElement(NavAction,{title:"[D]uplicate flow",icon:"fa-copy",onClick:_actions.FlowActions.duplicate.bind(null,e)}),_react2["default"].createElement(NavAction,{disabled:!0,title:"[r]eplay flow",icon:"fa-repeat",onClick:_actions.FlowActions.replay.bind(null,e)}),a,c)}});exports["default"]=Nav;
+},{"react":"react"}],38:[function(require,module,exports){
+"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}return function(t,n,r){return n&&e(t.prototype,n),r&&e(t,r),t}}(),_react=require("react"),_react2=_interopRequireDefault(_react),_classnames=require("classnames"),_classnames2=_interopRequireDefault(_classnames),_utils=require("../../utils"),ToggleInputButton=function(e){function t(e){_classCallCheck(this,t);var n=_possibleConstructorReturn(this,Object.getPrototypeOf(t).call(this,e));return n.state={txt:e.txt},n}return _inherits(t,e),_createClass(t,[{key:"onChange",value:function(e){this.setState({txt:e.target.value})}},{key:"onKeyDown",value:function(e){e.stopPropagation(),e.keyCode===_utils.Key.ENTER&&this.props.onToggleChanged(this.state.txt)}},{key:"render",value:function(){var e=this;return _react2["default"].createElement("div",{className:"input-group toggle-input-btn"},_react2["default"].createElement("span",{className:"input-group-btn",onClick:function(){return e.props.onToggleChanged(e.state.txt)}},_react2["default"].createElement("div",{className:(0,_classnames2["default"])("btn",this.props.checked?"btn-primary":"btn-default")},_react2["default"].createElement("span",{className:(0,_classnames2["default"])("fa",this.props.checked?"fa-check-square-o":"fa-square-o")})," ",this.props.name)),_react2["default"].createElement("input",{className:"form-control",placeholder:this.props.placeholder,disabled:this.props.checked,value:this.state.txt,type:this.props.inputType,onChange:function(t){return e.onChange(t)},onKeyDown:function(t){return e.onKeyDown(t)}}))}}]),t}(_react.Component);ToggleInputButton.propTypes={name:_react.PropTypes.string.isRequired,txt:_react.PropTypes.string.isRequired,onToggleChanged:_react.PropTypes.func.isRequired},exports["default"]=ToggleInputButton;
-},{"../../actions.js":2,"react":"react"}],28:[function(require,module,exports){
+},{"../../utils":52,"classnames":"classnames","react":"react"}],39:[function(require,module,exports){
"use strict";function _interopRequireDefault(t){return t&&t.__esModule?t:{"default":t}}function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function _inherits(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(exports,"__esModule",{value:!0});var _createClass=function(){function t(t,e){for(var o=0;o<e.length;o++){var r=e[o];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}return function(e,o,r){return o&&t(e.prototype,o),r&&t(e,r),e}}(),_get=function t(e,o,r){null===e&&(e=Function.prototype);var n=Object.getOwnPropertyDescriptor(e,o);if(void 0===n){var i=Object.getPrototypeOf(e);return null===i?void 0:t(i,o,r)}if("value"in n)return n.value;var c=n.get;if(void 0!==c)return c.call(r)},_react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),symShouldStick=Symbol("shouldStick"),isAtBottom=function(t){return t.scrollTop+t.clientHeight===t.scrollHeight};exports["default"]=function(t){var e,o;return Object.assign((o=e=function(t){function e(){return _classCallCheck(this,e),_possibleConstructorReturn(this,Object.getPrototypeOf(e).apply(this,arguments))}return _inherits(e,t),_createClass(e,[{key:"componentWillUpdate",value:function(){var t=_reactDom2["default"].findDOMNode(this);this[symShouldStick]=t.scrollTop&&isAtBottom(t),_get(Object.getPrototypeOf(e.prototype),"componentWillUpdate",this)&&_get(Object.getPrototypeOf(e.prototype),"componentWillUpdate",this).call(this)}},{key:"componentDidUpdate",value:function(){var t=_reactDom2["default"].findDOMNode(this);this[symShouldStick]&&!isAtBottom(t)&&(t.scrollTop=t.scrollHeight),_get(Object.getPrototypeOf(e.prototype),"componentDidUpdate",this)&&_get(Object.getPrototypeOf(e.prototype),"componentDidUpdate",this).call(this)}}]),e}(t),e.displayName=t.name,o),t)};
-},{"react":"react","react-dom":"react-dom"}],29:[function(require,module,exports){
+},{"react":"react","react-dom":"react-dom"}],40:[function(require,module,exports){
"use strict";function calcVScroll(t){if(!t)return{start:0,end:0,paddingTop:0,paddingBottom:0};var e=t.itemCount,o=t.rowHeight,r=t.viewportTop,a=t.viewportHeight,i=t.itemHeights,l=r+a,n=0,c=0,d=0,p=0;if(i)for(var h=0,s=0;e>h;h++){var m=i[h]||o;r>=s&&h%2===0&&(d=s,n=h),l>=s?c=h+1:p+=m,s+=m}else n=-2&Math.max(0,Math.floor(r/o)-1),c=Math.min(e,n+Math.ceil(a/o)+2),d=Math.min(n,e)*o,p=Math.max(0,e-c)*o;return{start:n,end:c,paddingTop:d,paddingBottom:p}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.calcVScroll=calcVScroll;
-},{}],30:[function(require,module,exports){
-"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0});var _react=require("react"),_react2=_interopRequireDefault(_react),_reactDom=require("react-dom"),_reactDom2=_interopRequireDefault(_reactDom),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_utils=require("../utils.js"),Prompt=_react2["default"].createClass({displayName:"Prompt",contextTypes:{returnFocus:_react2["default"].PropTypes.func},propTypes:{options:_react2["default"].PropTypes.array.isRequired,done:_react2["default"].PropTypes.func.isRequired,prompt:_react2["default"].PropTypes.string},componentDidMount:function(){_reactDom2["default"].findDOMNode(this).focus()},onKeyDown:function(e){e.stopPropagation(),e.preventDefault();for(var t=this.getOptions(),r=0;r<t.length;r++){var o=t[r].key;if(_utils.Key[o.toUpperCase()]===e.keyCode)return void this.done(o)}e.keyCode!==_utils.Key.ESC&&e.keyCode!==_utils.Key.ENTER||this.done(!1)},onClick:function(e){this.done(!1)},done:function(e){this.props.done(e),this.context.returnFocus()},getOptions:function(){for(var e=[],t=function(t){return _lodash2["default"].includes(_lodash2["default"].map(e,"key"),t)},r=0;r<this.props.options.length;r++){var o=this.props.options[r];if(_lodash2["default"].isString(o)){for(var n=o;n.length>0&&t(n[0]);)n=n.substr(1);o={text:o,key:n[0]}}if(!o.text||!o.key||t(o.key))throw"invalid options";e.push(o)}return e},render:function(){var e=this.getOptions();return e=_lodash2["default"].map(e,function(e){var t,r,o=e.text.indexOf(e.key);-1!==o?(t=e.text.substring(0,o),r=e.text.substring(o+1)):(t=e.text+" (",r=")");var n=function(t){this.done(e.key),t.stopPropagation()}.bind(this);return _react2["default"].createElement("span",{key:e.key,className:"option",onClick:n},t,_react2["default"].createElement("strong",{className:"text-primary"},e.key),r)}.bind(this)),_react2["default"].createElement("div",{tabIndex:"0",onKeyDown:this.onKeyDown,onClick:this.onClick,className:"prompt-dialog"},_react2["default"].createElement("div",{className:"prompt-content"},this.props.prompt||_react2["default"].createElement("strong",null,"Select: "),e))}});exports["default"]=Prompt;
-
-},{"../utils.js":42,"lodash":"lodash","react":"react","react-dom":"react-dom"}],31:[function(require,module,exports){
+},{}],41:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.AppDispatcher=void 0;var _flux=require("flux"),_flux2=_interopRequireDefault(_flux),PayloadSources={VIEW:"view",SERVER:"server"},AppDispatcher=exports.AppDispatcher=new _flux2["default"].Dispatcher;AppDispatcher.dispatchViewAction=function(e){e.source=PayloadSources.VIEW,this.dispatch(e)},AppDispatcher.dispatchServerAction=function(e){e.source=PayloadSources.SERVER,this.dispatch(e)};
-},{"flux":"flux"}],32:[function(require,module,exports){
+},{"flux":"flux"}],42:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _defineProperty(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function reducer(){var e=arguments.length<=0||void 0===arguments[0]?defaultState:arguments[0],t=arguments[1];switch(t.type){case TOGGLE_FILTER:var i=_extends({},e.filter,_defineProperty({},t.filter,!e.filter[t.filter]));return _extends({},e,{filter:i,filteredEvents:(0,_view.updateViewFilter)(e.events,function(e){return i[e.level]})});case TOGGLE_VISIBILITY:return _extends({},e,{visible:!e.visible});case UPDATE_LOG:var r=reduceList(e.events,t);return _extends({},e,{events:r,filteredEvents:(0,_view.updateViewList)(e.filteredEvents,e.events,r,t,function(t){return e.filter[t.level]})});default:return e}}function toggleEventLogFilter(e){return{type:TOGGLE_FILTER,filter:e}}function toggleEventLogVisibility(){return{type:TOGGLE_VISIBILITY}}function addLogEntry(e){var t=arguments.length<=1||void 0===arguments[1]?"web":arguments[1];return addItem({message:e,level:t,id:"log-"+id++})}Object.defineProperty(exports,"__esModule",{value:!0}),exports.fetchLogEntries=exports.updateLogEntries=exports.UPDATE_LOG=void 0;var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var i=arguments[t];for(var r in i)Object.prototype.hasOwnProperty.call(i,r)&&(e[r]=i[r])}return e};exports["default"]=reducer,exports.toggleEventLogFilter=toggleEventLogFilter,exports.toggleEventLogVisibility=toggleEventLogVisibility,exports.addLogEntry=addLogEntry;var _list=require("./utils/list"),_list2=_interopRequireDefault(_list),_view=require("./utils/view"),TOGGLE_FILTER="TOGGLE_EVENTLOG_FILTER",TOGGLE_VISIBILITY="TOGGLE_EVENTLOG_VISIBILITY",UPDATE_LOG=exports.UPDATE_LOG="UPDATE_EVENTLOG",_makeList=(0,_list2["default"])(UPDATE_LOG,"/events"),reduceList=_makeList.reduceList,updateList=_makeList.updateList,fetchList=_makeList.fetchList,addItem=_makeList.addItem,defaultState={visible:!1,filter:{debug:!1,info:!0,web:!0},events:reduceList(),filteredEvents:[]},id=0;exports.updateLogEntries=updateList,exports.fetchLogEntries=fetchList;
-},{"./utils/list":36,"./utils/view":37}],33:[function(require,module,exports){
+},{"./utils/list":46,"./utils/view":47}],43:[function(require,module,exports){
"use strict";function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t["default"]=e,t}function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function makeFilterFn(e){return e?_filt2["default"].parse(e):function(){return!0}}function makeSortFn(e){var t=columns[e.sortColumn];if(t){var r=t.sortKeyFun;return e.sortDesc&&(r=r&&function(e){var r=t.sortKeyFun(e);return _.isString(r)?(0,_utils.reverseString)(""+r):-r}),r}}function reducer(){var e=arguments.length<=0||void 0===arguments[0]?defaultState:arguments[0],t=arguments[1];switch(t.type){case UPDATE_FLOWS:var r=reduceList(e.all,t);return _extends({},e,{all:r,view:(0,_view.updateViewList)(e.view,e.all,r,t,makeFilterFn(t.filter),makeSortFn(e.sort))});case SET_FILTER:return _extends({},e,{filter:t.filter,view:(0,_view.updateViewFilter)(e.all,makeFilterFn(t.filter),makeSortFn(e.sort))});case SET_HIGHLIGHT:return _extends({},e,{highlight:t.highlight});case SET_SORT:return _extends({},e,{sort:t.sort,view:(0,_view.updateViewSort)(e.view,makeSortFn(t.sort))});case SELECT_FLOW:return _extends({},e,{selected:[t.flowId]});default:return e}}function setFilter(e){return{type:SET_FILTER,filter:e}}function setHighlight(e){return{type:SET_HIGHLIGHT,highlight:e}}function setSort(e){return{type:SET_SORT,sort:e}}function selectFlow(e){return function(t,r){t({type:SELECT_FLOW,currentSelection:r().flows.selected[0],flowId:e})}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.fetchFlows=exports.updateFlows=exports.SELECT_FLOW=exports.SET_SORT=exports.SET_HIGHLIGHT=exports.SET_FILTER=exports.UPDATE_FLOWS=void 0;var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var i in r)Object.prototype.hasOwnProperty.call(r,i)&&(e[i]=r[i])}return e};exports["default"]=reducer,exports.setFilter=setFilter,exports.setHighlight=setHighlight,exports.setSort=setSort,exports.selectFlow=selectFlow;var _list=require("./utils/list"),_list2=_interopRequireDefault(_list),_filt=require("../filt/filt"),_filt2=_interopRequireDefault(_filt),_view=require("./utils/view"),_utils=require("../utils.js"),_FlowColumns=require("../components/FlowTable/FlowColumns"),columns=_interopRequireWildcard(_FlowColumns),UPDATE_FLOWS=exports.UPDATE_FLOWS="UPDATE_FLOWS",SET_FILTER=exports.SET_FILTER="SET_FLOW_FILTER",SET_HIGHLIGHT=exports.SET_HIGHLIGHT="SET_FLOW_HIGHLIGHT",SET_SORT=exports.SET_SORT="SET_FLOW_SORT",SELECT_FLOW=exports.SELECT_FLOW="SELECT_FLOW",_makeList=(0,_list2["default"])(UPDATE_FLOWS,"/flows"),reduceList=_makeList.reduceList,updateList=_makeList.updateList,fetchList=_makeList.fetchList,defaultState={all:reduceList(),selected:[],view:[],filter:void 0,highlight:void 0,sort:{sortColumn:void 0,sortDesc:!1}};exports.updateFlows=updateList,exports.fetchFlows=fetchList;
-},{"../components/FlowTable/FlowColumns":7,"../filt/filt":39,"../utils.js":42,"./utils/list":36,"./utils/view":37}],34:[function(require,module,exports){
+},{"../components/FlowTable/FlowColumns":12,"../filt/filt":49,"../utils.js":52,"./utils/list":46,"./utils/view":47}],44:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0});var _redux=require("redux"),_eventLog=require("./eventLog"),_eventLog2=_interopRequireDefault(_eventLog),_websocket=require("./websocket"),_websocket2=_interopRequireDefault(_websocket),_flows=require("./flows"),_flows2=_interopRequireDefault(_flows),_ui=require("./ui"),_ui2=_interopRequireDefault(_ui),rootReducer=(0,_redux.combineReducers)({eventLog:_eventLog2["default"],websocket:_websocket2["default"],flows:_flows2["default"],ui:_ui2["default"]});exports["default"]=rootReducer;
-},{"./eventLog":32,"./flows":33,"./ui":35,"./websocket":38,"redux":"redux"}],35:[function(require,module,exports){
+},{"./eventLog":42,"./flows":43,"./ui":45,"./websocket":48,"redux":"redux"}],45:[function(require,module,exports){
"use strict";function reducer(){var e=arguments.length<=0||void 0===arguments[0]?defaultState:arguments[0],t=arguments[1];switch(t.type){case SET_ACTIVE_MENU:return _extends({},e,{activeMenu:t.activeMenu});case _flows.SELECT_FLOW:var r=t.flowId&&!t.currentSelection,n=!t.flowId&&t.currentSelection;return r?_extends({},e,{activeMenu:"Flow"}):n&&"Flow"===e.activeMenu?_extends({},e,{activeMenu:"Start"}):e;default:return e}}function setActiveMenu(e){return{type:SET_ACTIVE_MENU,activeMenu:e}}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e};exports["default"]=reducer,exports.setActiveMenu=setActiveMenu;var _flows=require("./flows"),SET_ACTIVE_MENU="SET_ACTIVE_MENU",defaultState={activeMenu:"Start"};
-},{"./flows":33}],36:[function(require,module,exports){
+},{"./flows":43}],46:[function(require,module,exports){
"use strict";function _defineProperty(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function _toConsumableArray(e){if(Array.isArray(e)){for(var t=0,r=Array(e.length);t<e.length;t++)r[t]=e[t];return r}return Array.from(e)}function makeList(e,t){function r(){var t=arguments.length<=0||void 0===arguments[0]?defaultState:arguments[0],n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];if(n.type!==e)return t;if(n.cmd===RECEIVE_LIST){for(var i={isFetching:!1,actionsDuringFetch:[],list:n.list,byId:{},indexOf:{}},o=0;o<n.list.length;o++){var d=n.list[o];i.byId[d.id]=d,i.indexOf[d.id]=o}var a=!0,s=!1,u=void 0;try{for(var c,f=t.actionsDuringFetch[Symbol.iterator]();!(a=(c=f.next()).done);a=!0)n=c.value,i=r(i,n)}catch(l){s=!0,u=l}finally{try{!a&&f["return"]&&f["return"]()}finally{if(s)throw u}}return i}if(t.isFetching)return _extends({},t,{actionsDuringFetch:[].concat(_toConsumableArray(t.actionsDuringFetch),[n])});var E=void 0,y=void 0;switch(n.cmd){case ADD:return{list:[].concat(_toConsumableArray(t.list),[n.item]),byId:_extends({},t.byId,_defineProperty({},n.item.id,n.item)),indexOf:_extends({},t.indexOf,_defineProperty({},n.item.id,t.list.length))};case UPDATE:return E=[].concat(_toConsumableArray(t.list)),y=t.indexOf[n.item.id],E[y]=n.item,_extends({},t,{list:E,byId:_extends({},t.byId,_defineProperty({},n.item.id,n.item))});case REMOVE:return E=[].concat(_toConsumableArray(t.list)),y=t.indexOf[n.item.id],E.splice(y,1),_extends({},t,{list:E,byId:_extends({},t.byId,_defineProperty({},n.item.id,void 0)),indexOf:_extends({},t.indexOf,_defineProperty({},n.item.id,void 0))});case REQUEST_LIST:return _extends({},t,{isFetching:!0});default:return console.debug("unknown action",n),t}}function n(t){return{type:e,cmd:ADD,item:t}}function i(t){return{type:e,cmd:UPDATE,item:t}}function o(t){return{type:e,cmd:REMOVE,item:t}}function d(e){return function(t){switch(e.cmd){case"add":return t(n(e.data));case"update":return t(i(e.data));case"remove":return t(o(e.data));case"reset":return t(u());default:console.error("unknown list update",e)}}}function a(){return{type:e,cmd:REQUEST_LIST}}function s(t){return{type:e,cmd:RECEIVE_LIST,list:t}}function u(){return function(e){return e(a()),(0,_utils.fetchApi)(t).then(function(t){return t.json().then(function(t){e(s(t.data))})})}}return{reduceList:r,updateList:d,fetchList:u,addItem:n,updateItem:i,removeItem:o}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.RECEIVE_LIST=exports.REQUEST_LIST=exports.REMOVE=exports.UPDATE=exports.ADD=void 0;var _extends=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(e[n]=r[n])}return e};exports["default"]=makeList;var _utils=require("../../utils"),ADD=exports.ADD="ADD",UPDATE=exports.UPDATE="UPDATE",REMOVE=exports.REMOVE="REMOVE",REQUEST_LIST=exports.REQUEST_LIST="REQUEST_LIST",RECEIVE_LIST=exports.RECEIVE_LIST="RECEIVE_LIST",defaultState={list:[],isFetching:!1,actionsDuringFetch:[],byId:{},indexOf:{}};
-},{"../../utils":42}],37:[function(require,module,exports){
+},{"../../utils":52}],47:[function(require,module,exports){
"use strict";function _toConsumableArray(e){if(Array.isArray(e)){for(var t=0,r=Array(e.length);t<e.length;t++)r[t]=e[t];return r}return Array.from(e)}function sortedIndexOf(e,t,r){r||(r=function(e){return 0});for(var n=0,o=e.length,i=r(t),u=void 0;o>n;)u=n+o>>>1,r(e[u])<i?n=u+1:o=u;for(;e[n].id!==t.id&&r(e[n+1])===i;)n++;return n}function updateViewList(e,t,r,n){var o=arguments.length<=4||void 0===arguments[4]?defaultFilterFn:arguments[4],i=arguments.length<=5||void 0===arguments[5]?defaultSortFn:arguments[5];switch(n.cmd){case _list.REQUEST_LIST:return e;case _list.RECEIVE_LIST:return updateViewFilter(r,o,i);case _list.ADD:return o(n.item)?sortedInsert(e,i,n.item):e;case _list.UPDATE:var u=t.byId[n.item.id],d=n.item,a=o(u),f=o(d);if(!a&&f)return sortedInsert(e,i,n.item);if(a&&!f)return sortedRemove(e,i,n.item);if(a&&f){var s=function(){var t=[].concat(_toConsumableArray(e));return t.indexOf=function(e){return sortedIndexOf(t,e,i)},t[t.indexOf(u)]=d,i&&i(u)!==i(d)&&t.sort(makeCompareFn(i)),{v:t}}();if("object"===("undefined"==typeof s?"undefined":_typeof(s)))return s.v}return e;case _list.REMOVE:var l=o(t.byId[n.item.id]);return l?sortedRemove(e,i,n.item):e;default:return console.error("Unknown list action: ",n),e}}function updateViewFilter(e){var t=arguments.length<=1||void 0===arguments[1]?defaultFilterFn:arguments[1],r=arguments.length<=2||void 0===arguments[2]?defaultSortFn:arguments[2],n=e.list.filter(t);return r&&n.sort(makeCompareFn(r)),n.indexOf=function(e){return sortedIndexOf(n,e,r)},n}function updateViewSort(e){var t=arguments.length<=1||void 0===arguments[1]?defaultSortFn:arguments[1],r=[].concat(_toConsumableArray(e));return t&&r.sort(makeCompareFn(t)),r.indexOf=function(e){return sortedIndexOf(r,e,t)},r}Object.defineProperty(exports,"__esModule",{value:!0});var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};exports.sortedIndexOf=sortedIndexOf,exports.updateViewList=updateViewList,exports.updateViewFilter=updateViewFilter,exports.updateViewSort=updateViewSort;var _list=require("./list"),defaultFilterFn=function(e){return!0},defaultSortFn=!1,makeCompareFn=function(e){var t=function(t,r){var n=e(t),o=e(r);return o>n?-1:n>o?1:0};return t},sortedInsert=function(e,t,r){var n=[].concat(_toConsumableArray(e),[r]);n.indexOf=function(e){return sortedIndexOf(n,e,t)};var o=makeCompareFn(t);return t&&o(e[e.length-1],r)>0&&(console.debug("sorting view..."),n.sort(o)),n},sortedRemove=function(e,t,r){var n=r.id,o=e.filter(function(e){return e.id!==n});return o.indexOf=function(e){return sortedIndexOf(o,e,t)},o};
-},{"./list":36}],38:[function(require,module,exports){
+},{"./list":46}],48:[function(require,module,exports){
"use strict";function _interopRequireWildcard(e){if(e&&e.__esModule)return e;var n={};if(null!=e)for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(n[t]=e[t]);return n["default"]=e,n}function reducer(){var e=arguments.length<=0||void 0===arguments[0]?defaultState:arguments[0],n=arguments[1];switch(n.type){case CONNECT:return _extends({},e,{socket:n.socket});case CONNECTED:return _extends({},e,{connected:!0});case DISCONNECT:return _extends({},e,{connected:!1});case DISCONNECTED:return _extends({},e,{socket:null});default:return e}}function connect(){return function(e){var n=new WebSocket(location.origin.replace("http","ws")+"/updates");return window.ws=n,n.addEventListener("open",function(){return e(onConnect())}),n.addEventListener("close",function(){return e(onDisconnect())}),n.addEventListener("message",function(n){return e(onMessage(n))}),n.addEventListener("error",function(n){return e(onError(n))}),e({type:CONNECT,socket:n}),n}}function disconnect(){return{type:DISCONNECT}}function onConnect(){return function(e){e({type:CONNECTED}),e(flowActions.fetchFlows()).then(function(){return _actions.ConnectionActions.open()})}}function onDisconnect(){return function(e){_actions.ConnectionActions.close(),e(eventLogActions.addLogEntry("WebSocket connection closed.")),e({type:DISCONNECTED})}}function onMessage(e){return function(n){var t=JSON.parse(e.data);switch(_dispatcher.AppDispatcher.dispatchServerAction(t),t.type){case eventLogActions.UPDATE_LOG:return n(eventLogActions.updateLogEntries(t));case flowActions.UPDATE_FLOWS:return n(flowActions.updateFlows(t));default:console.warn("unknown message",t)}n({type:MESSAGE,msg:e})}}function onError(e){return function(n){_actions.ConnectionActions.error(),n(eventLogActions.addLogEntry("WebSocket connection error.")),n({type:ERROR,error:e})}}Object.defineProperty(exports,"__esModule",{value:!0});var _extends=Object.assign||function(e){for(var n=1;n<arguments.length;n++){var t=arguments[n];for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o])}return e};exports["default"]=reducer,exports.connect=connect,exports.disconnect=disconnect,exports.onConnect=onConnect,exports.onDisconnect=onDisconnect,exports.onMessage=onMessage,exports.onError=onError;var _actions=require("../actions.js"),_dispatcher=require("../dispatcher.js"),_eventLog=require("./eventLog"),eventLogActions=_interopRequireWildcard(_eventLog),_flows=require("./flows"),flowActions=_interopRequireWildcard(_flows),CONNECT="WEBSOCKET_CONNECT",CONNECTED="WEBSOCKET_CONNECTED",DISCONNECT="WEBSOCKET_DISCONNECT",DISCONNECTED="WEBSOCKET_DISCONNECTED",ERROR="WEBSOCKET_ERROR",MESSAGE="WEBSOCKET_MESSAGE",defaultState={connected:!1,socket:null};
-},{"../actions.js":2,"../dispatcher.js":31,"./eventLog":32,"./flows":33}],39:[function(require,module,exports){
+},{"../actions.js":2,"../dispatcher.js":41,"./eventLog":42,"./flows":43}],49:[function(require,module,exports){
"use strict";module.exports=function(){function e(e,t){function r(){this.constructor=e}r.prototype=t.prototype,e.prototype=new r}function t(e,r,n,i){this.message=e,this.expected=r,this.found=n,this.location=i,this.name="SyntaxError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,t)}function r(e){function r(t){var r,n,i=Ot[t];if(i)return i;for(r=t-1;!Ot[r];)r--;for(i=Ot[r],i={line:i.line,column:i.column,seenCR:i.seenCR};t>r;)n=e.charAt(r),"\n"===n?(i.seenCR||i.line++,i.column=1,i.seenCR=!1):"\r"===n||"\u2028"===n||"\u2029"===n?(i.line++,i.column=1,i.seenCR=!0):(i.column++,i.seenCR=!1),r++;return Ot[t]=i,i}function n(e,t){var n=r(e),i=r(t);return{start:{offset:e,line:n.line,column:n.column},end:{offset:t,line:i.line,column:i.column}}}function i(e){Pt>Mt||(Mt>Pt&&(Pt=Mt,Qt=[]),Qt.push(e))}function s(e,r,n,i){function s(e){var t=1;for(e.sort(function(e,t){return e.description<t.description?-1:e.description>t.description?1:0});t<e.length;)e[t-1]===e[t]?e.splice(t,1):t++}function u(e,t){function r(e){function t(e){return e.charCodeAt(0).toString(16).toUpperCase()}return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\x08/g,"\\b").replace(/\t/g,"\\t").replace(/\n/g,"\\n").replace(/\f/g,"\\f").replace(/\r/g,"\\r").replace(/[\x00-\x07\x0B\x0E\x0F]/g,function(e){return"\\x0"+t(e)}).replace(/[\x10-\x1F\x80-\xFF]/g,function(e){return"\\x"+t(e)}).replace(/[\u0100-\u0FFF]/g,function(e){return"\\u0"+t(e)}).replace(/[\u1000-\uFFFF]/g,function(e){return"\\u"+t(e)})}var n,i,s,u=new Array(e.length);for(s=0;s<e.length;s++)u[s]=e[s].description;return n=e.length>1?u.slice(0,-1).join(", ")+" or "+u[e.length-1]:u[0],i=t?'"'+r(t)+'"':"end of input","Expected "+n+" but "+i+" found."}return null!==r&&s(r),new t(null!==e?e:u(r,n),r,n,i)}function u(){var e,t,r,n;return Vt++,e=Mt,t=a(),t!==P?(r=l(),r!==P?(n=a(),n!==P?(Nt=e,t=X(r),e=t):(Mt=e,e=P)):(Mt=e,e=P)):(Mt=e,e=P),Vt--,e===P&&(t=P,0===Vt&&i(W)),e}function c(){var t,r;return Vt++,Z.test(e.charAt(Mt))?(t=e.charAt(Mt),Mt++):(t=P,0===Vt&&i($)),Vt--,t===P&&(r=P,0===Vt&&i(Y)),t}function o(){var t,r;return Vt++,te.test(e.charAt(Mt))?(t=e.charAt(Mt),Mt++):(t=P,0===Vt&&i(re)),Vt--,t===P&&(r=P,0===Vt&&i(ee)),t}function a(){var e,t;for(Vt++,e=[],t=c();t!==P;)e.push(t),t=c();return Vt--,e===P&&(t=P,0===Vt&&i(ne)),e}function l(){var t,r,n,s,u,c;return t=Mt,r=p(),r!==P?(n=a(),n!==P?(124===e.charCodeAt(Mt)?(s=ie,Mt++):(s=P,0===Vt&&i(se)),s!==P?(u=a(),u!==P?(c=l(),c!==P?(Nt=t,r=ue(r,c),t=r):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P),t===P&&(t=p()),t}function p(){var t,r,n,s,u,o;if(t=Mt,r=f(),r!==P?(n=a(),n!==P?(38===e.charCodeAt(Mt)?(s=ce,Mt++):(s=P,0===Vt&&i(oe)),s!==P?(u=a(),u!==P?(o=p(),o!==P?(Nt=t,r=ae(r,o),t=r):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P),t===P){if(t=Mt,r=f(),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=p(),s!==P?(Nt=t,r=ae(r,s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;t===P&&(t=f())}return t}function f(){var t,r,n,s;return t=Mt,33===e.charCodeAt(Mt)?(r=le,Mt++):(r=P,0===Vt&&i(pe)),r!==P?(n=a(),n!==P?(s=f(),s!==P?(Nt=t,r=fe(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P),t===P&&(t=h()),t}function h(){var t,r,n,s,u,c;return t=Mt,40===e.charCodeAt(Mt)?(r=he,Mt++):(r=P,0===Vt&&i(de)),r!==P?(n=a(),n!==P?(s=l(),s!==P?(u=a(),u!==P?(41===e.charCodeAt(Mt)?(c=ve,Mt++):(c=P,0===Vt&&i(ye)),c!==P?(Nt=t,r=ge(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P)):(Mt=t,t=P),t===P&&(t=d()),t}function d(){var e;return e=v(),e===P&&(e=g()),e}function v(){var t,r;return t=y(),t===P&&(t=Mt,e.substr(Mt,2)===Ae?(r=Ae,Mt+=2):(r=P,0===Vt&&i(xe)),r!==P&&(Nt=t,r=Re()),t=r,t===P&&(t=Mt,e.substr(Mt,2)===me?(r=me,Mt+=2):(r=P,0===Vt&&i(qe)),r!==P&&(Nt=t,r=Ce()),t=r,t===P&&(t=Mt,e.substr(Mt,2)===we?(r=we,Mt+=2):(r=P,0===Vt&&i(Ee)),r!==P&&(Nt=t,r=be()),t=r,t===P&&(t=Mt,e.substr(Mt,2)===Fe?(r=Fe,Mt+=2):(r=P,0===Vt&&i(Ue)),r!==P&&(Nt=t,r=je()),t=r)))),t}function y(){var t,r;return t=Mt,e.substr(Mt,4)===Te?(r=Te,Mt+=4):(r=P,0===Vt&&i(_e)),r!==P&&(Nt=t,r=Se()),t=r,t===P&&(t=Mt,e.substr(Mt,5)===ke?(r=ke,Mt+=5):(r=P,0===Vt&&i(Be)),r!==P&&(Nt=t,r=Ie()),t=r),t}function g(){var t,r,n,s;if(t=Mt,e.substr(Mt,2)===ze?(r=ze,Mt+=2):(r=P,0===Vt&&i(De)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=A(),s!==P?(Nt=t,r=Ge(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,2)===He?(r=He,Mt+=2):(r=P,0===Vt&&i(Je)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=Ke(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,2)===Le?(r=Le,Mt+=2):(r=P,0===Vt&&i(Me)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=Ne(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,3)===Oe?(r=Oe,Mt+=3):(r=P,0===Vt&&i(Pe)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=Qe(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,3)===Ve?(r=Ve,Mt+=3):(r=P,0===Vt&&i(We)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=Xe(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,2)===Ye?(r=Ye,Mt+=2):(r=P,0===Vt&&i(Ze)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=$e(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,2)===et?(r=et,Mt+=2):(r=P,0===Vt&&i(tt)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=rt(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,3)===nt?(r=nt,Mt+=3):(r=P,0===Vt&&i(it)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=st(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,3)===ut?(r=ut,Mt+=3):(r=P,0===Vt&&i(ct)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=ot(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,e.substr(Mt,2)===at?(r=at,Mt+=2):(r=P,0===Vt&&i(lt)),r!==P){if(n=[],s=c(),s!==P)for(;s!==P;)n.push(s),s=c();else n=P;n!==P?(s=x(),s!==P?(Nt=t,r=pt(s),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;t===P&&(t=Mt,r=x(),r!==P&&(Nt=t,r=pt(r)),t=r)}}}}}}}}}return t}function A(){var t,r,n,s;if(Vt++,t=Mt,ht.test(e.charAt(Mt))?(r=e.charAt(Mt),Mt++):(r=P,0===Vt&&i(dt)),r===P&&(r=null),r!==P){if(n=[],vt.test(e.charAt(Mt))?(s=e.charAt(Mt),Mt++):(s=P,0===Vt&&i(yt)),s!==P)for(;s!==P;)n.push(s),vt.test(e.charAt(Mt))?(s=e.charAt(Mt),Mt++):(s=P,0===Vt&&i(yt));else n=P;n!==P?(ht.test(e.charAt(Mt))?(s=e.charAt(Mt),Mt++):(s=P,0===Vt&&i(dt)),s===P&&(s=null),s!==P?(Nt=t,r=gt(n),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;return Vt--,t===P&&(r=P,0===Vt&&i(ft)),t}function x(){var t,r,n,s;if(Vt++,t=Mt,34===e.charCodeAt(Mt)?(r=xt,Mt++):(r=P,0===Vt&&i(Rt)),r!==P){for(n=[],s=R();s!==P;)n.push(s),s=R();n!==P?(34===e.charCodeAt(Mt)?(s=xt,Mt++):(s=P,0===Vt&&i(Rt)),s!==P?(Nt=t,r=mt(n),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P){if(t=Mt,39===e.charCodeAt(Mt)?(r=qt,Mt++):(r=P,0===Vt&&i(Ct)),r!==P){for(n=[],s=m();s!==P;)n.push(s),s=m();n!==P?(39===e.charCodeAt(Mt)?(s=qt,Mt++):(s=P,0===Vt&&i(Ct)),s!==P?(Nt=t,r=mt(n),t=r):(Mt=t,t=P)):(Mt=t,t=P)}else Mt=t,t=P;if(t===P)if(t=Mt,r=Mt,Vt++,n=o(),Vt--,n===P?r=void 0:(Mt=r,r=P),r!==P){if(n=[],s=q(),s!==P)for(;s!==P;)n.push(s),s=q();else n=P;n!==P?(Nt=t,r=mt(n),t=r):(Mt=t,t=P)}else Mt=t,t=P}return Vt--,t===P&&(r=P,0===Vt&&i(At)),t}function R(){var t,r,n;return t=Mt,r=Mt,Vt++,wt.test(e.charAt(Mt))?(n=e.charAt(Mt),Mt++):(n=P,0===Vt&&i(Et)),Vt--,n===P?r=void 0:(Mt=r,r=P),r!==P?(e.length>Mt?(n=e.charAt(Mt),Mt++):(n=P,0===Vt&&i(bt)),n!==P?(Nt=t,r=Ft(n),t=r):(Mt=t,t=P)):(Mt=t,t=P),t===P&&(t=Mt,92===e.charCodeAt(Mt)?(r=Ut,Mt++):(r=P,0===Vt&&i(jt)),r!==P?(n=C(),n!==P?(Nt=t,r=Ft(n),t=r):(Mt=t,t=P)):(Mt=t,t=P)),t}function m(){var t,r,n;return t=Mt,r=Mt,Vt++,Tt.test(e.charAt(Mt))?(n=e.charAt(Mt),Mt++):(n=P,0===Vt&&i(_t)),Vt--,n===P?r=void 0:(Mt=r,r=P),r!==P?(e.length>Mt?(n=e.charAt(Mt),Mt++):(n=P,0===Vt&&i(bt)),n!==P?(Nt=t,r=Ft(n),t=r):(Mt=t,t=P)):(Mt=t,t=P),t===P&&(t=Mt,92===e.charCodeAt(Mt)?(r=Ut,Mt++):(r=P,0===Vt&&i(jt)),r!==P?(n=C(),n!==P?(Nt=t,r=Ft(n),t=r):(Mt=t,t=P)):(Mt=t,t=P)),t}function q(){var t,r,n;return t=Mt,r=Mt,Vt++,n=c(),Vt--,n===P?r=void 0:(Mt=r,r=P),r!==P?(e.length>Mt?(n=e.charAt(Mt),Mt++):(n=P,0===Vt&&i(bt)),n!==P?(Nt=t,r=Ft(n),t=r):(Mt=t,t=P)):(Mt=t,t=P),t}function C(){var t,r;return St.test(e.charAt(Mt))?(t=e.charAt(Mt),Mt++):(t=P,0===Vt&&i(kt)),t===P&&(t=Mt,110===e.charCodeAt(Mt)?(r=Bt,Mt++):(r=P,0===Vt&&i(It)),r!==P&&(Nt=t,r=zt()),t=r,t===P&&(t=Mt,114===e.charCodeAt(Mt)?(r=Dt,Mt++):(r=P,0===Vt&&i(Gt)),r!==P&&(Nt=t,r=Ht()),t=r,t===P&&(t=Mt,116===e.charCodeAt(Mt)?(r=Jt,Mt++):(r=P,0===Vt&&i(Kt)),r!==P&&(Nt=t,r=Lt()),t=r))),t}function w(e,t){function r(){return e.apply(this,arguments)||t.apply(this,arguments)}return r.desc=e.desc+" or "+t.desc,r}function E(e,t){function r(){return e.apply(this,arguments)&&t.apply(this,arguments)}return r.desc=e.desc+" and "+t.desc,r}function b(e){function t(){return!e.apply(this,arguments)}return t.desc="not "+e.desc,t}function F(e){function t(){return e.apply(this,arguments)}return t.desc="("+e.desc+")",t}function U(e){return!0}function j(e){return!1}function T(e){if(e.response)for(var t=Wt.ResponseUtils.getContentType(e.response),r=Xt.length;r--;)if(Xt[r].test(t))return!0;return!1}function _(e){function t(t){return t.response&&t.response.status_code===e}return t.desc="resp. code is "+e,t}function S(e){function t(t){return t.request&&e.test(t.request.host)}return e=new RegExp(e,"i"),t.desc="domain matches "+e,t}function k(e){return!!e.error}function B(e){function t(t){return t.request&&Wt.RequestUtils.match_header(t.request,e)||t.response&&Wt.ResponseUtils.match_header(t.response,e)}return e=new RegExp(e,"i"),t.desc="header matches "+e,t}function I(e){function t(t){return t.request&&Wt.RequestUtils.match_header(t.request,e)}return e=new RegExp(e,"i"),t.desc="req. header matches "+e,t}function z(e){function t(t){return t.response&&Wt.ResponseUtils.match_header(t.response,e)}return e=new RegExp(e,"i"),t.desc="resp. header matches "+e,t}function D(e){function t(t){return t.request&&e.test(t.request.method)}return e=new RegExp(e,"i"),t.desc="method matches "+e,t}function G(e){return e.request&&!e.response}function H(e){return!!e.response}function J(e){function t(t){return t.request&&e.test(Wt.RequestUtils.getContentType(t.request))||t.response&&e.test(Wt.ResponseUtils.getContentType(t.response))}return e=new RegExp(e,"i"),t.desc="content type matches "+e,t}function K(e){function t(t){return t.request&&e.test(Wt.RequestUtils.getContentType(t.request))}return e=new RegExp(e,"i"),t.desc="req. content type matches "+e,t}function L(e){function t(t){return t.response&&e.test(Wt.ResponseUtils.getContentType(t.response))}return e=new RegExp(e,"i"),t.desc="resp. content type matches "+e,t}function M(e){function t(t){return t.request&&e.test(Wt.RequestUtils.pretty_url(t.request))}return e=new RegExp(e,"i"),t.desc="url matches "+e,t}var N,O=arguments.length>1?arguments[1]:{},P={},Q={start:u},V=u,W={type:"other",description:"filter expression"},X=function(e){return e},Y={type:"other",description:"whitespace"},Z=/^[ \t\n\r]/,$={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},ee={type:"other",description:"control character"},te=/^[|&!()~"]/,re={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},ne={type:"other",description:"optional whitespace"},ie="|",se={type:"literal",value:"|",description:'"|"'},ue=function(e,t){return w(e,t)},ce="&",oe={type:"literal",value:"&",description:'"&"'},ae=function(e,t){return E(e,t)},le="!",pe={type:"literal",value:"!",description:'"!"'},fe=function(e){return b(e)},he="(",de={type:"literal",value:"(",description:'"("'},ve=")",ye={type:"literal",value:")",description:'")"'},ge=function(e){return F(e)},Ae="~a",xe={type:"literal",value:"~a",description:'"~a"'},Re=function(){return T},me="~e",qe={type:"literal",value:"~e",description:'"~e"'},Ce=function(){return k},we="~q",Ee={type:"literal",value:"~q",description:'"~q"'},be=function(){return G},Fe="~s",Ue={type:"literal",value:"~s",description:'"~s"'},je=function(){return H},Te="true",_e={type:"literal",value:"true",description:'"true"'},Se=function(){return U},ke="false",Be={type:"literal",value:"false",description:'"false"'},Ie=function(){return j},ze="~c",De={type:"literal",value:"~c",description:'"~c"'},Ge=function(e){return _(e)},He="~d",Je={type:"literal",value:"~d",description:'"~d"'},Ke=function(e){return S(e)},Le="~h",Me={type:"literal",value:"~h",description:'"~h"'},Ne=function(e){return B(e)},Oe="~hq",Pe={type:"literal",value:"~hq",description:'"~hq"'},Qe=function(e){return I(e)},Ve="~hs",We={type:"literal",value:"~hs",description:'"~hs"'},Xe=function(e){return z(e)},Ye="~m",Ze={type:"literal",value:"~m",description:'"~m"'},$e=function(e){return D(e)},et="~t",tt={type:"literal",value:"~t",description:'"~t"'},rt=function(e){return J(e)},nt="~tq",it={type:"literal",value:"~tq",description:'"~tq"'},st=function(e){return K(e)},ut="~ts",ct={type:"literal",value:"~ts",description:'"~ts"'},ot=function(e){return L(e)},at="~u",lt={type:"literal",value:"~u",description:'"~u"'},pt=function(e){return M(e)},ft={type:"other",description:"integer"},ht=/^['"]/,dt={type:"class",value:"['\"]",description:"['\"]"},vt=/^[0-9]/,yt={type:"class",value:"[0-9]",description:"[0-9]"},gt=function(e){return parseInt(e.join(""),10)},At={type:"other",description:"string"},xt='"',Rt={type:"literal",value:'"',description:'"\\""'},mt=function(e){return e.join("")},qt="'",Ct={type:"literal",value:"'",description:'"\'"'},wt=/^["\\]/,Et={type:"class",value:'["\\\\]',description:'["\\\\]'},bt={type:"any",description:"any character"},Ft=function(e){return e},Ut="\\",jt={type:"literal",value:"\\",description:'"\\\\"'},Tt=/^['\\]/,_t={type:"class",value:"['\\\\]",description:"['\\\\]"},St=/^['"\\]/,kt={type:"class",value:"['\"\\\\]",description:"['\"\\\\]"},Bt="n",It={type:"literal",value:"n",description:'"n"'},zt=function(){return"\n"},Dt="r",Gt={type:"literal",value:"r",description:'"r"'},Ht=function(){return"\r"},Jt="t",Kt={type:"literal",value:"t",description:'"t"'},Lt=function(){return" "},Mt=0,Nt=0,Ot=[{line:1,column:1,seenCR:!1}],Pt=0,Qt=[],Vt=0;if("startRule"in O){if(!(O.startRule in Q))throw new Error("Can't start parsing from rule \""+O.startRule+'".');V=Q[O.startRule]}var Wt=require("../flow/utils.js");U.desc="true",j.desc="false";var Xt=[new RegExp("text/javascript"),new RegExp("application/x-javascript"),new RegExp("application/javascript"),new RegExp("text/css"),new RegExp("image/.*"),new RegExp("application/x-shockwave-flash")];if(T.desc="is asset",k.desc="has error",G.desc="has no response",H.desc="has response",N=V(),N!==P&&Mt===e.length)return N;throw N!==P&&Mt<e.length&&i({type:"end",description:"end of input"}),s(null,Qt,Pt<e.length?e.charAt(Pt):null,Pt<e.length?n(Pt,Pt+1):n(Pt,Pt))}return e(t,Error),{SyntaxError:t,parse:r}}();
-},{"../flow/utils.js":40}],40:[function(require,module,exports){
+},{"../flow/utils.js":50}],50:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.parseHttpVersion=exports.isValidHttpVersion=exports.parseUrl=exports.ResponseUtils=exports.RequestUtils=exports.MessageUtils=void 0;var _lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_jquery=require("jquery"),_jquery2=_interopRequireDefault(_jquery),defaultPorts={http:80,https:443},MessageUtils=exports.MessageUtils={getContentType:function(e){var t=this.get_first_header(e,/^Content-Type$/i);return t?t.split(";")[0].trim():void 0},get_first_header:function(e,t){if(e._headerLookups||Object.defineProperty(e,"_headerLookups",{value:{},configurable:!1,enumerable:!1,writable:!1}),!(t in e._headerLookups)){for(var r,s=0;s<e.headers.length;s++)if(e.headers[s][0].match(t)){r=e.headers[s];break}e._headerLookups[t]=r?r[1]:void 0}return e._headerLookups[t]},match_header:function(e,t){for(var r=e.headers,s=r.length;s--;)if(t.test(r[s].join(" ")))return r[s];return!1},getContentURL:function(e,t){return t===e.request?t="request":t===e.response&&(t="response"),"/flows/"+e.id+"/"+t+"/content"},getContent:function(e,t){var r=MessageUtils.getContentURL(e,t);return _jquery2["default"].get(r)}},RequestUtils=exports.RequestUtils=_lodash2["default"].extend(MessageUtils,{pretty_host:function(e){return e.host},pretty_url:function(e){var t="";return defaultPorts[e.scheme]!==e.port&&(t=":"+e.port),e.scheme+"://"+this.pretty_host(e)+t+e.path}}),ResponseUtils=exports.ResponseUtils=_lodash2["default"].extend(MessageUtils,{}),parseUrl_regex=/^(?:(https?):\/\/)?([^\/:]+)?(?::(\d+))?(\/.*)?$/i,parseUrl=exports.parseUrl=function(e){var t=parseUrl_regex.exec(e);if(!t)return!1;var r=t[1],s=t[2],o=parseInt(t[3]),n=t[4];r&&(o=o||defaultPorts[r]);var i={};return r&&(i.scheme=r),s&&(i.host=s),o&&(i.port=o),n&&(i.path=n),i},isValidHttpVersion_regex=/^HTTP\/\d+(\.\d+)*$/i,isValidHttpVersion=exports.isValidHttpVersion=function(e){return isValidHttpVersion_regex.test(e)},parseHttpVersion=exports.parseHttpVersion=function(e){return e=e.replace("HTTP/","").split("."),_lodash2["default"].map(e,function(e){return parseInt(e)})};
-},{"jquery":"jquery","lodash":"lodash"}],41:[function(require,module,exports){
+},{"jquery":"jquery","lodash":"lodash"}],51:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function DictStore(){_events.EventEmitter.call(this),this.reset()}function LiveStoreMixin(e){this.type=e,this._updates_before_fetch=void 0,this._fetchxhr=!1,this.handle=this.handle.bind(this),_dispatcher.AppDispatcher.register(this.handle),window.ws&&window.ws.readyState===WebSocket.CONNECTING||this.fetch()}function LiveDictStore(e){DictStore.call(this),LiveStoreMixin.call(this,e)}function SettingsStore(){return new LiveDictStore(_actions.ActionTypes.SETTINGS_STORE)}Object.defineProperty(exports,"__esModule",{value:!0}),exports.SettingsStore=SettingsStore;var _lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_jquery=require("jquery"),_jquery2=_interopRequireDefault(_jquery),_events=require("events"),_actions=require("../actions.js"),_dispatcher=require("../dispatcher.js");_lodash2["default"].extend(DictStore.prototype,_events.EventEmitter.prototype,{update:function(e){_lodash2["default"].merge(this.dict,e),this.emit("recalculate")},reset:function(e){this.dict=e||{},this.emit("recalculate")}}),_lodash2["default"].extend(LiveStoreMixin.prototype,{handle:function(e){return e.type===_actions.ActionTypes.CONNECTION_OPEN?this.fetch():void(e.type===this.type&&(e.cmd===_actions.StoreCmds.RESET?this.fetch(e.data):this._updates_before_fetch?(console.log("defer update",e),this._updates_before_fetch.push(e)):this[e.cmd](e.data)))},close:function(){_dispatcher.AppDispatcher.unregister(this.handle)},fetch:function(e){console.log("fetch "+this.type),this._fetchxhr&&this._fetchxhr.abort(),this._updates_before_fetch=[],e?this.handle_fetch(e):this._fetchxhr=_jquery2["default"].getJSON("/"+this.type).done(function(e){this.handle_fetch(e.data)}.bind(this)).fail(function(){console.error("Could not fetch "+this.type)}.bind(this))},handle_fetch:function(e){this._fetchxhr=!1,console.log(this.type+" fetched.",this._updates_before_fetch),this.reset(e);var t=this._updates_before_fetch;this._updates_before_fetch=!1;for(var i=0;i<t.length;i++)this.handle(t[i])}}),_lodash2["default"].extend(LiveDictStore.prototype,DictStore.prototype,LiveStoreMixin.prototype);
-},{"../actions.js":2,"../dispatcher.js":31,"events":1,"jquery":"jquery","lodash":"lodash"}],42:[function(require,module,exports){
+},{"../actions.js":2,"../dispatcher.js":41,"events":1,"jquery":"jquery","lodash":"lodash"}],52:[function(require,module,exports){
"use strict";function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function reverseString(e){return String.fromCharCode.apply(String,_lodash2["default"].map(e.split(""),function(e){return 65535-e.charCodeAt(0)}))+end}function getCookie(e){var r=document.cookie.match(new RegExp("\\b"+e+"=([^;]*)\\b"));return r?r[1]:void 0}function fetchApi(e,r){return e+=-1===e.indexOf("?")?"?"+xsrf:"&"+xsrf,fetch(e,_extends({},r,{credentials:"same-origin"}))}Object.defineProperty(exports,"__esModule",{value:!0}),exports.formatTimeStamp=exports.formatTimeDelta=exports.formatSize=exports.Key=void 0;var _extends=Object.assign||function(e){for(var r=1;r<arguments.length;r++){var t=arguments[r];for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o])}return e};exports.reverseString=reverseString,exports.fetchApi=fetchApi;var _jquery=require("jquery"),_jquery2=_interopRequireDefault(_jquery),_lodash=require("lodash"),_lodash2=_interopRequireDefault(_lodash),_actions=require("./actions.js"),_actions2=_interopRequireDefault(_actions);window.$=_jquery2["default"],window._=_lodash2["default"],window.React=require("react");for(var Key=exports.Key={UP:38,DOWN:40,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,LEFT:37,RIGHT:39,ENTER:13,ESC:27,TAB:9,SPACE:32,BACKSPACE:8,SHIFT:16},i=65;90>=i;i++)Key[String.fromCharCode(i)]=i;var formatSize=exports.formatSize=function(e){if(0===e)return"0";for(var r=["b","kb","mb","gb","tb"],t=0;t<r.length&&!(Math.pow(1024,t+1)>e);t++);var o;return o=e%Math.pow(1024,t)===0?0:1,(e/Math.pow(1024,t)).toFixed(o)+r[t]},formatTimeDelta=exports.formatTimeDelta=function(e){for(var r=e,t=["ms","s","min","h"],o=[1e3,60,60],a=0;Math.abs(r)>=o[a]&&a<o.length;)r/=o[a],a++;return Math.round(r)+t[a]},formatTimeStamp=exports.formatTimeStamp=function(e){var r=new Date(1e3*e).toISOString();return r.replace("T"," ").replace("Z","")},end=String.fromCharCode(65535),xsrf="_xsrf="+getCookie("_xsrf");_jquery2["default"].ajaxPrefilter(function(e){["post","put","delete"].indexOf(e.type.toLowerCase())>=0&&"/"===e.url[0]&&(-1===e.url.indexOf("?")?e.url+="?"+xsrf:e.url+="&"+xsrf)}),(0,_jquery2["default"])(document).ajaxError(function(e,r,t,o){if("abort"!==o){var a=r.responseText;console.error(o,a,arguments),alert(a)}});
},{"./actions.js":2,"jquery":"jquery","lodash":"lodash","react":"react"}]},{},[3])
diff --git a/netlib/http/http2/__init__.py b/netlib/http/http2/__init__.py
index 633e6a20..6a979a0d 100644
--- a/netlib/http/http2/__init__.py
+++ b/netlib/http/http2/__init__.py
@@ -1,8 +1,6 @@
from __future__ import absolute_import, print_function, division
-from .connections import HTTP2Protocol
from netlib.http.http2 import framereader
__all__ = [
- "HTTP2Protocol",
"framereader",
]
diff --git a/netlib/http/http2/connections.py b/netlib/http/http2/connections.py
deleted file mode 100644
index 8f246feb..00000000
--- a/netlib/http/http2/connections.py
+++ /dev/null
@@ -1,432 +0,0 @@
-from __future__ import (absolute_import, print_function, division)
-import itertools
-import time
-
-import hyperframe.frame
-
-from hpack.hpack import Encoder, Decoder
-from netlib import utils, strutils
-from netlib.http import url
-import netlib.http.headers
-import netlib.http.response
-import netlib.http.request
-from netlib.http.http2 import framereader
-
-
-class TCPHandler(object):
-
- def __init__(self, rfile, wfile=None):
- self.rfile = rfile
- self.wfile = wfile
-
-
-class HTTP2Protocol(object):
-
- ERROR_CODES = utils.BiDi(
- NO_ERROR=0x0,
- PROTOCOL_ERROR=0x1,
- INTERNAL_ERROR=0x2,
- FLOW_CONTROL_ERROR=0x3,
- SETTINGS_TIMEOUT=0x4,
- STREAM_CLOSED=0x5,
- FRAME_SIZE_ERROR=0x6,
- REFUSED_STREAM=0x7,
- CANCEL=0x8,
- COMPRESSION_ERROR=0x9,
- CONNECT_ERROR=0xa,
- ENHANCE_YOUR_CALM=0xb,
- INADEQUATE_SECURITY=0xc,
- HTTP_1_1_REQUIRED=0xd
- )
-
- CLIENT_CONNECTION_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
-
- HTTP2_DEFAULT_SETTINGS = {
- hyperframe.frame.SettingsFrame.HEADER_TABLE_SIZE: 4096,
- hyperframe.frame.SettingsFrame.ENABLE_PUSH: 1,
- hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: None,
- hyperframe.frame.SettingsFrame.INITIAL_WINDOW_SIZE: 2 ** 16 - 1,
- hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE: 2 ** 14,
- hyperframe.frame.SettingsFrame.MAX_HEADER_LIST_SIZE: None,
- }
-
- def __init__(
- self,
- tcp_handler=None,
- rfile=None,
- wfile=None,
- is_server=False,
- dump_frames=False,
- encoder=None,
- decoder=None,
- unhandled_frame_cb=None,
- ):
- self.tcp_handler = tcp_handler or TCPHandler(rfile, wfile)
- self.is_server = is_server
- self.dump_frames = dump_frames
- self.encoder = encoder or Encoder()
- self.decoder = decoder or Decoder()
- self.unhandled_frame_cb = unhandled_frame_cb
-
- self.http2_settings = self.HTTP2_DEFAULT_SETTINGS.copy()
- self.current_stream_id = None
- self.connection_preface_performed = False
-
- def read_request(
- self,
- __rfile,
- include_body=True,
- body_size_limit=None,
- allow_empty=False,
- ):
- if body_size_limit is not None:
- raise NotImplementedError()
-
- self.perform_connection_preface()
-
- timestamp_start = time.time()
- if hasattr(self.tcp_handler.rfile, "reset_timestamps"):
- self.tcp_handler.rfile.reset_timestamps()
-
- stream_id, headers, body = self._receive_transmission(
- include_body=include_body,
- )
-
- if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"):
- # more accurate timestamp_start
- timestamp_start = self.tcp_handler.rfile.first_byte_timestamp
-
- timestamp_end = time.time()
-
- authority = headers.get(':authority', b'')
- method = headers.get(':method', 'GET')
- scheme = headers.get(':scheme', 'https')
- path = headers.get(':path', '/')
-
- headers.clear(":method")
- headers.clear(":scheme")
- headers.clear(":path")
-
- host = None
- port = None
-
- if path == '*' or path.startswith("/"):
- first_line_format = "relative"
- elif method == 'CONNECT':
- first_line_format = "authority"
- if ":" in authority:
- host, port = authority.split(":", 1)
- else:
- host = authority
- else:
- first_line_format = "absolute"
- # FIXME: verify if path or :host contains what we need
- scheme, host, port, _ = url.parse(path)
- scheme = scheme.decode('ascii')
- host = host.decode('ascii')
-
- if host is None:
- host = 'localhost'
- if port is None:
- port = 80 if scheme == 'http' else 443
- port = int(port)
-
- request = netlib.http.request.Request(
- first_line_format,
- method.encode('ascii'),
- scheme.encode('ascii'),
- host.encode('ascii'),
- port,
- path.encode('ascii'),
- b"HTTP/2.0",
- headers,
- body,
- timestamp_start,
- timestamp_end,
- )
- request.stream_id = stream_id
-
- return request
-
- def read_response(
- self,
- __rfile,
- request_method=b'',
- body_size_limit=None,
- include_body=True,
- stream_id=None,
- ):
- if body_size_limit is not None:
- raise NotImplementedError()
-
- self.perform_connection_preface()
-
- timestamp_start = time.time()
- if hasattr(self.tcp_handler.rfile, "reset_timestamps"):
- self.tcp_handler.rfile.reset_timestamps()
-
- stream_id, headers, body = self._receive_transmission(
- stream_id=stream_id,
- include_body=include_body,
- )
-
- if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"):
- # more accurate timestamp_start
- timestamp_start = self.tcp_handler.rfile.first_byte_timestamp
-
- if include_body:
- timestamp_end = time.time()
- else:
- timestamp_end = None
-
- response = netlib.http.response.Response(
- b"HTTP/2.0",
- int(headers.get(':status', 502)),
- b'',
- headers,
- body,
- timestamp_start=timestamp_start,
- timestamp_end=timestamp_end,
- )
- response.stream_id = stream_id
-
- return response
-
- def assemble(self, message):
- if isinstance(message, netlib.http.request.Request):
- return self.assemble_request(message)
- elif isinstance(message, netlib.http.response.Response):
- return self.assemble_response(message)
- else:
- raise ValueError("HTTP message not supported.")
-
- def assemble_request(self, request):
- assert isinstance(request, netlib.http.request.Request)
-
- authority = self.tcp_handler.sni if self.tcp_handler.sni else self.tcp_handler.address.host
- if self.tcp_handler.address.port != 443:
- authority += ":%d" % self.tcp_handler.address.port
-
- headers = request.headers.copy()
-
- if ':authority' not in headers:
- headers.insert(0, b':authority', authority.encode('ascii'))
- headers.insert(0, b':scheme', request.scheme.encode('ascii'))
- headers.insert(0, b':path', request.path.encode('ascii'))
- headers.insert(0, b':method', request.method.encode('ascii'))
-
- if hasattr(request, 'stream_id'):
- stream_id = request.stream_id
- else:
- stream_id = self._next_stream_id()
-
- return list(itertools.chain(
- self._create_headers(headers, stream_id, end_stream=(request.body is None or len(request.body) == 0)),
- self._create_body(request.body, stream_id)))
-
- def assemble_response(self, response):
- assert isinstance(response, netlib.http.response.Response)
-
- headers = response.headers.copy()
-
- if ':status' not in headers:
- headers.insert(0, b':status', strutils.always_bytes(response.status_code))
-
- if hasattr(response, 'stream_id'):
- stream_id = response.stream_id
- else:
- stream_id = self._next_stream_id()
-
- return list(itertools.chain(
- self._create_headers(headers, stream_id, end_stream=(response.body is None or len(response.body) == 0)),
- self._create_body(response.body, stream_id),
- ))
-
- def perform_connection_preface(self, force=False):
- if force or not self.connection_preface_performed:
- if self.is_server:
- self.perform_server_connection_preface(force)
- else:
- self.perform_client_connection_preface(force)
-
- def perform_server_connection_preface(self, force=False):
- if force or not self.connection_preface_performed:
- self.connection_preface_performed = True
-
- magic_length = len(self.CLIENT_CONNECTION_PREFACE)
- magic = self.tcp_handler.rfile.safe_read(magic_length)
- assert magic == self.CLIENT_CONNECTION_PREFACE
-
- frm = hyperframe.frame.SettingsFrame(settings={
- hyperframe.frame.SettingsFrame.ENABLE_PUSH: 0,
- hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: 1,
- })
- self.send_frame(frm, hide=True)
- self._receive_settings(hide=True)
-
- def perform_client_connection_preface(self, force=False):
- if force or not self.connection_preface_performed:
- self.connection_preface_performed = True
-
- self.tcp_handler.wfile.write(self.CLIENT_CONNECTION_PREFACE)
-
- self.send_frame(hyperframe.frame.SettingsFrame(), hide=True)
- self._receive_settings(hide=True) # server announces own settings
- self._receive_settings(hide=True) # server acks my settings
-
- def send_frame(self, frm, hide=False):
- raw_bytes = frm.serialize()
- self.tcp_handler.wfile.write(raw_bytes)
- self.tcp_handler.wfile.flush()
- if not hide and self.dump_frames: # pragma no cover
- print(frm.human_readable(">>"))
-
- def read_frame(self, hide=False):
- while True:
- frm = framereader.http2_read_frame(self.tcp_handler.rfile)
- if not hide and self.dump_frames: # pragma no cover
- print(frm.human_readable("<<"))
-
- if isinstance(frm, hyperframe.frame.PingFrame):
- raw_bytes = hyperframe.frame.PingFrame(flags=['ACK'], payload=frm.payload).serialize()
- self.tcp_handler.wfile.write(raw_bytes)
- self.tcp_handler.wfile.flush()
- continue
- if isinstance(frm, hyperframe.frame.SettingsFrame) and 'ACK' not in frm.flags:
- self._apply_settings(frm.settings, hide)
- if isinstance(frm, hyperframe.frame.DataFrame) and frm.flow_controlled_length > 0:
- self._update_flow_control_window(frm.stream_id, frm.flow_controlled_length)
- return frm
-
- def check_alpn(self):
- alp = self.tcp_handler.get_alpn_proto_negotiated()
- if alp != b'h2':
- raise NotImplementedError(
- "HTTP2Protocol can not handle unknown ALP: %s" % alp)
- return True
-
- def _handle_unexpected_frame(self, frm):
- if isinstance(frm, hyperframe.frame.SettingsFrame):
- return
- if self.unhandled_frame_cb:
- self.unhandled_frame_cb(frm)
-
- def _receive_settings(self, hide=False):
- while True:
- frm = self.read_frame(hide)
- if isinstance(frm, hyperframe.frame.SettingsFrame):
- break
- else:
- self._handle_unexpected_frame(frm)
-
- def _next_stream_id(self):
- if self.current_stream_id is None:
- if self.is_server:
- # servers must use even stream ids
- self.current_stream_id = 2
- else:
- # clients must use odd stream ids
- self.current_stream_id = 1
- else:
- self.current_stream_id += 2
- return self.current_stream_id
-
- def _apply_settings(self, settings, hide=False):
- for setting, value in settings.items():
- old_value = self.http2_settings[setting]
- if not old_value:
- old_value = '-'
- self.http2_settings[setting] = value
-
- frm = hyperframe.frame.SettingsFrame(flags=['ACK'])
- self.send_frame(frm, hide)
-
- def _update_flow_control_window(self, stream_id, increment):
- frm = hyperframe.frame.WindowUpdateFrame(stream_id=0, window_increment=increment)
- self.send_frame(frm)
- frm = hyperframe.frame.WindowUpdateFrame(stream_id=stream_id, window_increment=increment)
- self.send_frame(frm)
-
- def _create_headers(self, headers, stream_id, end_stream=True):
- def frame_cls(chunks):
- for i in chunks:
- if i == 0:
- yield hyperframe.frame.HeadersFrame, i
- else:
- yield hyperframe.frame.ContinuationFrame, i
-
- header_block_fragment = self.encoder.encode(headers.fields)
-
- chunk_size = self.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE]
- chunks = range(0, len(header_block_fragment), chunk_size)
- frms = [frm_cls(
- flags=[],
- stream_id=stream_id,
- data=header_block_fragment[i:i + chunk_size]) for frm_cls, i in frame_cls(chunks)]
-
- frms[-1].flags.add('END_HEADERS')
- if end_stream:
- frms[0].flags.add('END_STREAM')
-
- if self.dump_frames: # pragma no cover
- for frm in frms:
- print(frm.human_readable(">>"))
-
- return [frm.serialize() for frm in frms]
-
- def _create_body(self, body, stream_id):
- if body is None or len(body) == 0:
- return b''
-
- chunk_size = self.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE]
- chunks = range(0, len(body), chunk_size)
- frms = [hyperframe.frame.DataFrame(
- flags=[],
- stream_id=stream_id,
- data=body[i:i + chunk_size]) for i in chunks]
- frms[-1].flags.add('END_STREAM')
-
- if self.dump_frames: # pragma no cover
- for frm in frms:
- print(frm.human_readable(">>"))
-
- return [frm.serialize() for frm in frms]
-
- def _receive_transmission(self, stream_id=None, include_body=True):
- if not include_body:
- raise NotImplementedError()
-
- body_expected = True
-
- header_blocks = b''
- body = b''
-
- while True:
- frm = self.read_frame()
- if (
- (isinstance(frm, hyperframe.frame.HeadersFrame) or isinstance(frm, hyperframe.frame.ContinuationFrame)) and
- (stream_id is None or frm.stream_id == stream_id)
- ):
- stream_id = frm.stream_id
- header_blocks += frm.data
- if 'END_STREAM' in frm.flags:
- body_expected = False
- if 'END_HEADERS' in frm.flags:
- break
- else:
- self._handle_unexpected_frame(frm)
-
- while body_expected:
- frm = self.read_frame()
- if isinstance(frm, hyperframe.frame.DataFrame) and frm.stream_id == stream_id:
- body += frm.data
- if 'END_STREAM' in frm.flags:
- break
- else:
- self._handle_unexpected_frame(frm)
-
- headers = netlib.http.headers.Headers(
- (k.encode('ascii'), v.encode('ascii')) for k, v in self.decoder.decode(header_blocks)
- )
-
- return stream_id, headers, body
diff --git a/pathod/pathoc.py b/pathod/pathoc.py
index 478ce2a2..c6783878 100644
--- a/pathod/pathoc.py
+++ b/pathod/pathoc.py
@@ -11,18 +11,18 @@ import time
import OpenSSL.crypto
import six
+import logging
+from netlib.tutils import treq
+from netlib import strutils
from netlib import tcp, certutils, websockets, socks
from netlib import exceptions
from netlib.http import http1
-from netlib.http import http2
from netlib import basethread
-from pathod import log, language
+from . import log, language
+from .protocols import http2
-import logging
-from netlib.tutils import treq
-from netlib import strutils
logging.getLogger("hpack").setLevel(logging.WARNING)
@@ -227,7 +227,7 @@ class Pathoc(tcp.TCPClient):
"Pathoc might not be working as expected without ALPN.",
timestamp=False
)
- self.protocol = http2.HTTP2Protocol(self, dump_frames=self.http2_framedump)
+ self.protocol = http2.HTTP2StateProtocol(self, dump_frames=self.http2_framedump)
else:
self.protocol = http1
@@ -241,8 +241,8 @@ class Pathoc(tcp.TCPClient):
def http_connect(self, connect_to):
self.wfile.write(
- 'CONNECT %s:%s HTTP/1.1\r\n' % tuple(connect_to) +
- '\r\n'
+ b'CONNECT %s:%d HTTP/1.1\r\n' % (connect_to[0].encode("idna"), connect_to[1]) +
+ b'\r\n'
)
self.wfile.flush()
try:
diff --git a/pathod/protocols/http.py b/pathod/protocols/http.py
index 7736df4b..2ede2591 100644
--- a/pathod/protocols/http.py
+++ b/pathod/protocols/http.py
@@ -17,15 +17,15 @@ class HTTPProtocol(object):
"""
self.pathod_handler.wfile.write(
- 'HTTP/1.1 200 Connection established\r\n' +
- ('Proxy-agent: %s\r\n' % version.PATHOD) +
- '\r\n'
+ b'HTTP/1.1 200 Connection established\r\n' +
+ (b'Proxy-agent: %s\r\n' % version.PATHOD.encode()) +
+ b'\r\n'
)
self.pathod_handler.wfile.flush()
if not self.pathod_handler.server.ssloptions.not_after_connect:
try:
cert, key, chain_file_ = self.pathod_handler.server.ssloptions.get_cert(
- connect[0]
+ connect[0].encode()
)
self.pathod_handler.convert_to_ssl(
cert,
diff --git a/pathod/protocols/http2.py b/pathod/protocols/http2.py
index 3f45ec80..c8728940 100644
--- a/pathod/protocols/http2.py
+++ b/pathod/protocols/http2.py
@@ -1,12 +1,445 @@
-from netlib.http import http2
+from __future__ import (absolute_import, print_function, division)
+
+import itertools
+import time
+
+import hyperframe.frame
+from hpack.hpack import Encoder, Decoder
+
+from netlib import utils, strutils
+from netlib.http import url
+from netlib.http.http2 import framereader
+import netlib.http.headers
+import netlib.http.response
+import netlib.http.request
+
from .. import language
-class HTTP2Protocol:
+class TCPHandler(object):
+
+ def __init__(self, rfile, wfile=None):
+ self.rfile = rfile
+ self.wfile = wfile
+
+
+class HTTP2StateProtocol(object):
+
+ ERROR_CODES = utils.BiDi(
+ NO_ERROR=0x0,
+ PROTOCOL_ERROR=0x1,
+ INTERNAL_ERROR=0x2,
+ FLOW_CONTROL_ERROR=0x3,
+ SETTINGS_TIMEOUT=0x4,
+ STREAM_CLOSED=0x5,
+ FRAME_SIZE_ERROR=0x6,
+ REFUSED_STREAM=0x7,
+ CANCEL=0x8,
+ COMPRESSION_ERROR=0x9,
+ CONNECT_ERROR=0xa,
+ ENHANCE_YOUR_CALM=0xb,
+ INADEQUATE_SECURITY=0xc,
+ HTTP_1_1_REQUIRED=0xd
+ )
+
+ CLIENT_CONNECTION_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
+
+ HTTP2_DEFAULT_SETTINGS = {
+ hyperframe.frame.SettingsFrame.HEADER_TABLE_SIZE: 4096,
+ hyperframe.frame.SettingsFrame.ENABLE_PUSH: 1,
+ hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: None,
+ hyperframe.frame.SettingsFrame.INITIAL_WINDOW_SIZE: 2 ** 16 - 1,
+ hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE: 2 ** 14,
+ hyperframe.frame.SettingsFrame.MAX_HEADER_LIST_SIZE: None,
+ }
+
+ def __init__(
+ self,
+ tcp_handler=None,
+ rfile=None,
+ wfile=None,
+ is_server=False,
+ dump_frames=False,
+ encoder=None,
+ decoder=None,
+ unhandled_frame_cb=None,
+ ):
+ self.tcp_handler = tcp_handler or TCPHandler(rfile, wfile)
+ self.is_server = is_server
+ self.dump_frames = dump_frames
+ self.encoder = encoder or Encoder()
+ self.decoder = decoder or Decoder()
+ self.unhandled_frame_cb = unhandled_frame_cb
+
+ self.http2_settings = self.HTTP2_DEFAULT_SETTINGS.copy()
+ self.current_stream_id = None
+ self.connection_preface_performed = False
+
+ def read_request(
+ self,
+ __rfile,
+ include_body=True,
+ body_size_limit=None,
+ allow_empty=False,
+ ):
+ if body_size_limit is not None:
+ raise NotImplementedError()
+
+ self.perform_connection_preface()
+
+ timestamp_start = time.time()
+ if hasattr(self.tcp_handler.rfile, "reset_timestamps"):
+ self.tcp_handler.rfile.reset_timestamps()
+
+ stream_id, headers, body = self._receive_transmission(
+ include_body=include_body,
+ )
+
+ if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"):
+ # more accurate timestamp_start
+ timestamp_start = self.tcp_handler.rfile.first_byte_timestamp
+
+ timestamp_end = time.time()
+
+ authority = headers.get(':authority', b'')
+ method = headers.get(':method', 'GET')
+ scheme = headers.get(':scheme', 'https')
+ path = headers.get(':path', '/')
+
+ headers.clear(":method")
+ headers.clear(":scheme")
+ headers.clear(":path")
+
+ host = None
+ port = None
+
+ if path == '*' or path.startswith("/"):
+ first_line_format = "relative"
+ elif method == 'CONNECT':
+ first_line_format = "authority"
+ if ":" in authority:
+ host, port = authority.split(":", 1)
+ else:
+ host = authority
+ else:
+ first_line_format = "absolute"
+ # FIXME: verify if path or :host contains what we need
+ scheme, host, port, _ = url.parse(path)
+ scheme = scheme.decode('ascii')
+ host = host.decode('ascii')
+
+ if host is None:
+ host = 'localhost'
+ if port is None:
+ port = 80 if scheme == 'http' else 443
+ port = int(port)
+
+ request = netlib.http.request.Request(
+ first_line_format,
+ method.encode('ascii'),
+ scheme.encode('ascii'),
+ host.encode('ascii'),
+ port,
+ path.encode('ascii'),
+ b"HTTP/2.0",
+ headers,
+ body,
+ timestamp_start,
+ timestamp_end,
+ )
+ request.stream_id = stream_id
+
+ return request
+
+ def read_response(
+ self,
+ __rfile,
+ request_method=b'',
+ body_size_limit=None,
+ include_body=True,
+ stream_id=None,
+ ):
+ if body_size_limit is not None:
+ raise NotImplementedError()
+
+ self.perform_connection_preface()
+
+ timestamp_start = time.time()
+ if hasattr(self.tcp_handler.rfile, "reset_timestamps"):
+ self.tcp_handler.rfile.reset_timestamps()
+
+ stream_id, headers, body = self._receive_transmission(
+ stream_id=stream_id,
+ include_body=include_body,
+ )
+
+ if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"):
+ # more accurate timestamp_start
+ timestamp_start = self.tcp_handler.rfile.first_byte_timestamp
+
+ if include_body:
+ timestamp_end = time.time()
+ else:
+ timestamp_end = None
+
+ response = netlib.http.response.Response(
+ b"HTTP/2.0",
+ int(headers.get(':status', 502)),
+ b'',
+ headers,
+ body,
+ timestamp_start=timestamp_start,
+ timestamp_end=timestamp_end,
+ )
+ response.stream_id = stream_id
+
+ return response
+
+ def assemble(self, message):
+ if isinstance(message, netlib.http.request.Request):
+ return self.assemble_request(message)
+ elif isinstance(message, netlib.http.response.Response):
+ return self.assemble_response(message)
+ else:
+ raise ValueError("HTTP message not supported.")
+
+ def assemble_request(self, request):
+ assert isinstance(request, netlib.http.request.Request)
+
+ authority = self.tcp_handler.sni if self.tcp_handler.sni else self.tcp_handler.address.host
+ if self.tcp_handler.address.port != 443:
+ authority += ":%d" % self.tcp_handler.address.port
+
+ headers = request.headers.copy()
+
+ if ':authority' not in headers:
+ headers.insert(0, b':authority', authority.encode('ascii'))
+ headers.insert(0, b':scheme', request.scheme.encode('ascii'))
+ headers.insert(0, b':path', request.path.encode('ascii'))
+ headers.insert(0, b':method', request.method.encode('ascii'))
+
+ if hasattr(request, 'stream_id'):
+ stream_id = request.stream_id
+ else:
+ stream_id = self._next_stream_id()
+
+ return list(itertools.chain(
+ self._create_headers(headers, stream_id, end_stream=(request.body is None or len(request.body) == 0)),
+ self._create_body(request.body, stream_id)))
+
+ def assemble_response(self, response):
+ assert isinstance(response, netlib.http.response.Response)
+
+ headers = response.headers.copy()
+
+ if ':status' not in headers:
+ headers.insert(0, b':status', strutils.always_bytes(response.status_code))
+
+ if hasattr(response, 'stream_id'):
+ stream_id = response.stream_id
+ else:
+ stream_id = self._next_stream_id()
+
+ return list(itertools.chain(
+ self._create_headers(headers, stream_id, end_stream=(response.body is None or len(response.body) == 0)),
+ self._create_body(response.body, stream_id),
+ ))
+
+ def perform_connection_preface(self, force=False):
+ if force or not self.connection_preface_performed:
+ if self.is_server:
+ self.perform_server_connection_preface(force)
+ else:
+ self.perform_client_connection_preface(force)
+
+ def perform_server_connection_preface(self, force=False):
+ if force or not self.connection_preface_performed:
+ self.connection_preface_performed = True
+
+ magic_length = len(self.CLIENT_CONNECTION_PREFACE)
+ magic = self.tcp_handler.rfile.safe_read(magic_length)
+ assert magic == self.CLIENT_CONNECTION_PREFACE
+
+ frm = hyperframe.frame.SettingsFrame(settings={
+ hyperframe.frame.SettingsFrame.ENABLE_PUSH: 0,
+ hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: 1,
+ })
+ self.send_frame(frm, hide=True)
+ self._receive_settings(hide=True)
+
+ def perform_client_connection_preface(self, force=False):
+ if force or not self.connection_preface_performed:
+ self.connection_preface_performed = True
+
+ self.tcp_handler.wfile.write(self.CLIENT_CONNECTION_PREFACE)
+
+ self.send_frame(hyperframe.frame.SettingsFrame(), hide=True)
+ self._receive_settings(hide=True) # server announces own settings
+ self._receive_settings(hide=True) # server acks my settings
+
+ def send_frame(self, frm, hide=False):
+ raw_bytes = frm.serialize()
+ self.tcp_handler.wfile.write(raw_bytes)
+ self.tcp_handler.wfile.flush()
+ if not hide and self.dump_frames: # pragma no cover
+ print(frm.human_readable(">>"))
+
+ def read_frame(self, hide=False):
+ while True:
+ frm = framereader.http2_read_frame(self.tcp_handler.rfile)
+ if not hide and self.dump_frames: # pragma no cover
+ print(frm.human_readable("<<"))
+
+ if isinstance(frm, hyperframe.frame.PingFrame):
+ raw_bytes = hyperframe.frame.PingFrame(flags=['ACK'], payload=frm.payload).serialize()
+ self.tcp_handler.wfile.write(raw_bytes)
+ self.tcp_handler.wfile.flush()
+ continue
+ if isinstance(frm, hyperframe.frame.SettingsFrame) and 'ACK' not in frm.flags:
+ self._apply_settings(frm.settings, hide)
+ if isinstance(frm, hyperframe.frame.DataFrame) and frm.flow_controlled_length > 0:
+ self._update_flow_control_window(frm.stream_id, frm.flow_controlled_length)
+ return frm
+
+ def check_alpn(self):
+ alp = self.tcp_handler.get_alpn_proto_negotiated()
+ if alp != b'h2':
+ raise NotImplementedError(
+ "HTTP2Protocol can not handle unknown ALPN value: %s" % alp)
+ return True
+
+ def _handle_unexpected_frame(self, frm):
+ if isinstance(frm, hyperframe.frame.SettingsFrame):
+ return
+ if self.unhandled_frame_cb:
+ self.unhandled_frame_cb(frm)
+
+ def _receive_settings(self, hide=False):
+ while True:
+ frm = self.read_frame(hide)
+ if isinstance(frm, hyperframe.frame.SettingsFrame):
+ break
+ else:
+ self._handle_unexpected_frame(frm)
+
+ def _next_stream_id(self):
+ if self.current_stream_id is None:
+ if self.is_server:
+ # servers must use even stream ids
+ self.current_stream_id = 2
+ else:
+ # clients must use odd stream ids
+ self.current_stream_id = 1
+ else:
+ self.current_stream_id += 2
+ return self.current_stream_id
+
+ def _apply_settings(self, settings, hide=False):
+ for setting, value in settings.items():
+ old_value = self.http2_settings[setting]
+ if not old_value:
+ old_value = '-'
+ self.http2_settings[setting] = value
+
+ frm = hyperframe.frame.SettingsFrame(flags=['ACK'])
+ self.send_frame(frm, hide)
+
+ def _update_flow_control_window(self, stream_id, increment):
+ frm = hyperframe.frame.WindowUpdateFrame(stream_id=0, window_increment=increment)
+ self.send_frame(frm)
+ frm = hyperframe.frame.WindowUpdateFrame(stream_id=stream_id, window_increment=increment)
+ self.send_frame(frm)
+
+ def _create_headers(self, headers, stream_id, end_stream=True):
+ def frame_cls(chunks):
+ for i in chunks:
+ if i == 0:
+ yield hyperframe.frame.HeadersFrame, i
+ else:
+ yield hyperframe.frame.ContinuationFrame, i
+
+ header_block_fragment = self.encoder.encode(headers.fields)
+
+ chunk_size = self.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE]
+ chunks = range(0, len(header_block_fragment), chunk_size)
+ frms = [frm_cls(
+ flags=[],
+ stream_id=stream_id,
+ data=header_block_fragment[i:i + chunk_size]) for frm_cls, i in frame_cls(chunks)]
+
+ frms[-1].flags.add('END_HEADERS')
+ if end_stream:
+ frms[0].flags.add('END_STREAM')
+
+ if self.dump_frames: # pragma no cover
+ for frm in frms:
+ print(frm.human_readable(">>"))
+
+ return [frm.serialize() for frm in frms]
+
+ def _create_body(self, body, stream_id):
+ if body is None or len(body) == 0:
+ return b''
+
+ chunk_size = self.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE]
+ chunks = range(0, len(body), chunk_size)
+ frms = [hyperframe.frame.DataFrame(
+ flags=[],
+ stream_id=stream_id,
+ data=body[i:i + chunk_size]) for i in chunks]
+ frms[-1].flags.add('END_STREAM')
+
+ if self.dump_frames: # pragma no cover
+ for frm in frms:
+ print(frm.human_readable(">>"))
+
+ return [frm.serialize() for frm in frms]
+
+ def _receive_transmission(self, stream_id=None, include_body=True):
+ if not include_body:
+ raise NotImplementedError()
+
+ body_expected = True
+
+ header_blocks = b''
+ body = b''
+
+ while True:
+ frm = self.read_frame()
+ if (
+ (isinstance(frm, hyperframe.frame.HeadersFrame) or isinstance(frm, hyperframe.frame.ContinuationFrame)) and
+ (stream_id is None or frm.stream_id == stream_id)
+ ):
+ stream_id = frm.stream_id
+ header_blocks += frm.data
+ if 'END_STREAM' in frm.flags:
+ body_expected = False
+ if 'END_HEADERS' in frm.flags:
+ break
+ else:
+ self._handle_unexpected_frame(frm)
+
+ while body_expected:
+ frm = self.read_frame()
+ if isinstance(frm, hyperframe.frame.DataFrame) and frm.stream_id == stream_id:
+ body += frm.data
+ if 'END_STREAM' in frm.flags:
+ break
+ else:
+ self._handle_unexpected_frame(frm)
+
+ headers = netlib.http.headers.Headers(
+ (k.encode('ascii'), v.encode('ascii')) for k, v in self.decoder.decode(header_blocks)
+ )
+
+ return stream_id, headers, body
+
+
+class HTTP2Protocol(object):
def __init__(self, pathod_handler):
self.pathod_handler = pathod_handler
- self.wire_protocol = http2.HTTP2Protocol(
+ self.wire_protocol = HTTP2StateProtocol(
self.pathod_handler, is_server=True, dump_frames=self.pathod_handler.http2_framedump
)
diff --git a/test/netlib/http/http2/test_framereader.py b/test/netlib/http/http2/test_framereader.py
new file mode 100644
index 00000000..41b73189
--- /dev/null
+++ b/test/netlib/http/http2/test_framereader.py
@@ -0,0 +1 @@
+# foobar
diff --git a/test/pathod/__init__.py b/test/pathod/__init__.py
new file mode 100644
index 00000000..3f5dc124
--- /dev/null
+++ b/test/pathod/__init__.py
@@ -0,0 +1 @@
+from __future__ import (print_function, absolute_import, division)
diff --git a/test/pathod/test_language_actions.py b/test/pathod/test_language_actions.py
index f12d8105..2b1b6915 100644
--- a/test/pathod/test_language_actions.py
+++ b/test/pathod/test_language_actions.py
@@ -1,11 +1,10 @@
from six import BytesIO
-from pathod.language import actions
-from pathod import language
+from pathod.language import actions, parse_pathoc, parse_pathod, serve
def parse_request(s):
- return next(language.parse_pathoc(s))
+ return next(parse_pathoc(s))
def test_unique_name():
@@ -16,9 +15,9 @@ def test_unique_name():
class TestDisconnects:
def test_parse_pathod(self):
- a = next(language.parse_pathod("400:d0")).actions[0]
+ a = next(parse_pathod("400:d0")).actions[0]
assert a.spec() == "d0"
- a = next(language.parse_pathod("400:dr")).actions[0]
+ a = next(parse_pathod("400:dr")).actions[0]
assert a.spec() == "dr"
def test_at(self):
@@ -42,12 +41,12 @@ class TestDisconnects:
class TestInject:
def test_parse_pathod(self):
- a = next(language.parse_pathod("400:ir,@100")).actions[0]
+ a = next(parse_pathod("400:ir,@100")).actions[0]
assert a.offset == "r"
assert a.value.datatype == "bytes"
assert a.value.usize == 100
- a = next(language.parse_pathod("400:ia,@100")).actions[0]
+ a = next(parse_pathod("400:ia,@100")).actions[0]
assert a.offset == "a"
def test_at(self):
@@ -62,8 +61,8 @@ class TestInject:
def test_serve(self):
s = BytesIO()
- r = next(language.parse_pathod("400:i0,'foo'"))
- assert language.serve(r, s, {})
+ r = next(parse_pathod("400:i0,'foo'"))
+ assert serve(r, s, {})
def test_spec(self):
e = actions.InjectAt.expr()
@@ -96,7 +95,7 @@ class TestPauses:
assert v.offset == "a"
def test_request(self):
- r = next(language.parse_pathod('400:p10,10'))
+ r = next(parse_pathod('400:p10,10'))
assert r.actions[0].spec() == "p10,10"
def test_spec(self):
diff --git a/test/pathod/test_language_base.py b/test/pathod/test_language_base.py
index 7c7d8cf9..12a235e4 100644
--- a/test/pathod/test_language_base.py
+++ b/test/pathod/test_language_base.py
@@ -1,7 +1,8 @@
import os
from pathod import language
from pathod.language import base, exceptions
-import tutils
+
+from . import tutils
def parse_request(s):
diff --git a/test/pathod/test_language_generators.py b/test/pathod/test_language_generators.py
index 51f55991..4ec6ec3f 100644
--- a/test/pathod/test_language_generators.py
+++ b/test/pathod/test_language_generators.py
@@ -1,7 +1,7 @@
import os
from pathod.language import generators
-import tutils
+from . import tutils
def test_randomgenerator():
diff --git a/test/pathod/test_language_http.py b/test/pathod/test_language_http.py
index 18059e3a..dd0b8d02 100644
--- a/test/pathod/test_language_http.py
+++ b/test/pathod/test_language_http.py
@@ -1,7 +1,8 @@
from six import BytesIO
from pathod import language
from pathod.language import http, base
-import tutils
+
+from . import tutils
def parse_request(s):
diff --git a/test/pathod/test_language_http2.py b/test/pathod/test_language_http2.py
index a2bffe63..f4b34047 100644
--- a/test/pathod/test_language_http2.py
+++ b/test/pathod/test_language_http2.py
@@ -1,12 +1,13 @@
from six import BytesIO
-import netlib
from netlib import tcp
from netlib.http import user_agents
from pathod import language
from pathod.language import http2
-import tutils
+from pathod.protocols.http2 import HTTP2StateProtocol
+
+from . import tutils
def parse_request(s):
@@ -20,7 +21,7 @@ def parse_response(s):
def default_settings():
return language.Settings(
request_host="foo.com",
- protocol=netlib.http.http2.HTTP2Protocol(tcp.TCPClient(('localhost', 1234)))
+ protocol=HTTP2StateProtocol(tcp.TCPClient(('localhost', 1234)))
)
diff --git a/test/pathod/test_language_websocket.py b/test/pathod/test_language_websocket.py
index 58297141..89cbb772 100644
--- a/test/pathod/test_language_websocket.py
+++ b/test/pathod/test_language_websocket.py
@@ -2,7 +2,8 @@
from pathod import language
from pathod.language import websockets
import netlib.websockets
-import tutils
+
+from . import tutils
def parse_request(s):
diff --git a/test/pathod/test_pathoc.py b/test/pathod/test_pathoc.py
index 05cf518d..28f9f0f8 100644
--- a/test/pathod/test_pathoc.py
+++ b/test/pathod/test_pathoc.py
@@ -5,11 +5,13 @@ from mock import Mock
from netlib import http
from netlib import tcp
from netlib.exceptions import NetlibException
-from netlib.http import http1, http2
+from netlib.http import http1
+from netlib.tutils import raises
from pathod import pathoc, language
-from netlib.tutils import raises
-import tutils
+from pathod.protocols.http2 import HTTP2StateProtocol
+
+from . import tutils
def test_response():
@@ -169,7 +171,7 @@ class TestDaemon(PathocTestDaemon):
def test_connect_fail(self):
to = ("foobar", 80)
c = pathoc.Pathoc(("127.0.0.1", self.d.port), fp=None)
- c.rfile, c.wfile = StringIO(), StringIO()
+ c.rfile, c.wfile = BytesIO(), BytesIO()
with raises("connect failed"):
c.http_connect(to)
c.rfile = BytesIO(
@@ -219,7 +221,7 @@ class TestDaemonHTTP2(PathocTestDaemon):
ssl=True,
use_http2=True,
)
- assert isinstance(c.protocol, http2.HTTP2Protocol)
+ assert isinstance(c.protocol, HTTP2StateProtocol)
c = pathoc.Pathoc(
("127.0.0.1", self.d.port),
diff --git a/test/pathod/test_pathoc_cmdline.py b/test/pathod/test_pathoc_cmdline.py
index 35909325..922cf3a9 100644
--- a/test/pathod/test_pathoc_cmdline.py
+++ b/test/pathod/test_pathoc_cmdline.py
@@ -1,8 +1,10 @@
-from pathod import pathoc_cmdline as cmdline
-import tutils
from six.moves import cStringIO as StringIO
import mock
+from pathod import pathoc_cmdline as cmdline
+
+from . import tutils
+
@mock.patch("argparse.ArgumentParser.error")
def test_pathoc(perror):
diff --git a/test/pathod/test_pathod.py b/test/pathod/test_pathod.py
index ed4ef49f..0b34f924 100644
--- a/test/pathod/test_pathod.py
+++ b/test/pathod/test_pathod.py
@@ -3,7 +3,8 @@ from six.moves import cStringIO as StringIO
from pathod import pathod
from netlib import tcp
from netlib.exceptions import HttpException, TlsException
-import tutils
+
+from . import tutils
class TestPathod(object):
@@ -52,7 +53,7 @@ class TestNotAfterConnect(tutils.DaemonTests):
class TestCustomCert(tutils.DaemonTests):
ssl = True
ssloptions = dict(
- certs=[("*", tutils.test_data.path("data/testkey.pem"))],
+ certs=[(b"*", tutils.test_data.path("data/testkey.pem"))],
)
def test_connect(self):
@@ -66,7 +67,7 @@ class TestCustomCert(tutils.DaemonTests):
class TestSSLCN(tutils.DaemonTests):
ssl = True
ssloptions = dict(
- cn="foo.com"
+ cn=b"foo.com"
)
def test_connect(self):
@@ -100,7 +101,7 @@ class TestNocraft(tutils.DaemonTests):
def test_nocraft(self):
r = self.get(r"200:b'\xf0'")
assert r.status_code == 800
- assert "Crafting disabled" in r.content
+ assert b"Crafting disabled" in r.content
class CommonTests(tutils.DaemonTests):
@@ -137,7 +138,7 @@ class CommonTests(tutils.DaemonTests):
def test_static(self):
rsp = self.get("200:b<file")
assert rsp.status_code == 200
- assert rsp.content.strip() == "testfile"
+ assert rsp.content.strip() == b"testfile"
def test_anchor(self):
rsp = self.getpath("/anchor/foo")
@@ -148,7 +149,7 @@ class CommonTests(tutils.DaemonTests):
with c.connect():
if self.ssl:
c.convert_to_ssl()
- c.wfile.write("foo\n\n\n")
+ c.wfile.write(b"foo\n\n\n")
c.wfile.flush()
l = self.d.last_log()
assert l["type"] == "error"
@@ -177,7 +178,7 @@ class CommonTests(tutils.DaemonTests):
def test_source_access_denied(self):
rsp = self.get("200:b</foo")
assert rsp.status_code == 800
- assert "File access denied" in rsp.content
+ assert b"File access denied" in rsp.content
def test_proxy(self):
r, _ = self.pathoc([r"get:'http://foo.com/p/202':da"])
@@ -195,7 +196,7 @@ class CommonTests(tutils.DaemonTests):
["ws:/p/", "wf:f'wf:b\"test\"':pa,1"],
ws_read_limit=1
)
- assert r[1].payload == "test"
+ assert r[1].payload == b"test"
def test_websocket_frame_reflect_error(self):
r, _ = self.pathoc(
@@ -239,7 +240,7 @@ class TestDaemonSSL(CommonTests):
c.rbufsize = 0
c.wbufsize = 0
with c.connect():
- c.wfile.write("\0\0\0\0")
+ c.wfile.write(b"\0\0\0\0")
tutils.raises(TlsException, c.convert_to_ssl)
l = self.d.last_log()
assert l["type"] == "error"
diff --git a/test/pathod/test_pathod_cmdline.py b/test/pathod/test_pathod_cmdline.py
index 18d54c82..58123b37 100644
--- a/test/pathod/test_pathod_cmdline.py
+++ b/test/pathod/test_pathod_cmdline.py
@@ -1,7 +1,9 @@
-from pathod import pathod_cmdline as cmdline
-import tutils
import mock
+from pathod import pathod_cmdline as cmdline
+
+from . import tutils
+
def test_parse_anchor_spec():
assert cmdline.parse_anchor_spec("foo=200") == ("foo", "200")
diff --git a/test/netlib/http/http2/test_connections.py b/test/pathod/test_protocols_http2.py
index 2a43627a..e42c2858 100644
--- a/test/netlib/http/http2/test_connections.py
+++ b/test/pathod/test_protocols_http2.py
@@ -5,21 +5,22 @@ import hyperframe
from netlib import tcp, http
from netlib.tutils import raises
from netlib.exceptions import TcpDisconnect
-from netlib.http.http2.connections import HTTP2Protocol, TCPHandler
from netlib.http.http2 import framereader
-from ... import tservers
+from ..netlib import tservers as netlib_tservers
+
+from pathod.protocols.http2 import HTTP2StateProtocol, TCPHandler
class TestTCPHandlerWrapper:
def test_wrapped(self):
h = TCPHandler(rfile='foo', wfile='bar')
- p = HTTP2Protocol(h)
+ p = HTTP2StateProtocol(h)
assert p.tcp_handler.rfile == 'foo'
assert p.tcp_handler.wfile == 'bar'
def test_direct(self):
- p = HTTP2Protocol(rfile='foo', wfile='bar')
+ p = HTTP2StateProtocol(rfile='foo', wfile='bar')
assert isinstance(p.tcp_handler, TCPHandler)
assert p.tcp_handler.rfile == 'foo'
assert p.tcp_handler.wfile == 'bar'
@@ -36,10 +37,10 @@ class EchoHandler(tcp.BaseHandler):
class TestProtocol:
- @mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_server_connection_preface")
- @mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_client_connection_preface")
+ @mock.patch("pathod.protocols.http2.HTTP2StateProtocol.perform_server_connection_preface")
+ @mock.patch("pathod.protocols.http2.HTTP2StateProtocol.perform_client_connection_preface")
def test_perform_connection_preface(self, mock_client_method, mock_server_method):
- protocol = HTTP2Protocol(is_server=False)
+ protocol = HTTP2StateProtocol(is_server=False)
protocol.connection_preface_performed = True
protocol.perform_connection_preface()
@@ -50,10 +51,10 @@ class TestProtocol:
assert mock_client_method.called
assert not mock_server_method.called
- @mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_server_connection_preface")
- @mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_client_connection_preface")
+ @mock.patch("pathod.protocols.http2.HTTP2StateProtocol.perform_server_connection_preface")
+ @mock.patch("pathod.protocols.http2.HTTP2StateProtocol.perform_client_connection_preface")
def test_perform_connection_preface_server(self, mock_client_method, mock_server_method):
- protocol = HTTP2Protocol(is_server=True)
+ protocol = HTTP2StateProtocol(is_server=True)
protocol.connection_preface_performed = True
protocol.perform_connection_preface()
@@ -65,7 +66,7 @@ class TestProtocol:
assert mock_server_method.called
-class TestCheckALPNMatch(tservers.ServerTestBase):
+class TestCheckALPNMatch(netlib_tservers.ServerTestBase):
handler = EchoHandler
ssl = dict(
alpn_select=b'h2',
@@ -77,11 +78,11 @@ class TestCheckALPNMatch(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl(alpn_protos=[b'h2'])
- protocol = HTTP2Protocol(c)
+ protocol = HTTP2StateProtocol(c)
assert protocol.check_alpn()
-class TestCheckALPNMismatch(tservers.ServerTestBase):
+class TestCheckALPNMismatch(netlib_tservers.ServerTestBase):
handler = EchoHandler
ssl = dict(
alpn_select=None,
@@ -93,12 +94,12 @@ class TestCheckALPNMismatch(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl(alpn_protos=[b'h2'])
- protocol = HTTP2Protocol(c)
+ protocol = HTTP2StateProtocol(c)
with raises(NotImplementedError):
protocol.check_alpn()
-class TestPerformServerConnectionPreface(tservers.ServerTestBase):
+class TestPerformServerConnectionPreface(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
@@ -125,7 +126,7 @@ class TestPerformServerConnectionPreface(tservers.ServerTestBase):
def test_perform_server_connection_preface(self):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
- protocol = HTTP2Protocol(c)
+ protocol = HTTP2StateProtocol(c)
assert not protocol.connection_preface_performed
protocol.perform_server_connection_preface()
@@ -135,12 +136,12 @@ class TestPerformServerConnectionPreface(tservers.ServerTestBase):
protocol.perform_server_connection_preface(force=True)
-class TestPerformClientConnectionPreface(tservers.ServerTestBase):
+class TestPerformClientConnectionPreface(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
# check magic
- assert self.rfile.read(24) == HTTP2Protocol.CLIENT_CONNECTION_PREFACE
+ assert self.rfile.read(24) == HTTP2StateProtocol.CLIENT_CONNECTION_PREFACE
# check empty settings frame
assert self.rfile.read(9) ==\
@@ -161,7 +162,7 @@ class TestPerformClientConnectionPreface(tservers.ServerTestBase):
def test_perform_client_connection_preface(self):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
- protocol = HTTP2Protocol(c)
+ protocol = HTTP2StateProtocol(c)
assert not protocol.connection_preface_performed
protocol.perform_client_connection_preface()
@@ -170,7 +171,7 @@ class TestPerformClientConnectionPreface(tservers.ServerTestBase):
class TestClientStreamIds(object):
c = tcp.TCPClient(("127.0.0.1", 0))
- protocol = HTTP2Protocol(c)
+ protocol = HTTP2StateProtocol(c)
def test_client_stream_ids(self):
assert self.protocol.current_stream_id is None
@@ -182,9 +183,9 @@ class TestClientStreamIds(object):
assert self.protocol.current_stream_id == 5
-class TestServerStreamIds(object):
+class TestserverstreamIds(object):
c = tcp.TCPClient(("127.0.0.1", 0))
- protocol = HTTP2Protocol(c, is_server=True)
+ protocol = HTTP2StateProtocol(c, is_server=True)
def test_server_stream_ids(self):
assert self.protocol.current_stream_id is None
@@ -196,7 +197,7 @@ class TestServerStreamIds(object):
assert self.protocol.current_stream_id == 6
-class TestApplySettings(tservers.ServerTestBase):
+class TestApplySettings(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
# check settings acknowledgement
@@ -211,7 +212,7 @@ class TestApplySettings(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl()
- protocol = HTTP2Protocol(c)
+ protocol = HTTP2StateProtocol(c)
protocol._apply_settings({
hyperframe.frame.SettingsFrame.ENABLE_PUSH: 'foo',
@@ -239,12 +240,12 @@ class TestCreateHeaders(object):
(b':scheme', b'https'),
(b'foo', b'bar')])
- bytes = HTTP2Protocol(self.c)._create_headers(
+ bytes = HTTP2StateProtocol(self.c)._create_headers(
headers, 1, end_stream=True)
assert b''.join(bytes) ==\
codecs.decode('000014010500000001824488355217caf3a69a3f87408294e7838c767f', 'hex_codec')
- bytes = HTTP2Protocol(self.c)._create_headers(
+ bytes = HTTP2StateProtocol(self.c)._create_headers(
headers, 1, end_stream=False)
assert b''.join(bytes) ==\
codecs.decode('000014010400000001824488355217caf3a69a3f87408294e7838c767f', 'hex_codec')
@@ -257,7 +258,7 @@ class TestCreateHeaders(object):
(b'foo', b'bar'),
(b'server', b'version')])
- protocol = HTTP2Protocol(self.c)
+ protocol = HTTP2StateProtocol(self.c)
protocol.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] = 8
bytes = protocol._create_headers(headers, 1, end_stream=True)
assert len(bytes) == 3
@@ -270,17 +271,17 @@ class TestCreateBody(object):
c = tcp.TCPClient(("127.0.0.1", 0))
def test_create_body_empty(self):
- protocol = HTTP2Protocol(self.c)
+ protocol = HTTP2StateProtocol(self.c)
bytes = protocol._create_body(b'', 1)
assert b''.join(bytes) == b''
def test_create_body_single_frame(self):
- protocol = HTTP2Protocol(self.c)
+ protocol = HTTP2StateProtocol(self.c)
bytes = protocol._create_body(b'foobar', 1)
assert b''.join(bytes) == codecs.decode('000006000100000001666f6f626172', 'hex_codec')
def test_create_body_multiple_frames(self):
- protocol = HTTP2Protocol(self.c)
+ protocol = HTTP2StateProtocol(self.c)
protocol.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] = 5
bytes = protocol._create_body(b'foobarmehm42', 1)
assert len(bytes) == 3
@@ -289,7 +290,7 @@ class TestCreateBody(object):
assert bytes[2] == codecs.decode('0000020001000000013432', 'hex_codec')
-class TestReadRequest(tservers.ServerTestBase):
+class TestReadRequest(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
@@ -306,7 +307,7 @@ class TestReadRequest(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl()
- protocol = HTTP2Protocol(c, is_server=True)
+ protocol = HTTP2StateProtocol(c, is_server=True)
protocol.connection_preface_performed = True
req = protocol.read_request(NotImplemented)
@@ -319,7 +320,7 @@ class TestReadRequest(tservers.ServerTestBase):
assert req.content == b'foobar'
-class TestReadRequestRelative(tservers.ServerTestBase):
+class TestReadRequestRelative(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
self.wfile.write(
@@ -332,7 +333,7 @@ class TestReadRequestRelative(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl()
- protocol = HTTP2Protocol(c, is_server=True)
+ protocol = HTTP2StateProtocol(c, is_server=True)
protocol.connection_preface_performed = True
req = protocol.read_request(NotImplemented)
@@ -342,7 +343,7 @@ class TestReadRequestRelative(tservers.ServerTestBase):
assert req.path == "*"
-class TestReadRequestAbsolute(tservers.ServerTestBase):
+class TestReadRequestAbsolute(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
self.wfile.write(
@@ -355,7 +356,7 @@ class TestReadRequestAbsolute(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl()
- protocol = HTTP2Protocol(c, is_server=True)
+ protocol = HTTP2StateProtocol(c, is_server=True)
protocol.connection_preface_performed = True
req = protocol.read_request(NotImplemented)
@@ -366,7 +367,7 @@ class TestReadRequestAbsolute(tservers.ServerTestBase):
assert req.port == 22
-class TestReadRequestConnect(tservers.ServerTestBase):
+class TestReadRequestConnect(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
self.wfile.write(
@@ -381,7 +382,7 @@ class TestReadRequestConnect(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl()
- protocol = HTTP2Protocol(c, is_server=True)
+ protocol = HTTP2StateProtocol(c, is_server=True)
protocol.connection_preface_performed = True
req = protocol.read_request(NotImplemented)
@@ -397,7 +398,7 @@ class TestReadRequestConnect(tservers.ServerTestBase):
assert req.port == 443
-class TestReadResponse(tservers.ServerTestBase):
+class TestReadResponse(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
self.wfile.write(
@@ -413,7 +414,7 @@ class TestReadResponse(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl()
- protocol = HTTP2Protocol(c)
+ protocol = HTTP2StateProtocol(c)
protocol.connection_preface_performed = True
resp = protocol.read_response(NotImplemented, stream_id=42)
@@ -426,7 +427,7 @@ class TestReadResponse(tservers.ServerTestBase):
assert resp.timestamp_end
-class TestReadEmptyResponse(tservers.ServerTestBase):
+class TestReadEmptyResponse(netlib_tservers.ServerTestBase):
class handler(tcp.BaseHandler):
def handle(self):
self.wfile.write(
@@ -439,7 +440,7 @@ class TestReadEmptyResponse(tservers.ServerTestBase):
c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect():
c.convert_to_ssl()
- protocol = HTTP2Protocol(c)
+ protocol = HTTP2StateProtocol(c)
protocol.connection_preface_performed = True
resp = protocol.read_response(NotImplemented, stream_id=42)
@@ -456,7 +457,7 @@ class TestAssembleRequest(object):
c = tcp.TCPClient(("127.0.0.1", 0))
def test_request_simple(self):
- bytes = HTTP2Protocol(self.c).assemble_request(http.Request(
+ bytes = HTTP2StateProtocol(self.c).assemble_request(http.Request(
b'',
b'GET',
b'https',
@@ -483,12 +484,12 @@ class TestAssembleRequest(object):
None,
)
req.stream_id = 0x42
- bytes = HTTP2Protocol(self.c).assemble_request(req)
+ bytes = HTTP2StateProtocol(self.c).assemble_request(req)
assert len(bytes) == 1
assert bytes[0] == codecs.decode('00000d0105000000428284874188089d5c0b8170dc07', 'hex_codec')
def test_request_with_body(self):
- bytes = HTTP2Protocol(self.c).assemble_request(http.Request(
+ bytes = HTTP2StateProtocol(self.c).assemble_request(http.Request(
b'',
b'GET',
b'https',
@@ -510,7 +511,7 @@ class TestAssembleResponse(object):
c = tcp.TCPClient(("127.0.0.1", 0))
def test_simple(self):
- bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(http.Response(
+ bytes = HTTP2StateProtocol(self.c, is_server=True).assemble_response(http.Response(
b"HTTP/2.0",
200,
))
@@ -524,13 +525,13 @@ class TestAssembleResponse(object):
200,
)
resp.stream_id = 0x42
- bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(resp)
+ bytes = HTTP2StateProtocol(self.c, is_server=True).assemble_response(resp)
assert len(bytes) == 1
assert bytes[0] ==\
codecs.decode('00000101050000004288', 'hex_codec')
def test_with_body(self):
- bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(http.Response(
+ bytes = HTTP2StateProtocol(self.c, is_server=True).assemble_response(http.Response(
b"HTTP/2.0",
200,
b'',
diff --git a/test/pathod/test_test.py b/test/pathod/test_test.py
index 6399894e..d69e72f3 100644
--- a/test/pathod/test_test.py
+++ b/test/pathod/test_test.py
@@ -1,7 +1,8 @@
import logging
import requests
from pathod import test
-import tutils
+
+from . import tutils
import requests.packages.urllib3
diff --git a/test/pathod/test_utils.py b/test/pathod/test_utils.py
index 2bb82fe7..2bdfe501 100644
--- a/test/pathod/test_utils.py
+++ b/test/pathod/test_utils.py
@@ -1,5 +1,6 @@
from pathod import utils
-import tutils
+
+from . import tutils
def test_membool():
diff --git a/test/pathod/tutils.py b/test/pathod/tutils.py
index daaa8628..3a94b6eb 100644
--- a/test/pathod/tutils.py
+++ b/test/pathod/tutils.py
@@ -100,7 +100,7 @@ class DaemonTests(object):
)
with c.connect():
resp = c.request(
- "get:/p/%s" % urllib.parse.quote(spec).encode("string_escape")
+ "get:/p/%s" % urllib.parse.quote(spec)
)
return resp
diff --git a/tox.ini b/tox.ini
index c150e14e..1c48b91f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,7 @@ deps =
codecov>=2.0.5
passenv = CI TRAVIS_BUILD_ID TRAVIS TRAVIS_BRANCH TRAVIS_JOB_NUMBER TRAVIS_PULL_REQUEST TRAVIS_JOB_ID TRAVIS_REPO_SLUG TRAVIS_COMMIT
setenv =
- PY3TESTS = test/netlib test/mitmproxy/script test/pathod/test_utils.py test/pathod/test_log.py test/pathod/test_language_generators.py test/pathod/test_language_writer.py test/pathod/test_language_base.py test/pathod/test_language_http.py test/pathod/test_language_websocket.py test/pathod/test_language_http2.py test/pathod/test_pathoc.py
+ PY3TESTS = test/netlib test/pathod/ test/mitmproxy/script
[testenv:py27]
commands =
diff --git a/web/.editorconfig b/web/.editorconfig
index 9acd1b0f..ddc78021 100644
--- a/web/.editorconfig
+++ b/web/.editorconfig
@@ -3,3 +3,4 @@ indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
+end_of_line = lf
diff --git a/web/src/css/eventlog.less b/web/src/css/eventlog.less
index 908312cd..393f75db 100644
--- a/web/src/css/eventlog.less
+++ b/web/src/css/eventlog.less
@@ -10,6 +10,8 @@
background-color: #F2F2F2;
padding: 0 5px;
flex: 0 0 auto;
+ border-top: 1px solid #aaa;
+ cursor: row-resize;
}
> pre {
@@ -48,4 +50,4 @@
margin-top: -2px;
margin-left: 3px;
}
-} \ No newline at end of file
+}
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx
new file mode 100644
index 00000000..af3bffc1
--- /dev/null
+++ b/web/src/js/components/ContentView.jsx
@@ -0,0 +1,78 @@
+import React, { Component, PropTypes } from 'react'
+import { MessageUtils } from '../flow/utils.js'
+import { ViewAuto, ViewImage } from './ContentView/ContentViews'
+import * as ContentErrors from './ContentView/ContentErrors'
+import ContentLoader from './ContentView/ContentLoader'
+import ViewSelector from './ContentView/ViewSelector'
+
+export default class ContentView extends Component {
+
+ static 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,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { displayLarge: false, View: ViewAuto }
+ this.selectView = this.selectView.bind(this)
+ }
+
+ selectView(View) {
+ this.setState({ View })
+ }
+
+ displayLarge() {
+ this.setState({ displayLarge: true })
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.message !== this.props.message) {
+ this.setState({ displayLarge: false, View: ViewAuto })
+ }
+ }
+
+ isContentTooLarge(msg) {
+ return msg.contentLength > 1024 * 1024 * (ViewImage.matches(msg) ? 10 : 0.2)
+ }
+
+ render() {
+ const { flow, message } = this.props
+ const { displayLarge, View } = this.state
+
+ if (message.contentLength === 0) {
+ return <ContentErrors.ContentEmpty {...this.props}/>
+ }
+
+ if (message.contentLength === null) {
+ return <ContentErrors.ContentMissing {...this.props}/>
+ }
+
+ if (!displayLarge && this.isContentTooLarge(message)) {
+ return <ContentErrors.ContentTooLarge {...this.props} onClick={this.displayLarge}/>
+ }
+
+ return (
+ <div>
+ {View.textView ? (
+ <ContentLoader flow={flow} message={message}>
+ <this.state.View content="" />
+ </ContentLoader>
+ ) : (
+ <View flow={flow} message={message} />
+ )}
+ <div className="view-options text-center">
+ <ViewSelector onSelectView={this.selectView} active={View} message={message}/>
+ &nbsp;
+ <a className="btn btn-default btn-xs" href={MessageUtils.getContentURL(flow, message)}>
+ <i className="fa fa-download"/>
+ </a>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/ContentView/ContentErrors.jsx b/web/src/js/components/ContentView/ContentErrors.jsx
new file mode 100644
index 00000000..11594c7f
--- /dev/null
+++ b/web/src/js/components/ContentView/ContentErrors.jsx
@@ -0,0 +1,28 @@
+import React from 'react'
+import { ViewImage } from './ContentViews'
+import {formatSize} from '../../utils.js'
+
+export function ContentEmpty({ flow, message }) {
+ return (
+ <div className="alert alert-info">
+ No {flow.request === message ? 'request' : 'response'} content.
+ </div>
+ )
+}
+
+export function ContentMissing({ flow, message }) {
+ return (
+ <div className="alert alert-info">
+ {flow.request === message ? 'Request' : 'Response'} content missing.
+ </div>
+ )
+}
+
+export function ContentTooLarge({ message, onClick }) {
+ return (
+ <div className="alert alert-warning">
+ <button onClick={onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
+ {formatSize(message.contentLength)} content size.
+ </div>
+ )
+}
diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx
new file mode 100644
index 00000000..f346dc01
--- /dev/null
+++ b/web/src/js/components/ContentView/ContentLoader.jsx
@@ -0,0 +1,67 @@
+import React, { Component, PropTypes } from 'react'
+import { MessageUtils } from '../../flow/utils.js'
+
+export default class ContentLoader extends Component {
+
+ static propTypes = {
+ flow: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = { content: null, request: null }
+ }
+
+ requestContent(nextProps) {
+ if (this.state.request) {
+ this.state.request.abort()
+ }
+
+ const request = MessageUtils.getContent(nextProps.flow, nextProps.message)
+
+ this.setState({ content: null, request })
+
+ request
+ .done(content => {
+ this.setState({ content })
+ })
+ .fail((xhr, textStatus, errorThrown) => {
+ if (textStatus === 'abort') {
+ return
+ }
+ this.setState({ content: `AJAX Error: ${textStatus}\r\n${errorThrown}` })
+ })
+ .always(() => {
+ this.setState({ request: null })
+ })
+ }
+
+ componentWillMount() {
+ this.requestContent(this.props)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.message !== this.props.message) {
+ this.requestContent(nextProps)
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.state.request) {
+ this.state.request.abort()
+ }
+ }
+
+ render() {
+ return this.state.content ? (
+ React.cloneElement(this.props.children, {
+ content: this.state.content
+ })
+ ) : (
+ <div className="text-center">
+ <i className="fa fa-spinner fa-spin"></i>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx
new file mode 100644
index 00000000..b0297dcc
--- /dev/null
+++ b/web/src/js/components/ContentView/ContentViews.jsx
@@ -0,0 +1,70 @@
+import React, { PropTypes } from 'react'
+import ContentLoader from './ContentLoader'
+import { MessageUtils } from '../../flow/utils.js'
+
+const views = [ViewAuto, ViewImage, ViewJSON, ViewRaw]
+
+ViewImage.regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i
+ViewImage.matches = msg => ViewImage.regex.test(MessageUtils.getContentType(msg))
+
+ViewImage.propTypes = {
+ flow: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
+}
+
+export function ViewImage({ flow, message }) {
+ return (
+ <div className="flowview-image">
+ <img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
+ </div>
+ )
+}
+
+ViewRaw.textView = true
+ViewRaw.matches = () => true
+
+ViewRaw.propTypes = {
+ content: React.PropTypes.string.isRequired,
+}
+
+export function ViewRaw({ content }) {
+ return <pre>{content}</pre>
+}
+
+ViewJSON.textView = true
+ViewJSON.regex = /^application\/json$/i
+ViewJSON.matches = msg => ViewJSON.regex.test(MessageUtils.getContentType(msg))
+
+ViewJSON.propTypes = {
+ content: React.PropTypes.string.isRequired,
+}
+
+export function ViewJSON({ content }) {
+ let json = content
+ try {
+ json = JSON.stringify(JSON.parse(content), null, 2);
+ } catch (e) {
+ // @noop
+ }
+ return <pre>{json}</pre>
+}
+
+
+ViewAuto.matches = () => false
+ViewAuto.findView = msg => views.find(v => v.matches(msg)) || views[views.length - 1]
+
+ViewAuto.propTypes = {
+ message: React.PropTypes.object.isRequired,
+ flow: React.PropTypes.object.isRequired,
+}
+
+export function ViewAuto({ message, flow }) {
+ const View = ViewAuto.findView(message)
+ if (View.textView) {
+ return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader>
+ } else {
+ return <View message={message} flow={flow} />
+ }
+}
+
+export default views
diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx
new file mode 100644
index 00000000..df3a5b83
--- /dev/null
+++ b/web/src/js/components/ContentView/ViewSelector.jsx
@@ -0,0 +1,28 @@
+import React, { PropTypes } from 'react'
+import classnames from 'classnames'
+import views, { ViewAuto } from './ContentViews'
+
+ViewSelector.propTypes = {
+ active: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ onSelectView: PropTypes.func.isRequired,
+}
+
+export default function ViewSelector({ active, message, onSelectView }) {
+ return (
+ <div className="view-selector btn-group btn-group-xs">
+ {views.map(View => (
+ <button
+ key={View.name}
+ onClick={() => onSelectView(View)}
+ className={classnames('btn btn-default', { active: View === active })}>
+ {View === ViewAuto ? (
+ `auto: ${ViewAuto.findView(message).name.toLowerCase().replace('view', '')}`
+ ) : (
+ View.name.toLowerCase().replace('view', '')
+ )}
+ </button>
+ ))}
+ </div>
+ )
+}
diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx
index 58cecd1a..169162ee 100644
--- a/web/src/js/components/EventLog.jsx
+++ b/web/src/js/components/EventLog.jsx
@@ -1,31 +1,70 @@
-import React, { PropTypes } from 'react'
+import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { toggleEventLogFilter, toggleEventLogVisibility } from '../ducks/eventLog'
-import { ToggleButton } from './common'
+import ToggleButton from './common/ToggleButton'
import EventList from './EventLog/EventList'
-EventLog.propTypes = {
- filters: PropTypes.object.isRequired,
- events: PropTypes.array.isRequired,
- onToggleFilter: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired
-}
+class EventLog extends Component {
+
+ static propTypes = {
+ filters: PropTypes.object.isRequired,
+ events: PropTypes.array.isRequired,
+ onToggleFilter: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+ defaultHeight: PropTypes.number,
+ }
+
+ static defaultProps = {
+ defaultHeight: 200,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { height: this.props.defaultHeight }
+
+ this.onDragStart = this.onDragStart.bind(this)
+ this.onDragMove = this.onDragMove.bind(this)
+ this.onDragStop = this.onDragStop.bind(this)
+ }
-function EventLog({ filters, events, onToggleFilter, onClose }) {
- return (
- <div className="eventlog">
- <div>
- Eventlog
- <div className="pull-right">
- {['debug', 'info', 'web'].map(type => (
- <ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => onToggleFilter(type)}/>
- ))}
- <i onClick={onClose} className="fa fa-close"></i>
+ onDragStart(event) {
+ event.preventDefault()
+ this.dragStart = this.state.height + event.pageY
+ window.addEventListener('mousemove', this.onDragMove)
+ window.addEventListener('mouseup', this.onDragStop)
+ window.addEventListener('dragend', this.onDragStop)
+ }
+
+ onDragMove(event) {
+ event.preventDefault()
+ this.setState({ height: this.dragStart - event.pageY })
+ }
+
+ onDragStop(event) {
+ event.preventDefault()
+ window.removeEventListener('mousemove', this.onDragMove)
+ }
+
+ render() {
+ const { height } = this.state
+ const { filters, events, onToggleFilter, onClose } = this.props
+
+ return (
+ <div className="eventlog" style={{ height }}>
+ <div onMouseDown={this.onDragStart}>
+ Eventlog
+ <div className="pull-right">
+ {['debug', 'info', 'web'].map(type => (
+ <ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => onToggleFilter(type)}/>
+ ))}
+ <i onClick={onClose} className="fa fa-close"></i>
+ </div>
</div>
+ <EventList events={events} />
</div>
- <EventList events={events} />
- </div>
- )
+ )
+ }
}
export default connect(
diff --git a/web/src/js/components/FlowTable/FlowTableHead.jsx b/web/src/js/components/FlowTable/FlowTableHead.jsx
index 1df38aba..840f6a34 100644
--- a/web/src/js/components/FlowTable/FlowTableHead.jsx
+++ b/web/src/js/components/FlowTable/FlowTableHead.jsx
@@ -19,16 +19,12 @@ function FlowTableHead({ sortColumn, sortDesc, onSort }) {
{columns.map(Column => (
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
key={Column.name}
- onClick={() => onClick(Column)}>
+ onClick={() => onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })}>
{Column.headerName}
</th>
))}
</tr>
)
-
- function onClick(Column) {
- onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })
- }
}
export default connect(
diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx
new file mode 100644
index 00000000..23f8b3ea
--- /dev/null
+++ b/web/src/js/components/FlowView.jsx
@@ -0,0 +1,107 @@
+import React, { Component } from 'react'
+import _ from 'lodash'
+
+import Nav from './FlowView/Nav'
+import { Request, Response, Error } from './FlowView/Messages'
+import Details from './FlowView/Details'
+import Prompt from './Prompt'
+
+export default class FlowView extends Component {
+
+ static allTabs = { Request, Response, Error, Details }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { prompt: false }
+
+ this.closePrompt = this.closePrompt.bind(this)
+ this.selectTab = this.selectTab.bind(this)
+ }
+
+ getTabs() {
+ return ['request', 'response', 'error'].filter(k => this.props.flow[k]).concat(['details'])
+ }
+
+ nextTab(increment) {
+ const tabs = this.getTabs()
+ // JS modulo operator doesn't correct negative numbers, make sure that we are positive.
+ this.selectTab(tabs[(tabs.indexOf(this.props.tab) + increment + tabs.length) % tabs.length])
+ }
+
+ selectTab(panel) {
+ this.props.updateLocation(`/flows/${this.props.flow.id}/${panel}`)
+ }
+
+ closePrompt(edit) {
+ this.setState({ prompt: false })
+ if (edit) {
+ this.refs.tab.edit(edit)
+ }
+ }
+
+ promptEdit() {
+ let options
+
+ switch (this.props.tab) {
+
+ case 'request':
+ options = [
+ 'method',
+ 'url',
+ { text: 'http version', key: 'v' },
+ 'header'
+ ]
+ break
+
+ case 'response':
+ options = [
+ { text: 'http version', key: 'v' },
+ 'code',
+ 'message',
+ 'header'
+ ]
+ break
+
+ case 'details':
+ return
+
+ default:
+ throw 'Unknown tab for edit: ' + this.props.tab
+ }
+
+ this.setState({ prompt: { options, done: this.closePrompt } })
+ }
+
+ render() {
+ const tabs = this.getTabs()
+ let { flow, tab: active } = this.props
+
+ if (tabs.indexOf(active) < 0) {
+ if (active === 'response' && flow.error) {
+ active = 'error'
+ } else if (active === 'error' && flow.response) {
+ active = 'response'
+ } else {
+ active = tabs[0]
+ }
+ }
+
+ const Tab = FlowView.allTabs[_.capitalize(active)]
+
+ return (
+ <div className="flow-detail" onScroll={this.adjustHead}>
+ <Nav
+ flow={flow}
+ tabs={tabs}
+ active={active}
+ onSelectTab={this.selectTab}
+ />
+ <Tab ref="tab" flow={flow}/>
+ {this.state.prompt && (
+ <Prompt {...this.state.prompt}/>
+ )}
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/FlowView/Details.jsx b/web/src/js/components/FlowView/Details.jsx
new file mode 100644
index 00000000..78e68ecf
--- /dev/null
+++ b/web/src/js/components/FlowView/Details.jsx
@@ -0,0 +1,133 @@
+import React from 'react'
+import _ from 'lodash'
+import { formatTimeStamp, formatTimeDelta } from '../../utils.js'
+
+export function TimeStamp({ t, deltaTo, title }) {
+ return t ? (
+ <tr>
+ <td>{title}:</td>
+ <td>
+ {formatTimeStamp(t)}
+ {deltaTo && (
+ <span className="text-muted">
+ ({formatTimeDelta(1000 * (t - deltaTo))})
+ </span>
+ )}
+ </td>
+ </tr>
+ ) : (
+ <tr></tr>
+ )
+}
+
+export function ConnectionInfo({ conn }) {
+ return (
+ <table className="connection-table">
+ <tbody>
+ <tr key="address">
+ <td>Address:</td>
+ <td>{conn.address.address.join(':')}</td>
+ </tr>
+ {conn.sni ? (
+ <tr key="sni"></tr>
+ ) : (
+ <tr key="sni">
+ <td>
+ <abbr title="TLS Server Name Indication">TLS SNI:</abbr>
+ </td>
+ <td>{conn.sni}</td>
+ </tr>
+ )}
+ </tbody>
+ </table>
+ )
+}
+
+export function CertificateInfo({ flow }) {
+ // @todo We should fetch human-readable certificate representation from the server
+ return (
+ <div>
+ {flow.client_conn.cert && [
+ <h4 key="name">Client Certificate</h4>,
+ <pre key="value" style={{ maxHeight: 100 }}>{flow.client_conn.cert}</pre>
+ ]}
+
+ {flow.server_conn.cert && [
+ <h4 key="name">Server Certificate</h4>,
+ <pre key="value" style={{ maxHeight: 100 }}>{flow.server_conn.cert}</pre>
+ ]}
+ </div>
+ )
+}
+
+export function Timing({ flow }) {
+ const { server_conn: sc, client_conn: cc, request: req, response: res } = flow
+
+ const 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
+ }, res && {
+ title: "First response byte",
+ t: res.timestamp_start,
+ deltaTo: req.timestamp_start
+ }, res && {
+ title: "Response complete",
+ t: res.timestamp_end,
+ deltaTo: req.timestamp_start
+ }
+ ]
+
+ return (
+ <div>
+ <h4>Timing</h4>
+ <table className="timing-table">
+ <tbody>
+ {timestamps.filter(v => v).sort((a, b) => a.t - b.t).map(item => (
+ <TimeStamp key={item.title} {...item}/>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )
+}
+
+export default function Details({ flow }) {
+ return (
+ <section>
+ <h4>Client Connection</h4>
+ <ConnectionInfo conn={flow.client_conn}/>
+
+ <h4>Server Connection</h4>
+ <ConnectionInfo conn={flow.server_conn}/>
+
+ <CertificateInfo flow={flow}/>
+
+ <Timing flow={flow}/>
+ </section>
+ )
+}
diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx
new file mode 100644
index 00000000..880eeda1
--- /dev/null
+++ b/web/src/js/components/FlowView/Headers.jsx
@@ -0,0 +1,130 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import ValueEditor from '../ValueEditor'
+import { Key } from '../../utils.js'
+
+class HeaderEditor extends Component {
+
+ render() {
+ return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>
+ }
+
+ focus() {
+ ReactDOM.findDOMNode(this).focus()
+ }
+
+ onKeyDown(e) {
+ switch (e.keyCode) {
+ case Key.BACKSPACE:
+ var s = window.getSelection().getRangeAt(0)
+ if (s.startOffset === 0 && s.endOffset === 0) {
+ this.props.onRemove(e)
+ }
+ break
+ case Key.TAB:
+ if (!e.shiftKey) {
+ this.props.onTab(e)
+ }
+ break
+ }
+ }
+}
+
+export default class Headers extends Component {
+
+ static propTypes = {
+ onChange: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ }
+
+ onChange(row, col, val) {
+ const nextHeaders = _.cloneDeep(this.props.message.headers)
+
+ nextHeaders[row][col] = val
+
+ if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
+ // do not delete last row
+ if (nextHeaders.length === 1) {
+ nextHeaders[0][0] = 'Name'
+ nextHeaders[0][1] = 'Value'
+ } else {
+ nextHeaders.splice(row, 1)
+ // manually move selection target if this has been the last row.
+ if (row === nextHeaders.length) {
+ this._nextSel = `${row - 1}-value`
+ }
+ }
+ }
+
+ this.props.onChange(nextHeaders)
+ }
+
+ edit() {
+ this.refs['0-key'].focus()
+ }
+
+ onTab(row, col, e) {
+ const headers = this.props.message.headers
+
+ if (row !== headers.length - 1 || col !== 1) {
+ return
+ }
+
+ e.preventDefault()
+
+ const nextHeaders = _.cloneDeep(this.props.message.headers)
+ nextHeaders.push(['Name', 'Value'])
+ this.props.onChange(nextHeaders)
+ this._nextSel = `${row + 1}-key`
+ }
+
+ componentDidUpdate() {
+ if (this._nextSel && this.refs[this._nextSel]) {
+ this.refs[this._nextSel].focus()
+ this._nextSel = undefined
+ }
+ }
+
+ onRemove(row, col, e) {
+ if (col === 1) {
+ e.preventDefault()
+ this.refs[`${row}-key`].focus()
+ } else if (row > 0) {
+ e.preventDefault()
+ this.refs[`${row - 1}-value`].focus()
+ }
+ }
+
+ render() {
+ const { message } = this.props
+
+ return (
+ <table className="header-table">
+ <tbody>
+ {message.headers.map((header, i) => (
+ <tr key={i}>
+ <td className="header-name">
+ <HeaderEditor
+ ref={`${i}-key`}
+ content={header[0]}
+ onDone={val => this.onChange(i, 0, val)}
+ onRemove={event => this.onRemove(i, 0, event)}
+ onTab={event => this.onTab(i, 0, event)}
+ />:
+ </td>
+ <td className="header-value">
+ <HeaderEditor
+ ref={`${i}-value`}
+ content={header[1]}
+ onDone={val => this.onChange(i, 1, val)}
+ onRemove={event => this.onRemove(i, 1, event)}
+ onTab={event => this.onTab(i, 1, event)}
+ />
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ )
+ }
+}
diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx
new file mode 100644
index 00000000..ba6a5f2b
--- /dev/null
+++ b/web/src/js/components/FlowView/Messages.jsx
@@ -0,0 +1,168 @@
+import React, { Component } from 'react'
+import _ from 'lodash'
+
+import { FlowActions } from '../../actions.js'
+import { RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion } from '../../flow/utils.js'
+import { Key, formatTimeStamp } from '../../utils.js'
+import ContentView from '../ContentView'
+import ValueEditor from '../ValueEditor'
+import Headers from './Headers'
+
+class RequestLine extends Component {
+
+ render() {
+ const { flow } = this.props
+
+ return (
+ <div className="first-line request-line">
+ <ValueEditor
+ ref="method"
+ content={flow.request.method}
+ onDone={method => FlowActions.update(flow, { request: { method } })}
+ inline
+ />
+ &nbsp;
+ <ValueEditor
+ ref="url"
+ content={RequestUtils.pretty_url(flow.request)}
+ onDone={url => FlowActions.update(flow, { request: Object.assign({ path: '' }, parseUrl(url)) })}
+ isValid={url => !!parseUrl(url).host}
+ inline
+ />
+ &nbsp;
+ <ValueEditor
+ ref="httpVersion"
+ content={flow.request.http_version}
+ onDone={ver => FlowActions.update(flow, { request: { http_version: parseHttpVersion(ver) } })}
+ isValid={isValidHttpVersion}
+ inline
+ />
+ </div>
+ )
+ }
+}
+
+class ResponseLine extends Component {
+
+ render() {
+ const { flow } = this.props
+
+ return (
+ <div className="first-line response-line">
+ <ValueEditor
+ ref="httpVersion"
+ content={flow.response.http_version}
+ onDone={nextVer => FlowActions.update(flow, { response: { http_version: parseHttpVersion(nextVer) } })}
+ isValid={isValidHttpVersion}
+ inline
+ />
+ &nbsp;
+ <ValueEditor
+ ref="code"
+ content={flow.response.status_code + ''}
+ onDone={code => FlowActions.update(flow, { response: { code: parseInt(code) } })}
+ isValid={code => /^\d+$/.test(code)}
+ inline
+ />
+ &nbsp;
+ <ValueEditor
+ ref="msg"
+ content={flow.response.reason}
+ onDone={msg => FlowActions.update(flow, { response: { msg } })}
+ inline
+ />
+ </div>
+ )
+ }
+}
+
+export class Request extends Component {
+
+ render() {
+ const { flow } = this.props
+
+ return (
+ <section className="request">
+ <RequestLine ref="requestLine" flow={flow}/>
+ <Headers
+ ref="headers"
+ message={flow.request}
+ onChange={headers => FlowActions.update(flow, { request: { headers } })}
+ />
+ <hr/>
+ <ContentView flow={flow} message={flow.request}/>
+ </section>
+ )
+ }
+
+ edit(k) {
+ switch (k) {
+ case 'm':
+ this.refs.requestLine.refs.method.focus()
+ break
+ case 'u':
+ this.refs.requestLine.refs.url.focus()
+ break
+ case 'v':
+ this.refs.requestLine.refs.httpVersion.focus()
+ break
+ case 'h':
+ this.refs.headers.edit()
+ break
+ default:
+ throw new Error(`Unimplemented: ${k}`)
+ }
+ }
+}
+
+export class Response extends Component {
+
+ render() {
+ const { flow } = this.props
+
+ return (
+ <section className="response">
+ <ResponseLine ref="responseLine" flow={flow}/>
+ <Headers
+ ref="headers"
+ message={flow.response}
+ onChange={headers => FlowActions.update(flow, { response: { headers } })}
+ />
+ <hr/>
+ <ContentView flow={flow} message={flow.response}/>
+ </section>
+ )
+ }
+
+ edit(k) {
+ switch (k) {
+ case 'c':
+ this.refs.responseLine.refs.status_code.focus()
+ break
+ case 'm':
+ this.refs.responseLine.refs.msg.focus()
+ break
+ case 'v':
+ this.refs.responseLine.refs.httpVersion.focus()
+ break
+ case 'h':
+ this.refs.headers.edit()
+ break
+ default:
+ throw new Error(`'Unimplemented: ${k}`)
+ }
+ }
+}
+
+export function Error({ flow }) {
+ return (
+ <section>
+ <div className="alert alert-warning">
+ {flow.error.msg}
+ <div>
+ <small>{formatTimeStamp(flow.error.timestamp)}</small>
+ </div>
+ </div>
+ </section>
+ )
+}
diff --git a/web/src/js/components/FlowView/Nav.jsx b/web/src/js/components/FlowView/Nav.jsx
new file mode 100644
index 00000000..386c3a6c
--- /dev/null
+++ b/web/src/js/components/FlowView/Nav.jsx
@@ -0,0 +1,57 @@
+import React, { PropTypes } from 'react'
+import classnames from 'classnames'
+import { FlowActions } from '../../actions.js'
+
+NavAction.propTypes = {
+ icon: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired,
+}
+
+function NavAction({ icon, title, onClick }) {
+ return (
+ <a title={title}
+ href="#"
+ className="nav-action"
+ onClick={event => {
+ event.preventDefault()
+ onClick(event)
+ }}>
+ <i className={`fa fa-fw ${icon}`}></i>
+ </a>
+ )
+}
+
+Nav.propTypes = {
+ flow: PropTypes.object.isRequired,
+ active: PropTypes.string.isRequired,
+ tabs: PropTypes.array.isRequired,
+ onSelectTab: PropTypes.func.isRequired,
+}
+
+export default function Nav({ flow, active, tabs, onSelectTab }) {
+ return (
+ <nav className="nav-tabs nav-tabs-sm">
+ {tabs.map(tab => (
+ <a key={tab}
+ href="#"
+ className={classnames({ active: active === tab })}
+ onClick={event => {
+ event.preventDefault()
+ onSelectTab(tab)
+ }}>
+ {_.capitalize(tab)}
+ </a>
+ ))}
+ <NavAction title="[d]elete flow" icon="fa-trash" onClick={() => FlowActions.delete(flow)} />
+ <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={() => FlowActions.duplicate(flow)} />
+ <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={() => FlowActions.replay(flow)} />
+ {flow.intercepted && (
+ <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={() => FlowActions.accept(flow)} />
+ )}
+ {flow.modified && (
+ <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={() => FlowActions.revert(flow)} />
+ )}
+ </nav>
+ )
+}
diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx
index 903522f4..1f6de2d7 100644
--- a/web/src/js/components/Footer.jsx
+++ b/web/src/js/components/Footer.jsx
@@ -1,6 +1,5 @@
import React from 'react'
import { formatSize } from '../utils.js'
-import { SettingsState } from './common.js'
Footer.propTypes = {
settings: React.PropTypes.object.isRequired,
diff --git a/web/src/js/components/Header.js b/web/src/js/components/Header.jsx
index 93ca5154..93ca5154 100644
--- a/web/src/js/components/Header.js
+++ b/web/src/js/components/Header.jsx
diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx
index 4a43f40f..96f42652 100644
--- a/web/src/js/components/Header/FlowMenu.jsx
+++ b/web/src/js/components/Header/FlowMenu.jsx
@@ -1,10 +1,10 @@
import React, { PropTypes } from 'react'
-import { Button } from '../common.js'
-import {FlowActions} from "../../actions.js";
-import {MessageUtils} from "../../flow/utils.js";
+import Button from '../common/Button'
+import { FlowActions } from '../../actions.js'
+import { MessageUtils } from '../../flow/utils.js'
import { connect } from 'react-redux'
-FlowMenu.title = "Flow"
+FlowMenu.title = 'Flow'
FlowMenu.propTypes = {
flow: PropTypes.object.isRequired,
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
index 6bbf15d5..44f309fd 100644
--- a/web/src/js/components/Header/OptionMenu.jsx
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -1,8 +1,9 @@
import React, { PropTypes } from 'react'
-import { ToggleInputButton, ToggleButton } from '../common.js'
+import ToggleButton from '../common/ToggleButton'
+import ToggleInputButton from '../common/ToggleInputButton'
import { SettingsActions } from '../../actions.js'
-OptionMenu.title = "Options"
+OptionMenu.title = 'Options'
OptionMenu.propTypes = {
settings: PropTypes.object.isRequired,
diff --git a/web/src/js/components/Header/ViewMenu.jsx b/web/src/js/components/Header/ViewMenu.jsx
index a58b0a16..8d662c28 100644
--- a/web/src/js/components/Header/ViewMenu.jsx
+++ b/web/src/js/components/Header/ViewMenu.jsx
@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
-import { ToggleButton } from '../common.js'
+import ToggleButton from '../common/ToggleButton'
import { toggleEventLogVisibility } from '../../ducks/eventLog'
ViewMenu.title = 'View'
diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx
index 8c6ed6d0..7064d3bf 100644
--- a/web/src/js/components/MainView.jsx
+++ b/web/src/js/components/MainView.jsx
@@ -3,9 +3,9 @@ import { connect } from 'react-redux'
import { FlowActions } from '../actions.js'
import { Query } from '../actions.js'
import { Key } from '../utils.js'
-import { Splitter } from './common.js'
+import Splitter from './common/Splitter'
import FlowTable from './FlowTable'
-import FlowView from './flowview/index.js'
+import FlowView from './FlowView'
import { selectFlow, setFilter, setHighlight } from '../ducks/flows'
class MainView extends Component {
diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx
new file mode 100755
index 00000000..6b19b3b3
--- /dev/null
+++ b/web/src/js/components/Prompt.jsx
@@ -0,0 +1,71 @@
+import React, { PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import _ from 'lodash'
+
+import {Key} from '../utils.js'
+
+Prompt.contextTypes = {
+ returnFocus: PropTypes.func
+}
+
+Prompt.propTypes = {
+ options: PropTypes.array.isRequired,
+ done: PropTypes.func.isRequired,
+ prompt: PropTypes.string,
+}
+
+export default function Prompt({ prompt, done, options }, context) {
+ const opts = []
+
+ function keyTaken(k) {
+ return _.map(opts, 'key').includes(k)
+ }
+
+ for (let i = 0; i < options.length; i++) {
+ let opt = options[i]
+ if (_.isString(opt)) {
+ let str = opt
+ while (str.length > 0 && keyTaken(str[0])) {
+ str = str.substr(1)
+ }
+ opt = { text: opt, key: str[0] }
+ }
+ if (!opt.text || !opt.key || keyTaken(opt.key)) {
+ throw 'invalid options'
+ }
+ opts.push(opt)
+ }
+
+ return (
+ <div tabIndex="0" onKeyDown={onKeyDown} onClick={onClick} className="prompt-dialog">
+ <div className="prompt-content">
+ {prompt || <strong>Select: </strong> }
+ {opts.map(opt => {
+ const idx = opt.text.indexOf(opt.key)
+ function onClick(event) {
+ done(opt.key)
+ event.stopPropagation()
+ }
+ return (
+ <span key={opt.key} className="option" onClick={onClick}>
+ {idx !== -1 ? opt.text.substring(0, idx) : opt.text + '('}
+ {prefix}<strong className="text-primary">{opt.key}</strong>
+ {idx !== -1 ? opt.text.substring(idx + 1) : ')'}
+ </span>
+ )
+ })}
+ </div>
+ </div>
+ )
+
+ function onKeyDown(event) {
+ event.stopPropagation()
+ event.preventDefault()
+ const key = opts.find(opt => Key[opt.key.toUpperCase()] === event.keyCode)
+ if (!key && event.keyCode !== Key.ESC && event.keyCode !== Key.ENTER) {
+ return
+ }
+ done(k || false)
+ context.returnFocus()
+ }
+}
diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx
index 2aedba7c..b5d59b76 100644
--- a/web/src/js/components/ProxyApp.jsx
+++ b/web/src/js/components/ProxyApp.jsx
@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom'
import _ from 'lodash'
import { connect } from 'react-redux'
-import { Splitter } from './common.js'
import { connect as wsConnect } from '../ducks/websocket'
import Header from './Header'
import EventLog from './EventLog'
@@ -15,7 +14,6 @@ class ProxyAppMain extends Component {
static childContextTypes = {
returnFocus: PropTypes.func.isRequired,
- location: PropTypes.object.isRequired,
}
static contextTypes = {
@@ -72,7 +70,7 @@ class ProxyAppMain extends Component {
*/
componentDidMount() {
this.focus()
- this.settingsStore.addListener("recalculate", this.onSettingsChange)
+ this.settingsStore.addListener('recalculate', this.onSettingsChange)
}
/**
@@ -81,7 +79,7 @@ class ProxyAppMain extends Component {
* @todo stop listening to window's key events
*/
componentWillUnmount() {
- this.settingsStore.removeListener("recalculate", this.onSettingsChange)
+ this.settingsStore.removeListener('recalculate', this.onSettingsChange)
}
/**
@@ -95,10 +93,7 @@ class ProxyAppMain extends Component {
* @todo use props
*/
getChildContext() {
- return {
- returnFocus: this.focus,
- location: this.props.location
- }
+ return { returnFocus: this.focus }
}
/**
@@ -112,19 +107,20 @@ class ProxyAppMain extends Component {
/**
* @todo move to actions
+ * @todo bind on window
*/
onKeyDown(e) {
let name = null
switch (e.keyCode) {
case Key.I:
- name = "intercept"
+ name = 'intercept'
break
case Key.L:
- name = "search"
+ name = 'search'
break
case Key.H:
- name = "highlight"
+ name = 'highlight'
break
default:
let main = this.refs.view
@@ -139,7 +135,7 @@ class ProxyAppMain extends Component {
if (name) {
const headerComponent = this.refs.header
- headerComponent.setState({ active: Header.entries.MainMenu }, () => {
+ headerComponent.setState({ active: Header.entries[0] }, () => {
headerComponent.refs.active.refs[name].select()
})
}
@@ -156,12 +152,11 @@ class ProxyAppMain extends Component {
<Header ref="header" settings={settings} updateLocation={this.updateLocation} query={query} />
{React.cloneElement(
children,
- { ref: "view", location, query, updateLocation: this.updateLocation }
+ { ref: 'view', location, query, updateLocation: this.updateLocation }
)}
- {showEventLog && [
- <Splitter key="splitter" axis="y"/>,
+ {showEventLog && (
<EventLog key="eventlog"/>
- ]}
+ )}
<Footer settings={settings}/>
</div>
)
diff --git a/web/src/js/components/ValueEditor.jsx b/web/src/js/components/ValueEditor.jsx
new file mode 100755
index 00000000..0316924f
--- /dev/null
+++ b/web/src/js/components/ValueEditor.jsx
@@ -0,0 +1,36 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import ValidateEditor from './ValueEditor/ValidateEditor'
+
+export default class ValueEditor extends Component {
+
+ static contextTypes = {
+ returnFocus: PropTypes.func,
+ }
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ inline: PropTypes.bool,
+ }
+
+ constructor(props) {
+ super(props)
+ this.focus = this.focus.bind(this)
+ }
+
+ render() {
+ var tag = this.props.inline ? "span" : 'div'
+ return (
+ <ValidateEditor
+ {...this.props}
+ onStop={() => this.context.returnFocus()}
+ tag={tag}
+ />
+ )
+ }
+
+ focus() {
+ ReactDOM.findDOMNode(this).focus();
+ }
+}
diff --git a/web/src/js/components/ValueEditor/EditorBase.jsx b/web/src/js/components/ValueEditor/EditorBase.jsx
new file mode 100755
index 00000000..e737d2af
--- /dev/null
+++ b/web/src/js/components/ValueEditor/EditorBase.jsx
@@ -0,0 +1,166 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import {Key} from '../../utils.js'
+
+export default class EditorBase extends Component {
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ contentToHtml: PropTypes.func,
+ nodeToContent: PropTypes.func,
+ onStop: PropTypes.func,
+ submitOnEnter: PropTypes.bool,
+ className: PropTypes.string,
+ tag: PropTypes.string,
+ }
+
+ static defaultProps = {
+ contentToHtml: content => _.escape(content),
+ nodeToContent: node => node.textContent,
+ submitOnEnter: true,
+ className: '',
+ tag: 'div',
+ onStop: _.noop,
+ onMouseDown: _.noop,
+ onBlur: _.noop,
+ onInput: _.noop,
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {editable: false}
+
+ this.onPaste = this.onPaste.bind(this)
+ this.onMouseDown = this.onMouseDown.bind(this)
+ this.onMouseUp = this.onMouseUp.bind(this)
+ this.onFocus = this.onFocus.bind(this)
+ this.onClick = this.onClick.bind(this)
+ this.stop = this.stop.bind(this)
+ this.onBlur = this.onBlur.bind(this)
+ this.reset = this.reset.bind(this)
+ this.onKeyDown = this.onKeyDown.bind(this)
+ this.onInput = this.onInput.bind(this)
+ }
+
+ stop() {
+ // a stop would cause a blur as a side-effect.
+ // but a blur event must trigger a stop as well.
+ // to fix this, make stop = blur and do the actual stop in the onBlur handler.
+ ReactDOM.findDOMNode(this).blur()
+ this.props.onStop()
+ }
+
+ render() {
+ return (
+ <this.props.tag
+ {...this.props}
+ tabIndex="0"
+ className={`inline-input ${this.props.className}`}
+ contentEditable={this.state.editable || undefined}
+ onFocus={this.onFocus}
+ onMouseDown={this.onMouseDown}
+ onClick={this.onClick}
+ onBlur={this.onBlur}
+ onKeyDown={this.onKeyDown}
+ onInput={this.onInput}
+ onPaste={this.onPaste}
+ dangerouslySetInnerHTML={{ __html: this.props.contentToHtml(this.props.content) }}
+ />
+ )
+ }
+
+ onPaste(e) {
+ e.preventDefault()
+ var content = e.clipboardData.getData('text/plain')
+ document.execCommand('insertHTML', false, content)
+ }
+
+ onMouseDown(e) {
+ this._mouseDown = true
+ window.addEventListener('mouseup', this.onMouseUp)
+ this.props.onMouseDown(e)
+ }
+
+ onMouseUp() {
+ if (this._mouseDown) {
+ this._mouseDown = false
+ window.removeEventListener('mouseup', this.onMouseUp)
+ }
+ }
+
+ onClick(e) {
+ this.onMouseUp()
+ this.onFocus(e)
+ }
+
+ onFocus(e) {
+ if (this._mouseDown || this._ignore_events || this.state.editable) {
+ return
+ }
+
+ // contenteditable in FireFox is more or less broken.
+ // - we need to blur() and then focus(), otherwise the caret is not shown.
+ // - blur() + focus() == we need to save the caret position before
+ // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint
+ const sel = window.getSelection()
+ let range
+ if (sel.rangeCount > 0) {
+ range = sel.getRangeAt(0)
+ } else if (document.caretPositionFromPoint && e.clientX && e.clientY) {
+ const pos = document.caretPositionFromPoint(e.clientX, e.clientY)
+ range = document.createRange()
+ range.setStart(pos.offsetNode, pos.offset)
+ } else if (document.caretRangeFromPoint && e.clientX && e.clientY) {
+ range = document.caretRangeFromPoint(e.clientX, e.clientY)
+ } else {
+ range = document.createRange()
+ range.selectNodeContents(ReactDOM.findDOMNode(this))
+ }
+
+ this._ignore_events = true
+ this.setState({ editable: true }, () => {
+ const node = ReactDOM.findDOMNode(this)
+ node.blur()
+ node.focus()
+ this._ignore_events = false
+ })
+ }
+
+ onBlur(e) {
+ if (this._ignore_events) {
+ return
+ }
+ window.getSelection().removeAllRanges() //make sure that selection is cleared on blur
+ this.setState({ editable: false })
+ this.props.onDone(this.props.nodeToContent(ReactDOM.findDOMNode(this)))
+ this.props.onBlur(e)
+ }
+
+ reset() {
+ ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content)
+ }
+
+ onKeyDown(e) {
+ e.stopPropagation()
+ switch (e.keyCode) {
+ case Key.ESC:
+ e.preventDefault()
+ this.reset()
+ this.stop()
+ break
+ case Key.ENTER:
+ if (this.props.submitOnEnter && !e.shiftKey) {
+ e.preventDefault()
+ this.stop()
+ }
+ break
+ default:
+ break
+ }
+ }
+
+ onInput() {
+ this.props.onInput(this.props.nodeToContent(ReactDOM.findDOMNode(this)))
+ }
+}
diff --git a/web/src/js/components/ValueEditor/ValidateEditor.jsx b/web/src/js/components/ValueEditor/ValidateEditor.jsx
new file mode 100755
index 00000000..2f362986
--- /dev/null
+++ b/web/src/js/components/ValueEditor/ValidateEditor.jsx
@@ -0,0 +1,58 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import EditorBase from './EditorBase'
+
+export default class ValidateEditor extends Component {
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ onInput: PropTypes.func,
+ isValid: PropTypes.func,
+ className: PropTypes.string,
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = { currentContent: props.content }
+ this.onInput = this.onInput.bind(this)
+ this.onDone = this.onDone.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({ currentContent: nextProps.content })
+ }
+
+ onInput(currentContent) {
+ this.setState({ currentContent })
+ this.props.onInput && this.props.onInput(currentContent)
+ }
+
+ onDone(content) {
+ if (this.props.isValid && !this.props.isValid(content)) {
+ this.refs.editor.reset()
+ content = this.props.content
+ }
+ this.props.onDone(content)
+ }
+
+ render() {
+ let className = this.props.className || ''
+ if (this.props.isValid) {
+ if (this.props.isValid(this.state.currentContent)) {
+ className += ' has-success'
+ } else {
+ className += ' has-warning'
+ }
+ }
+ return (
+ <EditorBase
+ {...this.props}
+ ref="editor"
+ className={className}
+ onDone={this.onDone}
+ onInput={this.onInput}
+ />
+ )
+ }
+}
diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js
deleted file mode 100644
index 1497e15d..00000000
--- a/web/src/js/components/common.js
+++ /dev/null
@@ -1,173 +0,0 @@
-import React from "react"
-import ReactDOM from "react-dom"
-import {Key} from "../utils.js";
-import _ from "lodash"
-
-export var Splitter = React.createClass({
- getDefaultProps: function () {
- return {
- axis: "x"
- };
- },
- getInitialState: function () {
- return {
- applied: false,
- startX: false,
- startY: false
- };
- },
- onMouseDown: function (e) {
- this.setState({
- startX: e.pageX,
- startY: e.pageY
- });
- window.addEventListener("mousemove", this.onMouseMove);
- window.addEventListener("mouseup", this.onMouseUp);
- // Occasionally, only a dragEnd event is triggered, but no mouseUp.
- window.addEventListener("dragend", this.onDragEnd);
- },
- onDragEnd: function () {
- ReactDOM.findDOMNode(this).style.transform = "";
- window.removeEventListener("dragend", this.onDragEnd);
- window.removeEventListener("mouseup", this.onMouseUp);
- window.removeEventListener("mousemove", this.onMouseMove);
- },
- onMouseUp: function (e) {
- this.onDragEnd();
-
- var node = ReactDOM.findDOMNode(this);
- var prev = node.previousElementSibling;
- var next = node.nextElementSibling;
-
- var dX = e.pageX - this.state.startX;
- var dY = e.pageY - this.state.startY;
- var flexBasis;
- if (this.props.axis === "x") {
- flexBasis = prev.offsetWidth + dX;
- } else {
- flexBasis = prev.offsetHeight + dY;
- }
-
- prev.style.flex = "0 0 " + Math.max(0, flexBasis) + "px";
- next.style.flex = "1 1 auto";
-
- this.setState({
- applied: true
- });
- this.onResize();
- },
- onMouseMove: function (e) {
- var dX = 0, dY = 0;
- if (this.props.axis === "x") {
- dX = e.pageX - this.state.startX;
- } else {
- dY = e.pageY - this.state.startY;
- }
- ReactDOM.findDOMNode(this).style.transform = "translate(" + dX + "px," + dY + "px)";
- },
- onResize: function () {
- // Trigger a global resize event. This notifies components that employ virtual scrolling
- // that their viewport may have changed.
- window.setTimeout(function () {
- window.dispatchEvent(new CustomEvent("resize"));
- }, 1);
- },
- reset: function (willUnmount) {
- if (!this.state.applied) {
- return;
- }
- var node = ReactDOM.findDOMNode(this);
- var prev = node.previousElementSibling;
- var next = node.nextElementSibling;
-
- prev.style.flex = "";
- next.style.flex = "";
-
- if (!willUnmount) {
- this.setState({
- applied: false
- });
- }
- this.onResize();
- },
- componentWillUnmount: function () {
- this.reset(true);
- },
- render: function () {
- var className = "splitter";
- if (this.props.axis === "x") {
- className += " splitter-x";
- } else {
- className += " splitter-y";
- }
- return (
- <div className={className}>
- <div onMouseDown={this.onMouseDown} draggable="true"></div>
- </div>
- );
- }
-});
-
-export const ToggleButton = ({checked, onToggle, text}) =>
- <div className={"btn btn-toggle " + (checked ? "btn-primary" : "btn-default")} onClick={onToggle}>
- <i className={"fa fa-fw " + (checked ? "fa-check-square-o" : "fa-square-o")}/>
- &nbsp;
- {text}
- </div>;
-
-ToggleButton.propTypes = {
- checked: React.PropTypes.bool.isRequired,
- onToggle: React.PropTypes.func.isRequired,
- text: React.PropTypes.string.isRequired
-};
-
-export const Button = ({onClick, text, icon}) =>
- <div className={"btn btn-default"} onClick={onClick}>
- <i className={"fa fa-fw " + icon}/>
- &nbsp;
- {text}
- </div>;
-
-Button.propTypes = {
- onClick: React.PropTypes.func.isRequired,
- text: React.PropTypes.string.isRequired
-};
-
-export class ToggleInputButton extends React.Component {
- constructor(props) {
- super(props);
- this.state = {txt: props.txt};
- }
-
- render() {
- return (
- <div className="input-group toggle-input-btn">
- <span
- className="input-group-btn"
- onClick={() => this.props.onToggleChanged(this.state.txt)}>
- <div className={"btn " + (this.props.checked ? "btn-primary" : "btn-default")}>
- <span className={"fa " + (this.props.checked ? "fa-check-square-o" : "fa-square-o")}/>
- &nbsp;{this.props.name}
- </div>
- </span>
- <input
- className="form-control"
- placeholder={this.props.placeholder}
- disabled={this.props.checked}
- value={this.state.txt}
- type={this.props.inputType}
- onChange={e => this.setState({txt: e.target.value})}
- onKeyDown={e => {if (e.keyCode === Key.ENTER) this.props.onToggleChanged(this.state.txt); e.stopPropagation()}}/>
- </div>
- );
- }
-}
-
-ToggleInputButton.propTypes = {
- name: React.PropTypes.string.isRequired,
- txt: React.PropTypes.string.isRequired,
- onToggleChanged: React.PropTypes.func.isRequired
-};
-
-
-
diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx
new file mode 100644
index 00000000..cc2fe9dd
--- /dev/null
+++ b/web/src/js/components/common/Button.jsx
@@ -0,0 +1,16 @@
+import React, { PropTypes } from 'react'
+
+Button.propTypes = {
+ onClick: PropTypes.func.isRequired,
+ text: PropTypes.string.isRequired
+}
+
+export default function Button({ onClick, text, icon }) {
+ return (
+ <div className={"btn btn-default"} onClick={onClick}>
+ <i className={"fa fa-fw " + icon}/>
+ &nbsp;
+ {text}
+ </div>
+ )
+}
diff --git a/web/src/js/components/common/Splitter.jsx b/web/src/js/components/common/Splitter.jsx
new file mode 100644
index 00000000..9d22b6fd
--- /dev/null
+++ b/web/src/js/components/common/Splitter.jsx
@@ -0,0 +1,99 @@
+import React, { Component } from 'react'
+import ReactDOM from 'react-dom'
+import classnames from 'classnames'
+
+export default class Splitter extends Component {
+
+ static defaultProps = { axis: 'x' }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { applied: false, startX: false, startY: false }
+
+ this.onMouseMove = this.onMouseMove.bind(this)
+ this.onMouseUp = this.onMouseUp.bind(this)
+ this.onDragEnd = this.onDragEnd.bind(this)
+ }
+
+ onMouseDown(e) {
+ this.setState({ startX: e.pageX, startY: e.pageY })
+
+ window.addEventListener('mousemove', this.onMouseMove)
+ window.addEventListener('mouseup', this.onMouseUp)
+ // Occasionally, only a dragEnd event is triggered, but no mouseUp.
+ window.addEventListener('dragend', this.onDragEnd)
+ }
+
+ onDragEnd() {
+ ReactDOM.findDOMNode(this).style.transform = ''
+
+ window.removeEventListener('dragend', this.onDragEnd)
+ window.removeEventListener('mouseup', this.onMouseUp)
+ window.removeEventListener('mousemove', this.onMouseMove)
+ }
+
+ onMouseUp(e) {
+ this.onDragEnd()
+
+ const node = ReactDOM.findDOMNode(this)
+ const prev = node.previousElementSibling
+
+ let flexBasis = prev.offsetHeight + e.pageY - this.state.startY
+
+ if (this.props.axis === 'x') {
+ flexBasis = prev.offsetWidth + e.pageX - this.state.startX
+ }
+
+ prev.style.flex = `0 0 ${Math.max(0, flexBasis)}px`
+ node.nextElementSibling.style.flex = '1 1 auto'
+
+ this.setState({ applied: true })
+ this.onResize()
+ }
+
+ onMouseMove(e) {
+ let dX = 0
+ let dY = 0
+ if (this.props.axis === 'x') {
+ dX = e.pageX - this.state.startX
+ } else {
+ dY = e.pageY - this.state.startY
+ }
+ ReactDOM.findDOMNode(this).style.transform = `translate(${dX}px, ${dY}px)`
+ }
+
+ onResize() {
+ // Trigger a global resize event. This notifies components that employ virtual scrolling
+ // that their viewport may have changed.
+ window.setTimeout(() => window.dispatchEvent(new CustomEvent('resize')), 1)
+ }
+
+ reset(willUnmount) {
+ if (!this.state.applied) {
+ return
+ }
+
+ const node = ReactDOM.findDOMNode(this)
+
+ node.previousElementSibling.style.flex = ''
+ node.nextElementSibling.style.flex = ''
+
+ if (!willUnmount) {
+ this.setState({ applied: false })
+ }
+ this.onResize()
+ }
+
+ componentWillUnmount() {
+ this.reset(true)
+ }
+
+ render() {
+ return (
+ <div className={classnames('splitter', this.props.axis === 'x' ? 'splitter-x' : 'splitter-y')}>
+ <div onMouseDown={this.onMouseDown} draggable="true"></div>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/common/ToggleButton.jsx b/web/src/js/components/common/ToggleButton.jsx
new file mode 100644
index 00000000..6027728b
--- /dev/null
+++ b/web/src/js/components/common/ToggleButton.jsx
@@ -0,0 +1,17 @@
+import React, { PropTypes } from 'react'
+
+ToggleButton.propTypes = {
+ checked: PropTypes.bool.isRequired,
+ onToggle: PropTypes.func.isRequired,
+ text: PropTypes.string.isRequired
+}
+
+export default function ToggleButton({ checked, onToggle, text }) {
+ return (
+ <div className={"btn btn-toggle " + (checked ? "btn-primary" : "btn-default")} onClick={onToggle}>
+ <i className={"fa fa-fw " + (checked ? "fa-check-square-o" : "fa-square-o")}/>
+ &nbsp;
+ {text}
+ </div>
+ )
+}
diff --git a/web/src/js/components/common/ToggleInputButton.jsx b/web/src/js/components/common/ToggleInputButton.jsx
new file mode 100644
index 00000000..25d620ae
--- /dev/null
+++ b/web/src/js/components/common/ToggleInputButton.jsx
@@ -0,0 +1,52 @@
+import React, { Component, PropTypes } from 'react'
+import classnames from 'classnames'
+import { Key } from '../../utils'
+
+export default class ToggleInputButton extends Component {
+
+ static propTypes = {
+ name: PropTypes.string.isRequired,
+ txt: PropTypes.string.isRequired,
+ onToggleChanged: PropTypes.func.isRequired
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = { txt: props.txt }
+ }
+
+ onChange(e) {
+ this.setState({ txt: e.target.value })
+ }
+
+ onKeyDown(e) {
+ e.stopPropagation()
+ if (e.keyCode === Key.ENTER) {
+ this.props.onToggleChanged(this.state.txt)
+ }
+ }
+
+ render() {
+ return (
+ <div className="input-group toggle-input-btn">
+ <span className="input-group-btn"
+ onClick={() => this.props.onToggleChanged(this.state.txt)}>
+ <div className={classnames('btn', this.props.checked ? 'btn-primary' : 'btn-default')}>
+ <span className={classnames('fa', this.props.checked ? 'fa-check-square-o' : 'fa-square-o')}/>
+ &nbsp;
+ {this.props.name}
+ </div>
+ </span>
+ <input
+ className="form-control"
+ placeholder={this.props.placeholder}
+ disabled={this.props.checked}
+ value={this.state.txt}
+ type={this.props.inputType}
+ onChange={e => this.onChange(e)}
+ onKeyDown={e => this.onKeyDown(e)}
+ />
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/editor.js b/web/src/js/components/editor.js
deleted file mode 100644
index eed2f7c6..00000000
--- a/web/src/js/components/editor.js
+++ /dev/null
@@ -1,238 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import {Key} from "../utils.js";
-
-var contentToHtml = function (content) {
- return _.escape(content);
-};
-var nodeToContent = function (node) {
- return node.textContent;
-};
-
-/*
- Basic Editor Functionality
- */
-var EditorBase = React.createClass({
- propTypes: {
- content: React.PropTypes.string.isRequired,
- onDone: React.PropTypes.func.isRequired,
- contentToHtml: React.PropTypes.func,
- nodeToContent: React.PropTypes.func, // content === nodeToContent( Node<innerHTML=contentToHtml(content)> )
- onStop: React.PropTypes.func,
- submitOnEnter: React.PropTypes.bool,
- className: React.PropTypes.string,
- tag: React.PropTypes.string
- },
- getDefaultProps: function () {
- return {
- contentToHtml: contentToHtml,
- nodeToContent: nodeToContent,
- submitOnEnter: true,
- className: "",
- tag: "div"
- };
- },
- getInitialState: function () {
- return {
- editable: false
- };
- },
- render: function () {
- var className = "inline-input " + this.props.className;
- var html = {__html: this.props.contentToHtml(this.props.content)};
- var Tag = this.props.tag;
- return <Tag
- {...this.props}
- tabIndex="0"
- className={className}
- contentEditable={this.state.editable || undefined } // workaround: use undef instead of false to remove attr
- onFocus={this.onFocus}
- onMouseDown={this.onMouseDown}
- onClick={this.onClick}
- onBlur={this._stop}
- onKeyDown={this.onKeyDown}
- onInput={this.onInput}
- onPaste={this.onPaste}
- dangerouslySetInnerHTML={html}
- />;
- },
- onPaste: function (e) {
- e.preventDefault();
- var content = e.clipboardData.getData("text/plain");
- document.execCommand("insertHTML", false, content);
- },
- onMouseDown: function (e) {
- this._mouseDown = true;
- window.addEventListener("mouseup", this.onMouseUp);
- this.props.onMouseDown && this.props.onMouseDown(e);
- },
- onMouseUp: function () {
- if (this._mouseDown) {
- this._mouseDown = false;
- window.removeEventListener("mouseup", this.onMouseUp)
- }
- },
- onClick: function (e) {
- this.onMouseUp();
- this.onFocus(e);
- },
- onFocus: function (e) {
- console.log("onFocus", this._mouseDown, this._ignore_events, this.state.editable);
- if (this._mouseDown || this._ignore_events || this.state.editable) {
- return;
- }
-
- //contenteditable in FireFox is more or less broken.
- // - we need to blur() and then focus(), otherwise the caret is not shown.
- // - blur() + focus() == we need to save the caret position before
- // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint
- var sel = window.getSelection();
- var range;
- if (sel.rangeCount > 0) {
- range = sel.getRangeAt(0);
- } else if (document.caretPositionFromPoint && e.clientX && e.clientY) {
- var pos = document.caretPositionFromPoint(e.clientX, e.clientY);
- range = document.createRange();
- range.setStart(pos.offsetNode, pos.offset);
- } else if (document.caretRangeFromPoint && e.clientX && e.clientY) {
- range = document.caretRangeFromPoint(e.clientX, e.clientY);
- } else {
- range = document.createRange();
- range.selectNodeContents(ReactDOM.findDOMNode(this));
- }
-
- this._ignore_events = true;
- this.setState({editable: true}, function () {
- var node = ReactDOM.findDOMNode(this);
- node.blur();
- node.focus();
- this._ignore_events = false;
- //sel.removeAllRanges();
- //sel.addRange(range);
-
-
- });
- },
- stop: function () {
- // a stop would cause a blur as a side-effect.
- // but a blur event must trigger a stop as well.
- // to fix this, make stop = blur and do the actual stop in the onBlur handler.
- ReactDOM.findDOMNode(this).blur();
- this.props.onStop && this.props.onStop();
- },
- _stop: function (e) {
- if (this._ignore_events) {
- return;
- }
- console.log("_stop", _.extend({}, e));
- window.getSelection().removeAllRanges(); //make sure that selection is cleared on blur
- var node = ReactDOM.findDOMNode(this);
- var content = this.props.nodeToContent(node);
- this.setState({editable: false});
- this.props.onDone(content);
- this.props.onBlur && this.props.onBlur(e);
- },
- reset: function () {
- ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content);
- },
- onKeyDown: function (e) {
- e.stopPropagation();
- switch (e.keyCode) {
- case Key.ESC:
- e.preventDefault();
- this.reset();
- this.stop();
- break;
- case Key.ENTER:
- if (this.props.submitOnEnter && !e.shiftKey) {
- e.preventDefault();
- this.stop();
- }
- break;
- default:
- break;
- }
- },
- onInput: function () {
- var node = ReactDOM.findDOMNode(this);
- var content = this.props.nodeToContent(node);
- this.props.onInput && this.props.onInput(content);
- }
-});
-
-/*
- Add Validation to EditorBase
- */
-var ValidateEditor = React.createClass({
- propTypes: {
- content: React.PropTypes.string.isRequired,
- onDone: React.PropTypes.func.isRequired,
- onInput: React.PropTypes.func,
- isValid: React.PropTypes.func,
- className: React.PropTypes.string,
- },
- getInitialState: function () {
- return {
- currentContent: this.props.content
- };
- },
- componentWillReceiveProps: function () {
- this.setState({currentContent: this.props.content});
- },
- onInput: function (content) {
- this.setState({currentContent: content});
- this.props.onInput && this.props.onInput(content);
- },
- render: function () {
- var className = this.props.className || "";
- if (this.props.isValid) {
- if (this.props.isValid(this.state.currentContent)) {
- className += " has-success";
- } else {
- className += " has-warning"
- }
- }
- return <EditorBase
- {...this.props}
- ref="editor"
- className={className}
- onDone={this.onDone}
- onInput={this.onInput}
- />;
- },
- onDone: function (content) {
- if (this.props.isValid && !this.props.isValid(content)) {
- this.refs.editor.reset();
- content = this.props.content;
- }
- this.props.onDone(content);
- }
-});
-
-/*
- Text Editor with mitmweb-specific convenience features
- */
-export var ValueEditor = React.createClass({
- contextTypes: {
- returnFocus: React.PropTypes.func
- },
- propTypes: {
- content: React.PropTypes.string.isRequired,
- onDone: React.PropTypes.func.isRequired,
- inline: React.PropTypes.bool,
- },
- render: function () {
- var tag = this.props.inline ? "span" : "div";
- return <ValidateEditor
- {...this.props}
- onStop={this.onStop}
- tag={tag}
- />;
- },
- focus: function () {
- ReactDOM.findDOMNode(this).focus();
- },
- onStop: function () {
- this.context.returnFocus();
- }
-}); \ No newline at end of file
diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js
deleted file mode 100644
index cbac9a75..00000000
--- a/web/src/js/components/flowview/contentview.js
+++ /dev/null
@@ -1,267 +0,0 @@
-import React from "react";
-import _ from "lodash";
-
-import {MessageUtils} from "../../flow/utils.js";
-import {formatSize} from "../../utils.js";
-
-var ViewImage = React.createClass({
- propTypes: {
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
- },
- statics: {
- regex: /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i,
- matches: function (message) {
- return ViewImage.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 ContentLoader = React.createClass({
- propTypes: {
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
- },
- 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 React.cloneElement(this.props.children, {
- content: this.state.content
- })
- }
-});
-
-var ViewRaw = React.createClass({
- propTypes: {
- content: React.PropTypes.string.isRequired,
- },
- statics: {
- textView: true,
- matches: function (message) {
- return true;
- }
- },
- render: function () {
- return <pre>{this.props.content}</pre>;
- }
-});
-
-var ViewJSON = React.createClass({
- propTypes: {
- content: React.PropTypes.string.isRequired,
- },
- statics: {
- textView: true,
- regex: /^application\/json$/i,
- matches: function (message) {
- return ViewJSON.regex.test(MessageUtils.getContentType(message));
- }
- },
- render: function () {
- var json = this.props.content;
- try {
- json = JSON.stringify(JSON.parse(json), null, 2);
- } catch (e) {
- // @noop
- }
- return <pre>{json}</pre>;
- }
-});
-
-var ViewAuto = React.createClass({
- propTypes: {
- message: React.PropTypes.object.isRequired,
- flow: React.PropTypes.object.isRequired,
- },
- 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 { message, flow } = this.props
- var View = ViewAuto.findView(this.props.message);
- if (View.textView) {
- return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader>
- } else {
- return <View message={message} flow={flow} />
- }
- }
-});
-
-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 = 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 { flow, message } = this.props
- 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.textView ? (
- <ContentLoader flow={flow} message={message}><this.state.View content="" /></ContentLoader>
- ) : (
- <this.state.View flow={flow} message={message} />
- )}
- <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>;
- }
-});
-
-export default ContentView;
diff --git a/web/src/js/components/flowview/details.js b/web/src/js/components/flowview/details.js
deleted file mode 100644
index 45fe1292..00000000
--- a/web/src/js/components/flowview/details.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import React from "react";
-import _ from "lodash";
-
-import {formatTimeStamp, formatTimeDelta} from "../../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 = formatTimeStamp(this.props.t);
-
- var delta;
- if (this.props.deltaTo) {
- delta = 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>
- );
- }
-});
-
-export default 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
deleted file mode 100644
index 7f5e9768..00000000
--- a/web/src/js/components/flowview/index.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import React from "react";
-
-import Nav from "./nav.js";
-import {Request, Response, Error} from "./messages.js";
-import Details from "./details.js";
-import Prompt from "../prompt.js";
-
-
-var allTabs = {
- request: Request,
- response: Response,
- error: Error,
- details: Details
-};
-
-var FlowView = React.createClass({
- getInitialState: function () {
- return {
- prompt: false
- };
- },
- 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.props.tab);
- // 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.props.updateLocation(`/flows/${this.props.flow.id}/${panel}`);
- },
- promptEdit: function () {
- var options;
- switch (this.props.tab) {
- case "request":
- options = [
- "method",
- "url",
- {text: "http version", key: "v"},
- "header"
- /*, "content"*/];
- break;
- case "response":
- options = [
- {text: "http version", key: "v"},
- "code",
- "message",
- "header"
- /*, "content"*/];
- break;
- case "details":
- return;
- default:
- throw "Unknown tab for edit: " + this.props.tab;
- }
-
- this.setState({
- prompt: {
- done: function (k) {
- this.setState({prompt: false});
- if (k) {
- this.refs.tab.edit(k);
- }
- }.bind(this),
- options: options
- }
- });
- },
- render: function () {
- var flow = this.props.flow;
- var tabs = this.getTabs(flow);
- var active = this.props.tab;
-
- if (tabs.indexOf(active) < 0) {
- if (active === "response" && flow.error) {
- active = "error";
- } else if (active === "error" && flow.response) {
- active = "response";
- } else {
- active = tabs[0];
- }
- }
-
- var prompt = null;
- if (this.state.prompt) {
- prompt = <Prompt {...this.state.prompt}/>;
- }
-
- 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 ref="tab" flow={flow}/>
- {prompt}
- </div>
- );
- }
-});
-
-export default FlowView;
diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js
deleted file mode 100644
index 2885b3b1..00000000
--- a/web/src/js/components/flowview/messages.js
+++ /dev/null
@@ -1,320 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import _ from "lodash";
-
-import {FlowActions} from "../../actions.js";
-import {RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion} from "../../flow/utils.js";
-import {Key, formatTimeStamp} from "../../utils.js";
-import ContentView from "./contentview.js";
-import {ValueEditor} from "../editor.js";
-
-var Headers = React.createClass({
- propTypes: {
- onChange: React.PropTypes.func.isRequired,
- message: React.PropTypes.object.isRequired
- },
- onChange: function (row, col, val) {
- var nextHeaders = _.cloneDeep(this.props.message.headers);
- nextHeaders[row][col] = val;
- if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
- // do not delete last row
- if (nextHeaders.length === 1) {
- nextHeaders[0][0] = "Name";
- nextHeaders[0][1] = "Value";
- } else {
- nextHeaders.splice(row, 1);
- // manually move selection target if this has been the last row.
- if (row === nextHeaders.length) {
- this._nextSel = (row - 1) + "-value";
- }
- }
- }
- this.props.onChange(nextHeaders);
- },
- edit: function () {
- this.refs["0-key"].focus();
- },
- onTab: function (row, col, e) {
- var headers = this.props.message.headers;
- if (row === headers.length - 1 && col === 1) {
- e.preventDefault();
-
- var nextHeaders = _.cloneDeep(this.props.message.headers);
- nextHeaders.push(["Name", "Value"]);
- this.props.onChange(nextHeaders);
- this._nextSel = (row + 1) + "-key";
- }
- },
- componentDidUpdate: function () {
- if (this._nextSel && this.refs[this._nextSel]) {
- this.refs[this._nextSel].focus();
- this._nextSel = undefined;
- }
- },
- onRemove: function (row, col, e) {
- if (col === 1) {
- e.preventDefault();
- this.refs[row + "-key"].focus();
- } else if (row > 0) {
- e.preventDefault();
- this.refs[(row - 1) + "-value"].focus();
- }
- },
- render: function () {
-
- var rows = this.props.message.headers.map(function (header, i) {
-
- var kEdit = <HeaderEditor
- ref={i + "-key"}
- content={header[0]}
- onDone={this.onChange.bind(null, i, 0)}
- onRemove={this.onRemove.bind(null, i, 0)}
- onTab={this.onTab.bind(null, i, 0)}/>;
- var vEdit = <HeaderEditor
- ref={i + "-value"}
- content={header[1]}
- onDone={this.onChange.bind(null, i, 1)}
- onRemove={this.onRemove.bind(null, i, 1)}
- onTab={this.onTab.bind(null, i, 1)}/>;
- return (
- <tr key={i}>
- <td className="header-name">{kEdit}:</td>
- <td className="header-value">{vEdit}</td>
- </tr>
- );
- }.bind(this));
- return (
- <table className="header-table">
- <tbody>
- {rows}
- </tbody>
- </table>
- );
- }
-});
-
-var HeaderEditor = React.createClass({
- render: function () {
- return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>;
- },
- focus: function () {
- ReactDOM.findDOMNode(this).focus();
- },
- onKeyDown: function (e) {
- switch (e.keyCode) {
- case Key.BACKSPACE:
- var s = window.getSelection().getRangeAt(0);
- if (s.startOffset === 0 && s.endOffset === 0) {
- this.props.onRemove(e);
- }
- break;
- case Key.TAB:
- if (!e.shiftKey) {
- this.props.onTab(e);
- }
- break;
- }
- }
-});
-
-var RequestLine = React.createClass({
- render: function () {
- var flow = this.props.flow;
- var url = RequestUtils.pretty_url(flow.request);
- var httpver = flow.request.http_version;
-
- return <div className="first-line request-line">
- <ValueEditor
- ref="method"
- content={flow.request.method}
- onDone={this.onMethodChange}
- inline/>
- &nbsp;
- <ValueEditor
- ref="url"
- content={url}
- onDone={this.onUrlChange}
- isValid={this.isValidUrl}
- inline/>
- &nbsp;
- <ValueEditor
- ref="httpVersion"
- content={httpver}
- onDone={this.onHttpVersionChange}
- isValid={isValidHttpVersion}
- inline/>
- </div>
- },
- isValidUrl: function (url) {
- var u = parseUrl(url);
- return !!u.host;
- },
- onMethodChange: function (nextMethod) {
- FlowActions.update(
- this.props.flow,
- {request: {method: nextMethod}}
- );
- },
- onUrlChange: function (nextUrl) {
- var props = parseUrl(nextUrl);
- props.path = props.path || "";
- FlowActions.update(
- this.props.flow,
- {request: props}
- );
- },
- onHttpVersionChange: function (nextVer) {
- var ver = parseHttpVersion(nextVer);
- FlowActions.update(
- this.props.flow,
- {request: {http_version: ver}}
- );
- }
-});
-
-var ResponseLine = React.createClass({
- render: function () {
- var flow = this.props.flow;
- var httpver = flow.response.http_version;
- return <div className="first-line response-line">
- <ValueEditor
- ref="httpVersion"
- content={httpver}
- onDone={this.onHttpVersionChange}
- isValid={isValidHttpVersion}
- inline/>
- &nbsp;
- <ValueEditor
- ref="code"
- content={flow.response.status_code + ""}
- onDone={this.onCodeChange}
- isValid={this.isValidCode}
- inline/>
- &nbsp;
- <ValueEditor
- ref="msg"
- content={flow.response.reason}
- onDone={this.onMsgChange}
- inline/>
- </div>;
- },
- isValidCode: function (code) {
- return /^\d+$/.test(code);
- },
- onHttpVersionChange: function (nextVer) {
- var ver = parseHttpVersion(nextVer);
- FlowActions.update(
- this.props.flow,
- {response: {http_version: ver}}
- );
- },
- onMsgChange: function (nextMsg) {
- FlowActions.update(
- this.props.flow,
- {response: {msg: nextMsg}}
- );
- },
- onCodeChange: function (nextCode) {
- nextCode = parseInt(nextCode);
- FlowActions.update(
- this.props.flow,
- {response: {code: nextCode}}
- );
- }
-});
-
-export var Request = React.createClass({
- render: function () {
- var flow = this.props.flow;
- return (
- <section className="request">
- <RequestLine ref="requestLine" flow={flow}/>
- {/*<ResponseLine flow={flow}/>*/}
- <Headers ref="headers" message={flow.request} onChange={this.onHeaderChange}/>
- <hr/>
- <ContentView flow={flow} message={flow.request}/>
- </section>
- );
- },
- edit: function (k) {
- switch (k) {
- case "m":
- this.refs.requestLine.refs.method.focus();
- break;
- case "u":
- this.refs.requestLine.refs.url.focus();
- break;
- case "v":
- this.refs.requestLine.refs.httpVersion.focus();
- break;
- case "h":
- this.refs.headers.edit();
- break;
- default:
- throw "Unimplemented: " + k;
- }
- },
- onHeaderChange: function (nextHeaders) {
- FlowActions.update(this.props.flow, {
- request: {
- headers: nextHeaders
- }
- });
- }
-});
-
-export var Response = React.createClass({
- render: function () {
- var flow = this.props.flow;
- return (
- <section className="response">
- {/*<RequestLine flow={flow}/>*/}
- <ResponseLine ref="responseLine" flow={flow}/>
- <Headers ref="headers" message={flow.response} onChange={this.onHeaderChange}/>
- <hr/>
- <ContentView flow={flow} message={flow.response}/>
- </section>
- );
- },
- edit: function (k) {
- switch (k) {
- case "c":
- this.refs.responseLine.refs.status_code.focus();
- break;
- case "m":
- this.refs.responseLine.refs.msg.focus();
- break;
- case "v":
- this.refs.responseLine.refs.httpVersion.focus();
- break;
- case "h":
- this.refs.headers.edit();
- break;
- default:
- throw "Unimplemented: " + k;
- }
- },
- onHeaderChange: function (nextHeaders) {
- FlowActions.update(this.props.flow, {
- response: {
- headers: nextHeaders
- }
- });
- }
-});
-
-export var Error = React.createClass({
- render: function () {
- var flow = this.props.flow;
- return (
- <section>
- <div className="alert alert-warning">
- {flow.error.msg}
- <div>
- <small>{ formatTimeStamp(flow.error.timestamp) }</small>
- </div>
- </div>
- </section>
- );
- }
-});
diff --git a/web/src/js/components/flowview/nav.js b/web/src/js/components/flowview/nav.js
deleted file mode 100644
index a12fd1fd..00000000
--- a/web/src/js/components/flowview/nav.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from "react";
-
-import {FlowActions} from "../../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={FlowActions.accept.bind(null, flow)} />;
- }
- var revertButton = null;
- if(flow.modified){
- revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={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={FlowActions.delete.bind(null, flow)} />
- <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={FlowActions.duplicate.bind(null, flow)} />
- <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={FlowActions.replay.bind(null, flow)} />
- {acceptButton}
- {revertButton}
- </nav>
- );
- }
-});
-
-export default Nav; \ No newline at end of file
diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js
deleted file mode 100644
index 5ab26b82..00000000
--- a/web/src/js/components/prompt.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import _ from "lodash";
-
-import {Key} from "../utils.js";
-
-var Prompt = React.createClass({
- contextTypes: {
- returnFocus: React.PropTypes.func
- },
- propTypes: {
- options: React.PropTypes.array.isRequired,
- done: React.PropTypes.func.isRequired,
- prompt: React.PropTypes.string
- },
- componentDidMount: function () {
- ReactDOM.findDOMNode(this).focus();
- },
- onKeyDown: function (e) {
- e.stopPropagation();
- e.preventDefault();
- var opts = this.getOptions();
- for (var i = 0; i < opts.length; i++) {
- var k = opts[i].key;
- if (Key[k.toUpperCase()] === e.keyCode) {
- this.done(k);
- return;
- }
- }
- if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) {
- this.done(false);
- }
- },
- onClick: function (e) {
- this.done(false);
- },
- done: function (ret) {
- this.props.done(ret);
- this.context.returnFocus();
- },
- getOptions: function () {
- var opts = [];
-
- var keyTaken = function (k) {
- return _.includes(_.map(opts, "key"), k);
- };
-
- for (var i = 0; i < this.props.options.length; i++) {
- var opt = this.props.options[i];
- if (_.isString(opt)) {
- var str = opt;
- while (str.length > 0 && keyTaken(str[0])) {
- str = str.substr(1);
- }
- opt = {
- text: opt,
- key: str[0]
- };
- }
- if (!opt.text || !opt.key || keyTaken(opt.key)) {
- throw "invalid options";
- } else {
- opts.push(opt);
- }
- }
- return opts;
- },
- render: function () {
- var opts = this.getOptions();
- opts = _.map(opts, function (o) {
- var prefix, suffix;
- var idx = o.text.indexOf(o.key);
- if (idx !== -1) {
- prefix = o.text.substring(0, idx);
- suffix = o.text.substring(idx + 1);
-
- } else {
- prefix = o.text + " (";
- suffix = ")";
- }
- var onClick = function (e) {
- this.done(o.key);
- e.stopPropagation();
- }.bind(this);
- return <span
- key={o.key}
- className="option"
- onClick={onClick}>
- {prefix}
- <strong className="text-primary">{o.key}</strong>{suffix}
- </span>;
- }.bind(this));
- return <div tabIndex="0" onKeyDown={this.onKeyDown} onClick={this.onClick} className="prompt-dialog">
- <div className="prompt-content">
- {this.props.prompt || <strong>Select: </strong> }
- {opts}
- </div>
- </div>;
- }
-});
-
-export default Prompt;