aboutsummaryrefslogtreecommitdiffstats
path: root/web/src
diff options
context:
space:
mode:
authorAldo Cortesi <aldo@corte.si>2016-12-16 10:04:12 +1300
committerGitHub <noreply@github.com>2016-12-16 10:04:12 +1300
commit6b5673e84911f3e2b1599c22c9b4f482a55b9ef1 (patch)
treea6f54fdb9d9be4d6b061a3b30069b330d9325fd5 /web/src
parent78c78ce651478072f3b0a4a7d18f2a8de3147d33 (diff)
parentd854e08653ccee12119266e2cc3f5d6c279341e5 (diff)
downloadmitmproxy-6b5673e84911f3e2b1599c22c9b4f482a55b9ef1.tar.gz
mitmproxy-6b5673e84911f3e2b1599c22c9b4f482a55b9ef1.tar.bz2
mitmproxy-6b5673e84911f3e2b1599c22c9b4f482a55b9ef1.zip
Merge pull request #1845 from mhils/mitmweb-improvements
Mitmweb Improvements
Diffstat (limited to 'web/src')
-rw-r--r--web/src/css/flowtable.less5
-rw-r--r--web/src/css/header.less99
-rw-r--r--web/src/js/components/ContentView/ShowFullContentButton.jsx4
-rw-r--r--web/src/js/components/FlowTable/FlowColumns.jsx12
-rw-r--r--web/src/js/components/FlowView/ToggleEdit.jsx4
-rw-r--r--web/src/js/components/Footer.jsx15
-rw-r--r--web/src/js/components/Header.jsx11
-rw-r--r--web/src/js/components/Header/FileMenu.jsx8
-rw-r--r--web/src/js/components/Header/FilterDocs.jsx2
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx74
-rw-r--r--web/src/js/components/Header/MainMenu.jsx21
-rw-r--r--web/src/js/components/Header/MenuToggle.jsx69
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx118
-rw-r--r--web/src/js/components/Header/ViewMenu.jsx32
-rw-r--r--web/src/js/components/common/Button.jsx18
-rw-r--r--web/src/js/components/common/DocsLink.jsx14
-rw-r--r--web/src/js/ducks/flows.js44
-rw-r--r--web/src/js/ducks/ui/header.js6
-rw-r--r--web/src/js/ducks/ui/keyboard.js57
-rw-r--r--web/src/js/ducks/utils/store.js1
20 files changed, 424 insertions, 190 deletions
diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less
index 1b560eba..e8d3d5af 100644
--- a/web/src/css/flowtable.less
+++ b/web/src/css/flowtable.less
@@ -109,6 +109,9 @@
.fa-pause {
color: @interceptorange;
}
+ .fa-exclamation, .fa-times {
+ color: darkred;
+ }
}
.col-method {
width: 60px;
@@ -125,4 +128,4 @@
td.col-time, td.col-size {
text-align: right;
}
-} \ No newline at end of file
+}
diff --git a/web/src/css/header.less b/web/src/css/header.less
index 4813b933..042d6811 100644
--- a/web/src/css/header.less
+++ b/web/src/css/header.less
@@ -1,24 +1,107 @@
@import (reference) '../../node_modules/bootstrap/less/variables.less';
@import (reference) '../../node_modules/bootstrap/less/mixins/grid.less';
+@menu-height: 85px;
+
header {
- padding-top: 0.5em;
+ padding-top: 6px;
background-color: white;
@separator-color: lighten(grey, 15%);
- .menu {
- padding: 10px;
+ menu {
+ display: block;
+ margin: 0;
+ padding: 0;
border-bottom: solid @separator-color 1px;
+ height: @menu-height;
+ overflow: visible; // search help context laps over.
+ }
+}
+
+@menu-legend-height: 16px;
+@menu-group-hmargin: 3px;
+.menu-group {
+ margin: 0 @menu-group-hmargin;
+ display: inline-block;
+ height: @menu-height;
+ vertical-align: top;
+}
+
+.menu-content {
+ height: @menu-height - @menu-legend-height;
+ text-align: center;
+
+ > .btn {
+ height: @menu-height - @menu-legend-height;
+ text-align: center;
+ margin: 0 1px;
+ padding: 12px 5px;
+ border: none;
+ border-radius: 0;
+ i {
+ font-size: 20px;
+ display: block;
+ margin: 0 auto 5px;
+ }
+ }
+}
+
+
+.menu-entry {
+ text-align: left;
+ height: (@menu-height - @menu-legend-height)/3;
+ line-height: 1;
+ padding: 0.5rem 1rem;
+
+ label {
+ font-size: 1.2rem;
+ font-weight: normal;
+ margin: 0;
+ }
+ input[type=checkbox] {
+ margin: 0 2px;
+ vertical-align: middle;
}
}
-@menu-row-gutter-width: 5px;
-.menu-row {
- .make-row(@menu-row-gutter-width);
+
+.menu-legend {
+ height: @menu-legend-height;
+ text-align: center;
+ font-size: 12px;
+ padding: 0 5px;
+}
+
+.menu-group + .menu-group:before {
+ @space: 10px;
+ margin-left: -@menu-group-hmargin;
+ content: " ";
+ border-left: solid 1px lighten(grey, 40%);
+ margin-top: @space;
+ height: @menu-height - @space*2;
+ position: absolute;
+}
+
+@menu-main-gutter-width: 5px;
+.menu-main {
+ .make-row(@menu-main-gutter-width);
+ padding: 2px 5px;
}
.filter-input {
- .make-sm-column(3, @menu-row-gutter-width);
- margin-bottom:5px;
+ .make-sm-column(5, @menu-main-gutter-width);
+ padding: 2.5px;
+
+ @media (max-width: @screen-xs-max) {
+
+ padding: 2px 2.5px;
+
+ > .form-control, > .input-group-addon, > .input-group-btn > .btn {
+ height: 23.5px;
+ padding: 1px 5px;
+ font-size: 12px;
+ line-height: 1.5;
+ }
+ }
}
.filter-input .popover {
diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx
index cfd96dd8..fd68991e 100644
--- a/web/src/js/components/ContentView/ShowFullContentButton.jsx
+++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx
@@ -16,7 +16,9 @@ function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLi
return (
!showFullContent &&
<div>
- <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()} text="Show full content"/>
+ <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()}>
+ Show full content
+ </Button>
<span className="pull-right"> {visibleLines}/{contentLines} are visible &nbsp; </span>
</div>
)
diff --git a/web/src/js/components/FlowTable/FlowColumns.jsx b/web/src/js/components/FlowTable/FlowColumns.jsx
index 0ff80453..02a4fba1 100644
--- a/web/src/js/components/FlowTable/FlowColumns.jsx
+++ b/web/src/js/components/FlowTable/FlowColumns.jsx
@@ -54,6 +54,15 @@ IconColumn.getIcon = flow => {
}
export function PathColumn({ flow }) {
+
+ let err;
+ if(flow.error){
+ if (flow.error.msg === "Connection killed"){
+ err = <i className="fa fa-fw fa-times pull-right"></i>
+ } else {
+ err = <i className="fa fa-fw fa-exclamation pull-right"></i>
+ }
+ }
return (
<td className="col-path">
{flow.request.is_replay && (
@@ -62,6 +71,7 @@ export function PathColumn({ flow }) {
{flow.intercepted && (
<i className="fa fa-fw fa-pause pull-right"></i>
)}
+ {err}
{RequestUtils.pretty_url(flow.request)}
</td>
)
@@ -109,7 +119,7 @@ export function TimeColumn({ flow }) {
return (
<td className="col-time">
{flow.response ? (
- formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
+ formatTimeDelta(1000 * (flow.response.timestamp_end - flow.server_conn.timestamp_start))
) : (
'...'
)}
diff --git a/web/src/js/components/FlowView/ToggleEdit.jsx b/web/src/js/components/FlowView/ToggleEdit.jsx
index 9016348e..6a691a3d 100644
--- a/web/src/js/components/FlowView/ToggleEdit.jsx
+++ b/web/src/js/components/FlowView/ToggleEdit.jsx
@@ -14,11 +14,11 @@ function ToggleEdit({ isEdit, startEdit, stopEdit, flow, modifiedFlow }) {
return (
<div className="edit-flow-container">
{isEdit ?
- <a className="edit-flow" onClick={() => stopEdit(flow, modifiedFlow)}>
+ <a className="edit-flow" title="Finish Edit" onClick={() => stopEdit(flow, modifiedFlow)}>
<i className="fa fa-check"/>
</a>
:
- <a className="edit-flow" onClick={() => startEdit(flow)}>
+ <a className="edit-flow" title="Edit Flow" onClick={() => startEdit(flow)}>
<i className="fa fa-pencil"/>
</a>
}
diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx
index 1ae4ee73..ee26dad2 100644
--- a/web/src/js/components/Footer.jsx
+++ b/web/src/js/components/Footer.jsx
@@ -7,7 +7,8 @@ Footer.propTypes = {
}
function Footer({ settings }) {
- let {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickyauth, stickycookie, stream_large_bodies} = settings;
+ let {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, websocket, anticache, anticomp,
+ stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port} = settings;
return (
<footer>
{mode && mode != "regular" && (
@@ -25,8 +26,11 @@ function Footer({ settings }) {
{rawtcp && (
<span className="label label-success">raw-tcp</span>
)}
- {!http2 && (
- <span className="label label-success">no-http2</span>
+ {http2 && (
+ <span className="label label-success">http2</span>
+ )}
+ {!websocket && (
+ <span className="label label-success">no-websocket</span>
)}
{anticache && (
<span className="label label-success">anticache</span>
@@ -43,6 +47,11 @@ function Footer({ settings }) {
{stream_large_bodies && (
<span className="label label-success">stream: {formatSize(stream_large_bodies)}</span>
)}
+ <div className="pull-right">
+ <span className="label label-primary" title="HTTP Proxy Server Address">
+ [{listen_host || "*"}:{listen_port}]
+ </span>
+ </div>
</footer>
)
}
diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx
index 702786e6..c15c951f 100644
--- a/web/src/js/components/Header.jsx
+++ b/web/src/js/components/Header.jsx
@@ -2,14 +2,13 @@ import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import classnames from 'classnames'
import MainMenu from './Header/MainMenu'
-import ViewMenu from './Header/ViewMenu'
import OptionMenu from './Header/OptionMenu'
import FileMenu from './Header/FileMenu'
import FlowMenu from './Header/FlowMenu'
import {setActiveMenu} from '../ducks/ui/header'
class Header extends Component {
- static entries = [MainMenu, ViewMenu, OptionMenu]
+ static entries = [MainMenu, OptionMenu]
handleClick(active, e) {
e.preventDefault()
@@ -23,7 +22,9 @@ class Header extends Component {
if(selectedFlowId)
entries.push(FlowMenu)
- const Active = _.find(entries, (e) => e.title == activeMenu)
+ // Make sure to have a fallback in case FlowMenu is selected but we don't have any flows
+ // (e.g. because they are all deleted or not yet received)
+ const Active = _.find(entries, (e) => e.title == activeMenu) || MainMenu
return (
<header>
@@ -38,9 +39,9 @@ class Header extends Component {
</a>
))}
</nav>
- <div className="menu">
+ <menu>
<Active/>
- </div>
+ </menu>
</header>
)
}
diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx
index 53c63ea1..ec32c857 100644
--- a/web/src/js/components/Header/FileMenu.jsx
+++ b/web/src/js/components/Header/FileMenu.jsx
@@ -21,23 +21,23 @@ function FileMenu ({clearFlows, loadFlows, saveFlows}) {
<Dropdown className="pull-left" btnClass="special" text="mitmproxy">
<a href="#" onClick={e => FileMenu.onNewClick(e, clearFlows)}>
<i className="fa fa-fw fa-file"></i>
- New
+ &nbsp;New
</a>
<FileChooser
icon="fa-folder-open"
- text="Open..."
+ text="&nbsp;Open..."
onOpenFile={file => loadFlows(file)}
/>
<a href="#" onClick={e =>{ e.preventDefault(); saveFlows();}}>
<i className="fa fa-fw fa-floppy-o"></i>
- Save...
+ &nbsp;Save...
</a>
<Divider/>
<a href="http://mitm.it/" target="_blank">
<i className="fa fa-fw fa-external-link"></i>
- Install Certificates...
+ &nbsp;Install Certificates...
</a>
</Dropdown>
)
diff --git a/web/src/js/components/Header/FilterDocs.jsx b/web/src/js/components/Header/FilterDocs.jsx
index c237e230..49fb5fbc 100644
--- a/web/src/js/components/Header/FilterDocs.jsx
+++ b/web/src/js/components/Header/FilterDocs.jsx
@@ -47,7 +47,7 @@ export default class FilterDocs extends Component {
<a href="http://docs.mitmproxy.org/en/stable/features/filters.html"
target="_blank">
<i className="fa fa-external-link"></i>
- &nbsp mitmproxy docs</a>
+ &nbsp; mitmproxy docs</a>
</td>
</tr>
</tbody>
diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx
index e78a49aa..a404fdb7 100644
--- a/web/src/js/components/Header/FlowMenu.jsx
+++ b/web/src/js/components/Header/FlowMenu.jsx
@@ -1,32 +1,71 @@
-import React, { PropTypes } from 'react'
-import { connect } from 'react-redux'
-import Button from '../common/Button'
-import { MessageUtils } from '../../flow/utils.js'
-import * as flowsActions from '../../ducks/flows'
+import React, { PropTypes } from "react"
+import { connect } from "react-redux"
+import Button from "../common/Button"
+import { MessageUtils } from "../../flow/utils.js"
+import * as flowsActions from "../../ducks/flows"
FlowMenu.title = 'Flow'
FlowMenu.propTypes = {
- flow: PropTypes.object.isRequired,
- acceptFlow: PropTypes.func.isRequired,
+ flow: PropTypes.object,
+ resumeFlow: PropTypes.func.isRequired,
+ killFlow: PropTypes.func.isRequired,
replayFlow: PropTypes.func.isRequired,
duplicateFlow: PropTypes.func.isRequired,
removeFlow: PropTypes.func.isRequired,
revertFlow: PropTypes.func.isRequired
}
-function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) {
+function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) {
+ if (!flow)
+ return <div/>
return (
<div>
- <div className="menu-row">
- <Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow" text="Accept" icon="fa-play" onClick={() => acceptFlow(flow)} />
- <Button title="[r]eplay flow" text="Replay" icon="fa-repeat" onClick={() => replayFlow(flow)} />
- <Button title="[D]uplicate flow" text="Duplicate" icon="fa-copy" onClick={() => duplicateFlow(flow)} />
- <Button title="[d]elete flow" text="Delete" icon="fa-trash" onClick={() => removeFlow(flow)}/>
- <Button disabled={!flow || !flow.modified} title="revert changes to flow [V]" text="Revert" icon="fa-history" onClick={() => revertFlow(flow)} />
- <Button title="download" text="Download" icon="fa-download" onClick={() => window.location = MessageUtils.getContentURL(flow, flow.response)}/>
+ <div className="menu-group">
+ <div className="menu-content">
+ <Button title="[r]eplay flow" icon="fa-repeat text-primary"
+ onClick={() => replayFlow(flow)}>
+ Replay
+ </Button>
+ <Button title="[D]uplicate flow" icon="fa-copy text-info"
+ onClick={() => duplicateFlow(flow)}>
+ Duplicate
+ </Button>
+ <Button disabled={!flow || !flow.modified} title="revert changes to flow [V]"
+ icon="fa-history text-warning" onClick={() => revertFlow(flow)}>
+ Revert
+ </Button>
+ <Button title="[d]elete flow" icon="fa-trash text-danger"
+ onClick={() => removeFlow(flow)}>
+ Delete
+ </Button>
+ </div>
+ <div className="menu-legend">Flow Modification</div>
</div>
- <div className="clearfix"/>
+ <div className="menu-group">
+ <div className="menu-content">
+ <Button title="download" icon="fa-download"
+ onClick={() => window.location = MessageUtils.getContentURL(flow, flow.response)}>
+ Download
+ </Button>
+ </div>
+ <div className="menu-legend">Export</div>
+ </div>
+ <div className="menu-group">
+ <div className="menu-content">
+ <Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow"
+ icon="fa-play text-success" onClick={() => resumeFlow(flow)}>
+ Resume
+ </Button>
+ <Button disabled={!flow || !flow.intercepted} title="kill intercepted flow [x]"
+ icon="fa-times text-danger" onClick={() => killFlow(flow)}>
+ Abort
+ </Button>
+ </div>
+ <div className="menu-legend">Interception</div>
+ </div>
+
+
</div>
)
}
@@ -36,7 +75,8 @@ export default connect(
flow: state.flows.byId[state.flows.selected[0]],
}),
{
- acceptFlow: flowsActions.accept,
+ resumeFlow: flowsActions.resume,
+ killFlow: flowsActions.kill,
replayFlow: flowsActions.replay,
duplicateFlow: flowsActions.duplicate,
removeFlow: flowsActions.remove,
diff --git a/web/src/js/components/Header/MainMenu.jsx b/web/src/js/components/Header/MainMenu.jsx
index 5ab3fa9d..6a4e12bf 100644
--- a/web/src/js/components/Header/MainMenu.jsx
+++ b/web/src/js/components/Header/MainMenu.jsx
@@ -1,20 +1,17 @@
-import React, { Component, PropTypes } from 'react'
-import { connect } from 'react-redux'
-import FilterInput from './FilterInput'
-import { update as updateSettings } from '../../ducks/settings'
-import { setFilter, setHighlight } from '../../ducks/flows'
+import React, { Component, PropTypes } from "react"
+import { connect } from "react-redux"
+import FilterInput from "./FilterInput"
+import { update as updateSettings } from "../../ducks/settings"
+import { setFilter, setHighlight } from "../../ducks/flows"
MainMenu.title = "Start"
export default function MainMenu() {
return (
- <div>
- <div className="menu-row">
- <FlowFilterInput/>
- <HighlightInput/>
- <InterceptInput/>
- </div>
- <div className="clearfix"></div>
+ <div className="menu-main">
+ <FlowFilterInput/>
+ <HighlightInput/>
+ <InterceptInput/>
</div>
)
}
diff --git a/web/src/js/components/Header/MenuToggle.jsx b/web/src/js/components/Header/MenuToggle.jsx
new file mode 100644
index 00000000..91f093c6
--- /dev/null
+++ b/web/src/js/components/Header/MenuToggle.jsx
@@ -0,0 +1,69 @@
+import { PropTypes } from "react"
+import { connect } from "react-redux"
+import { update as updateSettings } from "../../ducks/settings"
+import { toggleVisibility } from "../../ducks/eventLog"
+
+MenuToggle.propTypes = {
+ value: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+ children: PropTypes.node.isRequired,
+}
+
+export function MenuToggle({ value, onChange, children }) {
+ return (
+ <div className="menu-entry">
+ <label>
+ <input type="checkbox"
+ checked={value}
+ onChange={onChange}/>
+ {children}
+ </label>
+ </div>
+ )
+}
+
+
+SettingsToggle.propTypes = {
+ setting: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+}
+
+export function SettingsToggle({ setting, children, settings, updateSettings }) {
+ return (
+ <MenuToggle
+ value={settings[setting] || false} // we don't have settings initially, so just pass false.
+ onChange={() => updateSettings({ [setting]: !settings[setting] })}
+ >
+ {children}
+ </MenuToggle>
+ )
+}
+SettingsToggle = connect(
+ state => ({
+ settings: state.settings,
+ }),
+ {
+ updateSettings,
+ }
+)(SettingsToggle)
+
+
+export function EventlogToggle({ toggleVisibility, eventLogVisible }) {
+ return (
+ <MenuToggle
+ value={eventLogVisible}
+ onChange={toggleVisibility}
+ >
+ Display Event Log
+ </MenuToggle>
+ )
+}
+EventlogToggle = connect(
+ state => ({
+ eventLogVisible: state.eventLog.visible,
+ }),
+ {
+ toggleVisibility,
+ }
+)(EventlogToggle)
+
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
index 186a9c6a..d6a8dfc2 100644
--- a/web/src/js/components/Header/OptionMenu.jsx
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -1,71 +1,65 @@
-import React, { PropTypes } from 'react'
-import { connect } from 'react-redux'
-import ToggleButton from '../common/ToggleButton'
-import ToggleInputButton from '../common/ToggleInputButton'
-import { update as updateSettings } from '../../ducks/settings'
+import React, { PropTypes } from "react"
+import { connect } from "react-redux"
+import { SettingsToggle, EventlogToggle } from "./MenuToggle"
+import DocsLink from "../common/DocsLink"
OptionMenu.title = 'Options'
-OptionMenu.propTypes = {
- settings: PropTypes.object.isRequired,
- updateSettings: PropTypes.func.isRequired,
-}
-
-function OptionMenu({ settings, updateSettings }) {
+export default function OptionMenu() {
return (
<div>
- <div className="menu-row">
- <ToggleButton text="showhost"
- checked={settings.showhost}
- onToggle={() => updateSettings({ showhost: !settings.showhost })}
- />
- <ToggleButton text="no_upstream_cert"
- checked={settings.no_upstream_cert}
- onToggle={() => updateSettings({ no_upstream_cert: !settings.no_upstream_cert })}
- />
- <ToggleButton text="rawtcp"
- checked={settings.rawtcp}
- onToggle={() => updateSettings({ rawtcp: !settings.rawtcp })}
- />
- <ToggleButton text="http2"
- checked={settings.http2}
- onToggle={() => updateSettings({ http2: !settings.http2 })}
- />
- <ToggleButton text="anticache"
- checked={settings.anticache}
- onToggle={() => updateSettings({ anticache: !settings.anticache })}
- />
- <ToggleButton text="anticomp"
- checked={settings.anticomp}
- onToggle={() => updateSettings({ anticomp: !settings.anticomp })}
- />
- <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
- checked={!!settings.stickyauth}
- txt={settings.stickyauth}
- onToggleChanged={txt => updateSettings({ stickyauth: !settings.stickyauth ? txt : null })}
- />
- <ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
- checked={!!settings.stickycookie}
- txt={settings.stickycookie}
- onToggleChanged={txt => updateSettings({ stickycookie: !settings.stickycookie ? txt : null })}
- />
- <ToggleInputButton name="stream_large_bodies" placeholder="stream..."
- checked={!!settings.stream_large_bodies}
- txt={settings.stream_large_bodies}
- inputType="number"
- onToggleChanged={txt => updateSettings({ stream_large_bodies: !settings.stream_large_bodies ? txt : null })}
- />
+ <div className="menu-group">
+ <div className="menu-content">
+ <SettingsToggle setting="http2">HTTP/2.0</SettingsToggle>
+ <SettingsToggle setting="websocket">WebSockets</SettingsToggle>
+ <SettingsToggle setting="rawtcp">Raw TCP</SettingsToggle>
+ </div>
+ <div className="menu-legend">Protocol Support</div>
+ </div>
+ <div className="menu-group">
+ <div className="menu-content">
+ <SettingsToggle setting="anticache">
+ Disable Caching <DocsLink resource="features/anticache.html"/>
+ </SettingsToggle>
+ <SettingsToggle setting="anticomp">
+ Disable Compression <i className="fa fa-question-circle"
+ title="Do not forward Accept-Encoding headers to the server to force an uncompressed response."></i>
+ </SettingsToggle>
+ </div>
+ <div className="menu-legend">HTTP Options</div>
</div>
- <div className="clearfix"/>
+ <div className="menu-group">
+ <div className="menu-content">
+ <SettingsToggle setting="showhost">
+ Use Host Header <i className="fa fa-question-circle"
+ title="Use the Host header to construct URLs for display."></i>
+ </SettingsToggle>
+ <EventlogToggle/>
+ </div>
+ <div className="menu-legend">View Options</div>
+ </div>
+ { /*
+ <ToggleButton text="no_upstream_cert"
+ checked={settings.no_upstream_cert}
+ onToggle={() => updateSettings({ no_upstream_cert: !settings.no_upstream_cert })}
+ />
+ <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
+ checked={!!settings.stickyauth}
+ txt={settings.stickyauth}
+ onToggleChanged={txt => updateSettings({ stickyauth: !settings.stickyauth ? txt : null })}
+ />
+ <ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
+ checked={!!settings.stickycookie}
+ txt={settings.stickycookie}
+ onToggleChanged={txt => updateSettings({ stickycookie: !settings.stickycookie ? txt : null })}
+ />
+ <ToggleInputButton name="stream_large_bodies" placeholder="stream..."
+ checked={!!settings.stream_large_bodies}
+ txt={settings.stream_large_bodies}
+ inputType="number"
+ onToggleChanged={txt => updateSettings({ stream_large_bodies: !settings.stream_large_bodies ? txt : null })}
+ />
+ */}
</div>
)
}
-
-export default connect(
- state => ({
- settings: state.settings,
- }),
- {
- updateSettings,
- }
-)(OptionMenu)
diff --git a/web/src/js/components/Header/ViewMenu.jsx b/web/src/js/components/Header/ViewMenu.jsx
deleted file mode 100644
index 22b370d1..00000000
--- a/web/src/js/components/Header/ViewMenu.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React, { PropTypes } from 'react'
-import { connect } from 'react-redux'
-import ToggleButton from '../common/ToggleButton'
-import { toggleVisibility } from '../../ducks/eventLog'
-
-ViewMenu.title = 'View'
-ViewMenu.route = 'flows'
-
-ViewMenu.propTypes = {
- eventLogVisible: PropTypes.bool.isRequired,
- toggleEventLog: PropTypes.func.isRequired,
-}
-
-function ViewMenu({ eventLogVisible, toggleEventLog }) {
- return (
- <div>
- <div className="menu-row">
- <ToggleButton text="Show Event Log" checked={eventLogVisible} onToggle={toggleEventLog} />
- </div>
- <div className="clearfix"></div>
- </div>
- )
-}
-
-export default connect(
- state => ({
- eventLogVisible: state.eventLog.visible,
- }),
- {
- toggleEventLog: toggleVisibility,
- }
-)(ViewMenu)
diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx
index bfbb455d..f05a68d0 100644
--- a/web/src/js/components/common/Button.jsx
+++ b/web/src/js/components/common/Button.jsx
@@ -1,19 +1,21 @@
-import React, { PropTypes } from 'react'
-import classnames from 'classnames'
+import React, { PropTypes } from "react"
+import classnames from "classnames"
Button.propTypes = {
onClick: PropTypes.func.isRequired,
- text: PropTypes.string,
- icon: PropTypes.string
+ children: PropTypes.node.isRequired,
+ icon: PropTypes.string,
+ title: PropTypes.string,
}
-export default function Button({ onClick, text, icon, disabled, className }) {
+export default function Button({ onClick, children, icon, disabled, className, title }) {
return (
<div className={classnames(className, 'btn btn-default')}
- onClick={onClick}
- disabled={disabled}>
+ onClick={!disabled && onClick}
+ disabled={disabled}
+ title={title}>
{icon && (<i className={"fa fa-fw " + icon}/> )}
- {text && text}
+ {children}
</div>
)
}
diff --git a/web/src/js/components/common/DocsLink.jsx b/web/src/js/components/common/DocsLink.jsx
new file mode 100644
index 00000000..182811a3
--- /dev/null
+++ b/web/src/js/components/common/DocsLink.jsx
@@ -0,0 +1,14 @@
+import { PropTypes } from 'react'
+
+DocsLink.propTypes = {
+ resource: PropTypes.string.isRequired,
+}
+
+export default function DocsLink({ children, resource }) {
+ let url = `http://docs.mitmproxy.org/en/stable/${resource}`
+ return (
+ <a target="_blank" href={url}>
+ {children || <i className="fa fa-question-circle"></i>}
+ </a>
+ )
+}
diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js
index d3717533..92408891 100644
--- a/web/src/js/ducks/flows.js
+++ b/web/src/js/ducks/flows.js
@@ -36,8 +36,29 @@ export default function reduce(state = defaultState, action) {
makeFilter(state.filter),
makeSort(state.sort)
)
+
+ let selected = state.selected
+ if(action.type === REMOVE && state.selected.includes(action.data)) {
+ if(state.selected.length > 1){
+ selected = selected.filter(x => x !== action.data)
+ } else {
+ selected = []
+ if (action.data in state.viewIndex && state.view.length > 1) {
+ let currentIndex = state.viewIndex[action.data],
+ nextSelection
+ if(currentIndex === state.view.length -1){ // last row
+ nextSelection = state.view[currentIndex - 1]
+ } else {
+ nextSelection = state.view[currentIndex + 1]
+ }
+ selected.push(nextSelection.id)
+ }
+ }
+ }
+
return {
...state,
+ selected,
...reduceStore(state, storeAction)
}
@@ -48,6 +69,12 @@ export default function reduce(state = defaultState, action) {
...reduceStore(state, storeActions.setFilter(makeFilter(action.filter), makeSort(state.sort)))
}
+ case SET_HIGHLIGHT:
+ return {
+ ...state,
+ highlight: action.highlight
+ }
+
case SET_SORT:
return {
...state,
@@ -144,14 +171,23 @@ export function selectRelative(shift) {
}
-export function accept(flow) {
- return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' })
+export function resume(flow) {
+ return dispatch => fetchApi(`/flows/${flow.id}/resume`, { method: 'POST' })
+}
+
+export function resumeAll() {
+ return dispatch => fetchApi('/flows/resume', { method: 'POST' })
+}
+
+export function kill(flow) {
+ return dispatch => fetchApi(`/flows/${flow.id}/kill`, { method: 'POST' })
}
-export function acceptAll() {
- return dispatch => fetchApi('/flows/accept', { method: 'POST' })
+export function killAll() {
+ return dispatch => fetchApi('/flows/kill', { method: 'POST' })
}
+
export function remove(flow) {
return dispatch => fetchApi(`/flows/${flow.id}`, { method: 'DELETE' })
}
diff --git a/web/src/js/ducks/ui/header.js b/web/src/js/ducks/ui/header.js
index 25dfe602..6581149e 100644
--- a/web/src/js/ducks/ui/header.js
+++ b/web/src/js/ducks/ui/header.js
@@ -1,4 +1,4 @@
-import * as flowsActions from '../flows'
+import * as flowsActions from "../flows"
export const SET_ACTIVE_MENU = 'UI_SET_ACTIVE_MENU'
@@ -19,7 +19,7 @@ export default function reducer(state = defaultState, action) {
case flowsActions.SELECT:
// First Select
- if (action.flowIds.length && !state.isFlowSelected) {
+ if (action.flowIds.length > 0 && !state.isFlowSelected) {
return {
...state,
activeMenu: 'Flow',
@@ -28,7 +28,7 @@ export default function reducer(state = defaultState, action) {
}
// Deselect
- if (!action.flowIds.length && state.isFlowSelected) {
+ if (action.flowIds.length === 0 && state.isFlowSelected) {
let activeMenu = state.activeMenu
if (activeMenu == 'Flow') {
activeMenu = 'Start'
diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js
index 7418eca9..30fd76e1 100644
--- a/web/src/js/ducks/ui/keyboard.js
+++ b/web/src/js/ducks/ui/keyboard.js
@@ -1,6 +1,6 @@
-import { Key } from '../../utils'
-import { selectTab } from './flow'
-import * as flowsActions from '../flows'
+import { Key } from "../../utils"
+import { selectTab } from "./flow"
+import * as flowsActions from "../flows"
export function onKeyDown(e) {
@@ -9,7 +9,7 @@ export function onKeyDown(e) {
return () => {
}
}
- var key = e.keyCode
+ var key = e.keyCode
var shiftKey = e.shiftKey
e.preventDefault()
return (dispatch, getState) => {
@@ -48,9 +48,8 @@ export function onKeyDown(e) {
dispatch(flowsActions.select(null))
break
- case Key.LEFT:
- {
- if(!flow) break
+ case Key.LEFT: {
+ if (!flow) break
let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']),
currentTab = getState().ui.flow.tab,
nextTab = tabs[(tabs.indexOf(currentTab) - 1 + tabs.length) % tabs.length]
@@ -59,9 +58,8 @@ export function onKeyDown(e) {
}
case Key.TAB:
- case Key.RIGHT:
- {
- if(!flow) break
+ case Key.RIGHT: {
+ if (!flow) break
let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']),
currentTab = getState().ui.flow.tab,
nextTab = tabs[(tabs.indexOf(currentTab) + 1) % tabs.length]
@@ -69,14 +67,7 @@ export function onKeyDown(e) {
break
}
- case Key.C:
- if (shiftKey) {
- dispatch(flowsActions.clear())
- }
- break
-
- case Key.D:
- {
+ case Key.D: {
if (!flow) {
return
}
@@ -88,32 +79,46 @@ export function onKeyDown(e) {
break
}
- case Key.A:
- {
+ case Key.A: {
if (shiftKey) {
- dispatch(flowsActions.acceptAll())
+ dispatch(flowsActions.resumeAll())
} else if (flow && flow.intercepted) {
- dispatch(flowsActions.accept(flow))
+ dispatch(flowsActions.resume(flow))
}
break
}
- case Key.R:
- {
+ case Key.R: {
if (!shiftKey && flow) {
dispatch(flowsActions.replay(flow))
}
break
}
- case Key.V:
- {
+ case Key.V: {
if (!shiftKey && flow && flow.modified) {
dispatch(flowsActions.revert(flow))
}
break
}
+ case Key.X: {
+ if (shiftKey) {
+ dispatch(flowsActions.killAll())
+ } else if (flow && flow.intercepted) {
+ dispatch(flowsActions.kill(flow))
+ }
+ break
+ }
+
+ case Key.Z: {
+ if (!shiftKey) {
+ dispatch(flowsActions.clear())
+ }
+ break
+ }
+
+
default:
return
}
diff --git a/web/src/js/ducks/utils/store.js b/web/src/js/ducks/utils/store.js
index 9ea4f02e..ac272650 100644
--- a/web/src/js/ducks/utils/store.js
+++ b/web/src/js/ducks/utils/store.js
@@ -85,6 +85,7 @@ export default function reduce(state = defaultState, action) {
if (!(action.id in byId)) {
break
}
+ byId = {...byId}
delete byId[action.id];
({data: list, dataIndex: listIndex} = removeData(list, listIndex, action.id))