aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/js/components')
-rw-r--r--web/src/js/components/EventLog.jsx41
-rw-r--r--web/src/js/components/EventLog/EventList.jsx90
-rw-r--r--web/src/js/components/FlowTable.jsx120
-rw-r--r--web/src/js/components/FlowTable/FlowColumns.jsx137
-rw-r--r--web/src/js/components/FlowTable/FlowRow.jsx28
-rw-r--r--web/src/js/components/FlowTable/FlowTableHead.jsx43
-rw-r--r--web/src/js/components/Footer.jsx (renamed from web/src/js/components/footer.js)45
-rw-r--r--web/src/js/components/Header.js56
-rw-r--r--web/src/js/components/Header/FileMenu.jsx100
-rw-r--r--web/src/js/components/Header/FilterDocs.jsx56
-rw-r--r--web/src/js/components/Header/FilterInput.jsx133
-rw-r--r--web/src/js/components/Header/MainMenu.jsx73
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx60
-rw-r--r--web/src/js/components/Header/ViewMenu.jsx33
-rw-r--r--web/src/js/components/MainView.jsx (renamed from web/src/js/components/MainView.js)47
-rw-r--r--web/src/js/components/ProxyApp.jsx (renamed from web/src/js/components/ProxyApp.js)11
-rw-r--r--web/src/js/components/eventlog.js153
-rw-r--r--web/src/js/components/flowtable-columns.js131
-rw-r--r--web/src/js/components/flowtable.js201
-rw-r--r--web/src/js/components/header.js453
-rw-r--r--web/src/js/components/prompt.js4
21 files changed, 1027 insertions, 988 deletions
diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx
new file mode 100644
index 00000000..24b3c2bf
--- /dev/null
+++ b/web/src/js/components/EventLog.jsx
@@ -0,0 +1,41 @@
+import React, { PropTypes } from 'react'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+import { toggleEventLogFilter, toggleEventLogVisibility } from '../ducks/eventLog'
+import { ToggleButton } from './common'
+import EventList from './EventLog/EventList'
+
+EventLog.propTypes = {
+ filters: PropTypes.object.isRequired,
+ events: PropTypes.array.isRequired,
+ onToggleFilter: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired
+}
+
+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>
+ </div>
+ </div>
+ <EventList events={events} />
+ </div>
+ )
+}
+
+export default connect(
+ state => ({
+ filters: state.eventLog.filter,
+ events: state.eventLog.filteredEvents,
+ }),
+ dispatch => bindActionCreators({
+ onClose: toggleEventLogVisibility,
+ onToggleFilter: toggleEventLogFilter,
+ }, dispatch)
+)(EventLog)
diff --git a/web/src/js/components/EventLog/EventList.jsx b/web/src/js/components/EventLog/EventList.jsx
new file mode 100644
index 00000000..d0b036e7
--- /dev/null
+++ b/web/src/js/components/EventLog/EventList.jsx
@@ -0,0 +1,90 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import shallowEqual from 'shallowequal'
+import AutoScroll from '../helpers/AutoScroll'
+import { calcVScroll } from '../helpers/VirtualScroll'
+
+class EventLogList extends Component {
+
+ static propTypes = {
+ events: PropTypes.array.isRequired,
+ rowHeight: PropTypes.number,
+ }
+
+ static defaultProps = {
+ rowHeight: 18,
+ }
+
+ constructor(props) {
+ super(props)
+
+ this.heights = {}
+ this.state = { vScroll: calcVScroll() }
+
+ this.onViewportUpdate = this.onViewportUpdate.bind(this)
+ }
+
+ componentDidMount() {
+ window.addEventListener('resize', this.onViewportUpdate)
+ this.onViewportUpdate()
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.onViewportUpdate)
+ }
+
+ componentDidUpdate() {
+ this.onViewportUpdate()
+ }
+
+ onViewportUpdate() {
+ const viewport = ReactDOM.findDOMNode(this)
+
+ const vScroll = calcVScroll({
+ itemCount: this.props.events.length,
+ rowHeight: this.props.rowHeight,
+ viewportTop: viewport.scrollTop,
+ viewportHeight: viewport.offsetHeight,
+ itemHeights: this.props.events.map(entry => this.heights[entry.id]),
+ })
+
+ if (!shallowEqual(this.state.vScroll, vScroll)) {
+ this.setState({vScroll})
+ }
+ }
+
+ setHeight(id, node) {
+ if (node && !this.heights[id]) {
+ const height = node.offsetHeight
+ if (this.heights[id] !== height) {
+ this.heights[id] = height
+ this.onViewportUpdate()
+ }
+ }
+ }
+
+ render() {
+ const { vScroll } = this.state
+ const { events } = this.props
+
+ return (
+ <pre onScroll={this.onViewportUpdate}>
+ <div style={{ height: vScroll.paddingTop }}></div>
+ {events.slice(vScroll.start, vScroll.end).map(event => (
+ <div key={event.id} ref={node => this.setHeight(event.id, node)}>
+ <LogIcon event={event}/>
+ {event.message}
+ </div>
+ ))}
+ <div style={{ height: vScroll.paddingBottom }}></div>
+ </pre>
+ )
+ }
+}
+
+function LogIcon({ event }) {
+ const icon = { web: 'html5', debug: 'bug' }[event.level] || 'info'
+ return <i className={`fa fa-fw fa-${icon}`}></i>
+}
+
+export default AutoScroll(EventLogList)
diff --git a/web/src/js/components/FlowTable.jsx b/web/src/js/components/FlowTable.jsx
new file mode 100644
index 00000000..eddeed62
--- /dev/null
+++ b/web/src/js/components/FlowTable.jsx
@@ -0,0 +1,120 @@
+import React, { PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import shallowEqual from 'shallowequal'
+import AutoScroll from './helpers/AutoScroll'
+import { calcVScroll } from './helpers/VirtualScroll'
+import FlowTableHead from './FlowTable/FlowTableHead'
+import FlowRow from './FlowTable/FlowRow'
+import Filt from "../filt/filt"
+
+class FlowTable extends React.Component {
+
+ static propTypes = {
+ onSelect: PropTypes.func.isRequired,
+ flows: PropTypes.array.isRequired,
+ rowHeight: PropTypes.number,
+ highlight: PropTypes.string,
+ selected: PropTypes.object,
+ }
+
+ static defaultProps = {
+ rowHeight: 32,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { vScroll: calcVScroll() }
+ this.onViewportUpdate = this.onViewportUpdate.bind(this)
+ }
+
+ componentWillMount() {
+ window.addEventListener('resize', this.onViewportUpdate)
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.onViewportUpdate)
+ }
+
+ componentDidUpdate() {
+ this.onViewportUpdate()
+
+ if (!this.shouldScrollIntoView) {
+ return
+ }
+
+ this.shouldScrollIntoView = false
+
+ const { rowHeight, flows, selected } = this.props
+ const viewport = ReactDOM.findDOMNode(this)
+ const head = ReactDOM.findDOMNode(this.refs.head)
+
+ const headHeight = head ? head.offsetHeight : 0
+
+ const rowTop = (flows.indexOf(selected) * rowHeight) + headHeight
+ const rowBottom = rowTop + rowHeight
+
+ const viewportTop = viewport.scrollTop
+ const viewportHeight = viewport.offsetHeight
+
+ // Account for pinned thead
+ if (rowTop - headHeight < viewportTop) {
+ viewport.scrollTop = rowTop - headHeight
+ } else if (rowBottom > viewportTop + viewportHeight) {
+ viewport.scrollTop = rowBottom - viewportHeight
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.selected && nextProps.selected !== this.props.selected) {
+ this.shouldScrollIntoView = true
+ }
+ }
+
+ onViewportUpdate() {
+ const viewport = ReactDOM.findDOMNode(this)
+ const viewportTop = viewport.scrollTop
+
+ const vScroll = calcVScroll({
+ viewportTop,
+ viewportHeight: viewport.offsetHeight,
+ itemCount: this.props.flows.length,
+ rowHeight: this.props.rowHeight,
+ })
+
+ if (this.state.viewportTop !== viewportTop || !shallowEqual(this.state.vScroll, vScroll)) {
+ this.setState({ vScroll, viewportTop })
+ }
+ }
+
+ render() {
+ const { vScroll, viewportTop } = this.state
+ const { flows, selected, highlight } = this.props
+ const isHighlighted = highlight ? Filt.parse(highlight) : () => false
+
+ return (
+ <div className="flow-table" onScroll={this.onViewportUpdate}>
+ <table>
+ <thead ref="head" style={{ transform: `translateY(${viewportTop}px)` }}>
+ <FlowTableHead />
+ </thead>
+ <tbody>
+ <tr style={{ height: vScroll.paddingTop }}></tr>
+ {flows.slice(vScroll.start, vScroll.end).map(flow => (
+ <FlowRow
+ key={flow.id}
+ flow={flow}
+ selected={flow === selected}
+ highlighted={isHighlighted(flow)}
+ onSelect={this.props.onSelect}
+ />
+ ))}
+ <tr style={{ height: vScroll.paddingBottom }}></tr>
+ </tbody>
+ </table>
+ </div>
+ )
+ }
+}
+
+export default AutoScroll(FlowTable)
diff --git a/web/src/js/components/FlowTable/FlowColumns.jsx b/web/src/js/components/FlowTable/FlowColumns.jsx
new file mode 100644
index 00000000..11c0796c
--- /dev/null
+++ b/web/src/js/components/FlowTable/FlowColumns.jsx
@@ -0,0 +1,137 @@
+import React, { Component } from 'react'
+import classnames from 'classnames'
+import { RequestUtils, ResponseUtils } from '../../flow/utils.js'
+import { formatSize, formatTimeDelta } from '../../utils.js'
+
+export function TLSColumn({ flow }) {
+ return (
+ <td className={classnames('col-tls', flow.request.scheme === 'https' ? 'col-tls-https' : 'col-tls-http')}></td>
+ )
+}
+
+TLSColumn.sortKeyFun = flow => flow.request.scheme
+TLSColumn.headerClass = 'col-tls'
+TLSColumn.headerName = ''
+
+export function IconColumn({ flow }) {
+ return (
+ <td className="col-icon">
+ <div className={classnames('resource-icon', IconColumn.getIcon(flow))}></div>
+ </td>
+ )
+}
+
+IconColumn.headerClass = 'col-icon'
+IconColumn.headerName = ''
+
+IconColumn.getIcon = flow => {
+ if (!flow.response) {
+ return 'resource-icon-plain'
+ }
+
+ var contentType = ResponseUtils.getContentType(flow.response) || ''
+
+ // @todo We should assign a type to the flow somewhere else.
+ if (flow.response.status_code === 304) {
+ return 'resource-icon-not-modified'
+ }
+ if (300 <= flow.response.status_code && flow.response.status_code < 400) {
+ return 'resource-icon-redirect'
+ }
+ if (contentType.indexOf('image') >= 0) {
+ return 'resource-icon-image'
+ }
+ if (contentType.indexOf('javascript') >= 0) {
+ return 'resource-icon-js'
+ }
+ if (contentType.indexOf('css') >= 0) {
+ return 'resource-icon-css'
+ }
+ if (contentType.indexOf('html') >= 0) {
+ return 'resource-icon-document'
+ }
+
+ return 'resource-icon-plain'
+}
+
+export function PathColumn({ flow }) {
+ return (
+ <td className="col-path">
+ {flow.request.is_replay && (
+ <i className="fa fa-fw fa-repeat pull-right"></i>
+ )}
+ {flow.intercepted && (
+ <i className="fa fa-fw fa-pause pull-right"></i>
+ )}
+ {RequestUtils.pretty_url(flow.request)}
+ </td>
+ )
+}
+
+PathColumn.sortKeyFun = flow => RequestUtils.pretty_url(flow.request)
+PathColumn.headerClass = 'col-path'
+PathColumn.headerName = 'Path'
+
+export function MethodColumn({ flow }) {
+ return (
+ <td className="col-method">{flow.request.method}</td>
+ )
+}
+
+MethodColumn.sortKeyFun = flow => flow.request.method
+MethodColumn.headerClass = 'col-method'
+MethodColumn.headerName = 'Method'
+
+export function StatusColumn({ flow }) {
+ return (
+ <td className="col-status">{flow.response && flow.response.status_code}</td>
+ )
+}
+
+StatusColumn.sortKeyFun = flow => flow.response && flow.response.status_code
+StatusColumn.headerClass = 'col-status'
+StatusColumn.headerName = 'Status'
+
+export function SizeColumn({ flow }) {
+ return (
+ <td className="col-size">{formatSize(SizeColumn.getTotalSize(flow))}</td>
+ )
+}
+
+SizeColumn.sortKeyFun = flow => {
+ let total = flow.request.contentLength
+ if (flow.response) {
+ total += flow.response.contentLength || 0
+ }
+ return total
+}
+
+SizeColumn.getTotalSize = SizeColumn.sortKeyFun
+SizeColumn.headerClass = 'col-size'
+SizeColumn.headerName = 'Size'
+
+export function TimeColumn({ flow }) {
+ return (
+ <td className="col-time">
+ {flow.response ? (
+ formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
+ ) : (
+ '...'
+ )}
+ </td>
+ )
+}
+
+TimeColumn.sortKeyFun = flow => flow.response && flow.response.timestamp_end - flow.request.timestamp_start
+TimeColumn.headerClass = 'col-time'
+TimeColumn.headerName = 'Time'
+
+export default [
+ TLSColumn,
+ IconColumn,
+ PathColumn,
+ MethodColumn,
+ StatusColumn,
+ SizeColumn,
+ TimeColumn,
+]
diff --git a/web/src/js/components/FlowTable/FlowRow.jsx b/web/src/js/components/FlowTable/FlowRow.jsx
new file mode 100644
index 00000000..749bc0ce
--- /dev/null
+++ b/web/src/js/components/FlowTable/FlowRow.jsx
@@ -0,0 +1,28 @@
+import React, { PropTypes } from 'react'
+import classnames from 'classnames'
+import columns from './FlowColumns'
+
+FlowRow.propTypes = {
+ onSelect: PropTypes.func.isRequired,
+ flow: PropTypes.object.isRequired,
+ highlighted: PropTypes.bool,
+ selected: PropTypes.bool,
+}
+
+export default function FlowRow({ flow, selected, highlighted, onSelect }) {
+ const className = classnames({
+ 'selected': selected,
+ 'highlighted': highlighted,
+ 'intercepted': flow.intercepted,
+ 'has-request': flow.request,
+ 'has-response': flow.response,
+ })
+
+ return (
+ <tr className={className} onClick={() => onSelect(flow)}>
+ {columns.map(Column => (
+ <Column key={Column.name} flow={flow}/>
+ ))}
+ </tr>
+ )
+}
diff --git a/web/src/js/components/FlowTable/FlowTableHead.jsx b/web/src/js/components/FlowTable/FlowTableHead.jsx
new file mode 100644
index 00000000..a46219d1
--- /dev/null
+++ b/web/src/js/components/FlowTable/FlowTableHead.jsx
@@ -0,0 +1,43 @@
+import React, { PropTypes } from 'react'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+import classnames from 'classnames'
+import columns from './FlowColumns'
+
+import { setSort } from "../../ducks/flows"
+
+FlowTableHead.propTypes = {
+ onSort: PropTypes.func.isRequired,
+ sortDesc: React.PropTypes.bool.isRequired,
+ sortColumn: React.PropTypes.string,
+}
+
+function FlowTableHead({ sortColumn, sortDesc, onSort }) {
+ const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
+
+ return (
+ <tr>
+ {columns.map(Column => (
+ <th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
+ key={Column.name}
+ onClick={() => onClick(Column)}>
+ {Column.headerName}
+ </th>
+ ))}
+ </tr>
+ )
+
+ function onClick(Column) {
+ onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })
+ }
+}
+
+export default connect(
+ state => ({
+ sortDesc: state.flows.sort.sortDesc,
+ sortColumn: state.flows.sort.sortColumn,
+ }),
+ dispatch => bindActionCreators({
+ onSort: setSort,
+ }, dispatch)
+)(FlowTableHead)
diff --git a/web/src/js/components/footer.js b/web/src/js/components/Footer.jsx
index 8fe1081b..903522f4 100644
--- a/web/src/js/components/footer.js
+++ b/web/src/js/components/Footer.jsx
@@ -1,50 +1,47 @@
-import React from "react";
-import {formatSize} from "../utils.js"
-import {SettingsState} from "./common.js";
+import React from 'react'
+import { formatSize } from '../utils.js'
+import { SettingsState } from './common.js'
Footer.propTypes = {
settings: React.PropTypes.object.isRequired,
-};
+}
export default function Footer({ settings }) {
- const {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickyauth, stickycookie, stream} = settings;
return (
<footer>
- {mode && mode != "regular" && (
- <span className="label label-success">{mode} mode</span>
+ {settings.mode && settings.mode != "regular" && (
+ <span className="label label-success">{settings.mode} mode</span>
)}
- {intercept && (
- <span className="label label-success">Intercept: {intercept}</span>
+ {settings.intercept && (
+ <span className="label label-success">Intercept: {settings.intercept}</span>
)}
- {showhost && (
+ {settings.showhost && (
<span className="label label-success">showhost</span>
)}
- {no_upstream_cert && (
+ {settings.no_upstream_cert && (
<span className="label label-success">no-upstream-cert</span>
)}
- {rawtcp && (
+ {settings.rawtcp && (
<span className="label label-success">raw-tcp</span>
)}
- {!http2 && (
+ {!settings.http2 && (
<span className="label label-success">no-http2</span>
)}
- {anticache && (
+ {settings.anticache && (
<span className="label label-success">anticache</span>
)}
- {anticomp && (
+ {settings.anticomp && (
<span className="label label-success">anticomp</span>
)}
- {stickyauth && (
- <span className="label label-success">stickyauth: {stickyauth}</span>
+ {settings.stickyauth && (
+ <span className="label label-success">stickyauth: {settings.stickyauth}</span>
)}
- {stickycookie && (
- <span className="label label-success">stickycookie: {stickycookie}</span>
+ {settings.stickycookie && (
+ <span className="label label-success">stickycookie: {settings.stickycookie}</span>
)}
- {stream && (
- <span className="label label-success">stream: {formatSize(stream)}</span>
+ {settings.stream && (
+ <span className="label label-success">stream: {formatSize(settings.stream)}</span>
)}
-
-
</footer>
- );
+ )
}
diff --git a/web/src/js/components/Header.js b/web/src/js/components/Header.js
new file mode 100644
index 00000000..7134f7d9
--- /dev/null
+++ b/web/src/js/components/Header.js
@@ -0,0 +1,56 @@
+import React, { Component, PropTypes } from 'react'
+import classnames from 'classnames'
+import { toggleEventLogVisibility } from '../ducks/eventLog'
+import MainMenu from './Header/MainMenu'
+import ViewMenu from './Header/ViewMenu'
+import OptionMenu from './Header/OptionMenu'
+import FileMenu from './Header/FileMenu'
+
+export default class Header extends Component {
+
+ static entries = [MainMenu, ViewMenu, OptionMenu]
+
+ static propTypes = {
+ settings: PropTypes.object.isRequired,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = { active: Header.entries[0] }
+ }
+
+ handleClick(active, e) {
+ e.preventDefault()
+ this.props.updateLocation(active.route)
+ this.setState({ active })
+ }
+
+ render() {
+ const { active: Active } = this.state
+ const { settings, updateLocation, query } = this.props
+
+ return (
+ <header>
+ <nav className="nav-tabs nav-tabs-lg">
+ <FileMenu/>
+ {Header.entries.map(Entry => (
+ <a key={Entry.title}
+ href="#"
+ className={classnames({ active: Entry === Active })}
+ onClick={e => this.handleClick(Entry, e)}>
+ {Entry.title}
+ </a>
+ ))}
+ </nav>
+ <div className="menu">
+ <Active
+ ref="active"
+ settings={settings}
+ updateLocation={updateLocation}
+ query={query}
+ />
+ </div>
+ </header>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx
new file mode 100644
index 00000000..b075b3c8
--- /dev/null
+++ b/web/src/js/components/Header/FileMenu.jsx
@@ -0,0 +1,100 @@
+import React, { Component } from 'react'
+import classnames from 'classnames'
+import { FlowActions } from '../../actions.js'
+
+export default class FileMenu extends Component {
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = { show: false }
+
+ this.close = this.close.bind(this)
+ this.onFileClick = this.onFileClick.bind(this)
+ this.onNewClick = this.onNewClick.bind(this)
+ this.onOpenClick = this.onOpenClick.bind(this)
+ this.onOpenFile = this.onOpenFile.bind(this)
+ this.onSaveClick = this.onSaveClick.bind(this)
+ }
+
+ close() {
+ this.setState({ show: false })
+ document.removeEventListener('click', this.close)
+ }
+
+ onFileClick(e) {
+ e.preventDefault()
+
+ if (this.state.show) {
+ return
+ }
+
+ document.addEventListener('click', this.close)
+ this.setState({ show: true })
+ }
+
+ onNewClick(e) {
+ e.preventDefault()
+ if (confirm('Delete all flows?')) {
+ FlowActions.clear()
+ }
+ }
+
+ onOpenClick(e) {
+ e.preventDefault()
+ this.fileInput.click()
+ }
+
+ onOpenFile(e) {
+ e.preventDefault()
+ if (e.target.files.length > 0) {
+ FlowActions.upload(e.target.files[0])
+ this.fileInput.value = ''
+ }
+ }
+
+ onSaveClick(e) {
+ e.preventDefault()
+ FlowActions.download()
+ }
+
+ render() {
+ return (
+ <div className={classnames('dropdown pull-left', { open: this.state.show })}>
+ <a href="#" className="special" onClick={this.onFileClick}>mitmproxy</a>
+ <ul className="dropdown-menu" role="menu">
+ <li>
+ <a href="#" onClick={this.onNewClick}>
+ <i className="fa fa-fw fa-file"></i>
+ New
+ </a>
+ </li>
+ <li>
+ <a href="#" onClick={this.onOpenClick}>
+ <i className="fa fa-fw fa-folder-open"></i>
+ Open...
+ </a>
+ <input
+ ref={ref => this.fileInput = ref}
+ className="hidden"
+ type="file"
+ onChange={this.onOpenFile}
+ />
+ </li>
+ <li>
+ <a href="#" onClick={this.onSaveClick}>
+ <i className="fa fa-fw fa-floppy-o"></i>
+ Save...
+ </a>
+ </li>
+ <li role="presentation" className="divider"></li>
+ <li>
+ <a href="http://mitm.it/" target="_blank">
+ <i className="fa fa-fw fa-external-link"></i>
+ Install Certificates...
+ </a>
+ </li>
+ </ul>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/FilterDocs.jsx b/web/src/js/components/Header/FilterDocs.jsx
new file mode 100644
index 00000000..efb4818c
--- /dev/null
+++ b/web/src/js/components/Header/FilterDocs.jsx
@@ -0,0 +1,56 @@
+import React, { Component } from 'react'
+import $ from 'jquery'
+
+export default class FilterDocs extends Component {
+
+ // @todo move to redux
+
+ static xhr = null
+ static doc = null
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = { doc: FilterDocs.doc }
+ }
+
+ componentWillMount() {
+ if (!FilterDocs.xhr) {
+ FilterDocs.xhr = $.getJSON('/filter-help')
+ FilterDocs.xhr.fail(() => {
+ FilterDocs.xhr = null
+ })
+ }
+ if (!this.state.doc) {
+ FilterDocs.xhr.done(doc => {
+ FilterDocs.doc = doc
+ this.setState({ doc })
+ })
+ }
+ }
+
+ render() {
+ const { doc } = this.state
+ return !doc ? (
+ <i className="fa fa-spinner fa-spin"></i>
+ ) : (
+ <table className="table table-condensed">
+ <tbody>
+ {doc.commands.map(cmd => (
+ <tr key={cmd[1]}>
+ <td>{cmd[0].replace(' ', '\u00a0')}</td>
+ <td>{cmd[1]}</td>
+ </tr>
+ ))}
+ <tr key="docs-link">
+ <td colSpan="2">
+ <a href="http://docs.mitmproxy.org/en/stable/features/filters.html"
+ target="_blank">
+ <i className="fa fa-external-link"></i>
+ &nbsp mitmproxy docs</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/FilterInput.jsx b/web/src/js/components/Header/FilterInput.jsx
new file mode 100644
index 00000000..5b49b788
--- /dev/null
+++ b/web/src/js/components/Header/FilterInput.jsx
@@ -0,0 +1,133 @@
+import React, { PropTypes, Component } from 'react'
+import ReactDOM from 'react-dom'
+import classnames from 'classnames'
+import { Key } from '../../utils.js'
+import Filt from '../../filt/filt'
+import FilterDocs from './FilterDocs'
+
+export default class FilterInput extends Component {
+
+ static contextTypes = {
+ returnFocus: React.PropTypes.func,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+
+ // Consider both focus and mouseover for showing/hiding the tooltip,
+ // because onBlur of the input is triggered before the click on the tooltip
+ // finalized, hiding the tooltip just as the user clicks on it.
+ this.state = { value: this.props.value, focus: false, mousefocus: false }
+
+ this.onChange = this.onChange.bind(this)
+ this.onFocus = this.onFocus.bind(this)
+ this.onBlur = this.onBlur.bind(this)
+ this.onKeyDown = this.onKeyDown.bind(this)
+ this.onMouseEnter = this.onMouseEnter.bind(this)
+ this.onMouseLeave = this.onMouseLeave.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({ value: nextProps.value })
+ }
+
+ isValid(filt) {
+ try {
+ const str = filt == null ? this.state.value : filt
+ if (str) {
+ Filt.parse(str)
+ }
+ return true
+ } catch (e) {
+ return false
+ }
+ }
+
+ getDesc() {
+ if (!this.state.value) {
+ return <FilterDocs/>
+ }
+ try {
+ return Filt.parse(this.state.value).desc
+ } catch (e) {
+ return '' + e
+ }
+ }
+
+ onChange(e) {
+ const value = e.target.value
+ this.setState({ value })
+
+ // Only propagate valid filters upwards.
+ if (this.isValid(value)) {
+ this.props.onChange(value)
+ }
+ }
+
+ onFocus() {
+ this.setState({ focus: true })
+ }
+
+ onBlur() {
+ this.setState({ focus: false })
+ }
+
+ onMouseEnter() {
+ this.setState({ mousefocus: true })
+ }
+
+ onMouseLeave() {
+ this.setState({ mousefocus: false })
+ }
+
+ onKeyDown(e) {
+ if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) {
+ this.blur()
+ // If closed using ESC/ENTER, hide the tooltip.
+ this.setState({mousefocus: false})
+ }
+ e.stopPropagation()
+ }
+
+ blur() {
+ ReactDOM.findDOMNode(this.refs.input).blur()
+ this.context.returnFocus()
+ }
+
+ select() {
+ ReactDOM.findDOMNode(this.refs.input).select()
+ }
+
+ render() {
+ const { type, color, placeholder } = this.props
+ const { value, focus, mousefocus } = this.state
+ return (
+ <div className={classnames('filter-input input-group', { 'has-error': !this.isValid() })}>
+ <span className="input-group-addon">
+ <i className={'fa fa-fw fa-' + type} style={{ color }}></i>
+ </span>
+ <input
+ type="text"
+ ref="input"
+ placeholder={placeholder}
+ className="form-control"
+ value={value}
+ onChange={this.onChange}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ onKeyDown={this.onKeyDown}
+ />
+ {(focus || mousefocus) && (
+ <div className="popover bottom"
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}>
+ <div className="arrow"></div>
+ <div className="popover-content">
+ {this.getDesc()}
+ </div>
+ </div>
+ )}
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/MainMenu.jsx b/web/src/js/components/Header/MainMenu.jsx
new file mode 100644
index 00000000..86bf961a
--- /dev/null
+++ b/web/src/js/components/Header/MainMenu.jsx
@@ -0,0 +1,73 @@
+import React, { Component, PropTypes } from 'react'
+import { SettingsActions } from "../../actions.js"
+import FilterInput from './FilterInput'
+import { Query } from '../../actions.js'
+
+export default class MainMenu extends Component {
+
+ static title = 'Start'
+ static route = 'flows'
+
+ static propTypes = {
+ settings: React.PropTypes.object.isRequired,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+ this.onSearchChange = this.onSearchChange.bind(this)
+ this.onHighlightChange = this.onHighlightChange.bind(this)
+ this.onInterceptChange = this.onInterceptChange.bind(this)
+ }
+
+ onSearchChange(val) {
+ this.props.updateLocation(undefined, { [Query.SEARCH]: val })
+ }
+
+ onHighlightChange(val) {
+ this.props.updateLocation(undefined, { [Query.HIGHLIGHT]: val })
+ }
+
+ onInterceptChange(val) {
+ SettingsActions.update({ intercept: val })
+ }
+
+ render() {
+ const { query, settings } = this.props
+
+ const search = query[Query.SEARCH] || ''
+ const highlight = query[Query.HIGHLIGHT] || ''
+ const intercept = settings.intercept || ''
+
+ return (
+ <div>
+ <div className="menu-row">
+ <FilterInput
+ ref="search"
+ placeholder="Search"
+ type="search"
+ color="black"
+ value={search}
+ onChange={this.onSearchChange}
+ />
+ <FilterInput
+ ref="highlight"
+ placeholder="Highlight"
+ type="tag"
+ color="hsl(48, 100%, 50%)"
+ value={highlight}
+ onChange={this.onHighlightChange}
+ />
+ <FilterInput
+ ref="intercept"
+ placeholder="Intercept"
+ type="pause"
+ color="hsl(208, 56%, 53%)"
+ value={intercept}
+ onChange={this.onInterceptChange}
+ />
+ </div>
+ <div className="clearfix"></div>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
new file mode 100644
index 00000000..6bbf15d5
--- /dev/null
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -0,0 +1,60 @@
+import React, { PropTypes } from 'react'
+import { ToggleInputButton, ToggleButton } from '../common.js'
+import { SettingsActions } from '../../actions.js'
+
+OptionMenu.title = "Options"
+
+OptionMenu.propTypes = {
+ settings: PropTypes.object.isRequired,
+}
+
+export default function OptionMenu({ settings }) {
+ // @todo use settings.map
+ return (
+ <div>
+ <div className="menu-row">
+ <ToggleButton text="showhost"
+ checked={settings.showhost}
+ onToggle={() => SettingsActions.update({ showhost: !settings.showhost })}
+ />
+ <ToggleButton text="no_upstream_cert"
+ checked={settings.no_upstream_cert}
+ onToggle={() => SettingsActions.update({ no_upstream_cert: !settings.no_upstream_cert })}
+ />
+ <ToggleButton text="rawtcp"
+ checked={settings.rawtcp}
+ onToggle={() => SettingsActions.update({ rawtcp: !settings.rawtcp })}
+ />
+ <ToggleButton text="http2"
+ checked={settings.http2}
+ onToggle={() => SettingsActions.update({ http2: !settings.http2 })}
+ />
+ <ToggleButton text="anticache"
+ checked={settings.anticache}
+ onToggle={() => SettingsActions.update({ anticache: !settings.anticache })}
+ />
+ <ToggleButton text="anticomp"
+ checked={settings.anticomp}
+ onToggle={() => SettingsActions.update({ anticomp: !settings.anticomp })}
+ />
+ <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
+ checked={!!settings.stickyauth}
+ txt={settings.stickyauth || ''}
+ onToggleChanged={txt => SettingsActions.update({ stickyauth: !settings.stickyauth ? txt : null })}
+ />
+ <ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
+ checked={!!settings.stickycookie}
+ txt={settings.stickycookie || ''}
+ onToggleChanged={txt => SettingsActions.update({ stickycookie: !settings.stickycookie ? txt : null })}
+ />
+ <ToggleInputButton name="stream" placeholder="stream..."
+ checked={!!settings.stream}
+ txt={settings.stream || ''}
+ inputType="number"
+ onToggleChanged={txt => SettingsActions.update({ stream: !settings.stream ? txt : null })}
+ />
+ </div>
+ <div className="clearfix"/>
+ </div>
+ )
+}
diff --git a/web/src/js/components/Header/ViewMenu.jsx b/web/src/js/components/Header/ViewMenu.jsx
new file mode 100644
index 00000000..45359a83
--- /dev/null
+++ b/web/src/js/components/Header/ViewMenu.jsx
@@ -0,0 +1,33 @@
+import React, { PropTypes } from 'react'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+import { ToggleButton } from '../common.js'
+import { toggleEventLogVisibility } from '../../ducks/eventLog'
+
+ViewMenu.title = 'View'
+ViewMenu.route = 'flows'
+
+ViewMenu.propTypes = {
+ visible: PropTypes.bool.isRequired,
+ onToggle: PropTypes.func.isRequired,
+}
+
+function ViewMenu({ visible, onToggle }) {
+ return (
+ <div>
+ <div className="menu-row">
+ <ToggleButton text="Show Event Log" checked={visible} onToggle={onToggle} />
+ </div>
+ <div className="clearfix"></div>
+ </div>
+ )
+}
+
+export default connect(
+ state => ({
+ visible: state.eventLog.visible,
+ }),
+ dispatch => bindActionCreators({
+ onToggle: toggleEventLogVisibility,
+ }, dispatch)
+)(ViewMenu)
diff --git a/web/src/js/components/MainView.js b/web/src/js/components/MainView.jsx
index 6172ce77..dbea76e5 100644
--- a/web/src/js/components/MainView.js
+++ b/web/src/js/components/MainView.jsx
@@ -1,16 +1,22 @@
-import React, { Component } from "react"
-
-import { FlowActions } from "../actions.js"
-import { Query } from "../actions.js"
-import { Key } from "../utils.js"
-import { Splitter } from "./common.js"
-import FlowTable from "./flowtable.js"
-import FlowView from "./flowview/index.js"
+import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
-import { selectFlow, setFilter, setHighlight } from "../ducks/flows"
+import { bindActionCreators } from 'redux'
+
+import { FlowActions } from '../actions.js'
+import { Query } from '../actions.js'
+import { Key } from '../utils.js'
+import { Splitter } from './common.js'
+import FlowTable from './FlowTable'
+import FlowView from './flowview/index.js'
+import { selectFlow, setFilter, setHighlight } from '../ducks/flows'
class MainView extends Component {
+ static propTypes = {
+ highlight: PropTypes.string,
+ sort: PropTypes.object,
+ }
+
/**
* @todo move to actions
* @todo replace with mapStateToProps
@@ -33,9 +39,9 @@ class MainView extends Component {
*/
selectFlow(flow) {
if (flow) {
- this.props.updateLocation(`/flows/${flow.id}/${this.props.routeParams.detailTab || "request"}`)
+ this.props.updateLocation(`/flows/${flow.id}/${this.props.routeParams.detailTab || 'request'}`)
} else {
- this.props.updateLocation("/flows")
+ this.props.updateLocation('/flows')
}
}
@@ -143,20 +149,22 @@ class MainView extends Component {
case Key.SHIFT:
break
default:
- console.debug("keydown", e.keyCode)
+ console.debug('keydown', e.keyCode)
return
}
e.preventDefault()
}
render() {
- const { selectedFlow } = this.props
+ const { flows, selectedFlow, highlight, sort } = this.props
return (
<div className="main-view">
<FlowTable
ref="flowTable"
- selectFlow={flow => this.selectFlow(flow)}
+ flows={flows}
selected={selectedFlow}
+ highlight={highlight}
+ onSelect={flow => this.selectFlow(flow)}
/>
{selectedFlow && [
<Splitter key="splitter"/>,
@@ -178,14 +186,15 @@ export default connect(
state => ({
flows: state.flows.view,
filter: state.flows.filter,
+ sort: state.flows.sort,
highlight: state.flows.highlight,
selectedFlow: state.flows.all.byId[state.flows.selected[0]]
}),
- dispatch => ({
- selectFlow: flowId => dispatch(selectFlow(flowId)),
- setFilter: filter => dispatch(setFilter(filter)),
- setHighlight: highlight => dispatch(setHighlight(highlight))
- }),
+ dispatch => bindActionCreators({
+ selectFlow,
+ setFilter,
+ setHighlight,
+ }, dispatch),
undefined,
{ withRef: true }
)(MainView)
diff --git a/web/src/js/components/ProxyApp.js b/web/src/js/components/ProxyApp.jsx
index 71a7bf9b..81272268 100644
--- a/web/src/js/components/ProxyApp.js
+++ b/web/src/js/components/ProxyApp.jsx
@@ -4,9 +4,9 @@ import _ from "lodash"
import { connect } from 'react-redux'
import { Splitter } from "./common.js"
-import { Header, MainMenu } from "./header.js"
-import EventLog from "./eventlog.js"
-import Footer from "./footer.js"
+import Header from "./Header"
+import EventLog from "./EventLog"
+import Footer from "./Footer"
import { SettingsStore } from "../store/store.js"
import { Key } from "../utils.js"
@@ -31,6 +31,7 @@ class ProxyAppMain extends Component {
this.state = { settings: this.settingsStore.dict }
+ this.focus = this.focus.bind(this)
this.onKeyDown = this.onKeyDown.bind(this)
this.updateLocation = this.updateLocation.bind(this)
this.onSettingsChange = this.onSettingsChange.bind(this)
@@ -45,7 +46,7 @@ class ProxyAppMain extends Component {
}
const query = this.props.location.query
for (const key of Object.keys(queryUpdate || {})) {
- query[i] = queryUpdate[i] || undefined
+ query[key] = queryUpdate[key] || undefined
}
this.context.router.replace({ pathname, query })
}
@@ -133,7 +134,7 @@ class ProxyAppMain extends Component {
if (name) {
const headerComponent = this.refs.header
- headerComponent.setState({active: MainMenu}, function () {
+ headerComponent.setState({ active: Header.entries.MainMenu }, () => {
headerComponent.refs.active.refs[name].select()
})
}
diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js
deleted file mode 100644
index 6ada58ff..00000000
--- a/web/src/js/components/eventlog.js
+++ /dev/null
@@ -1,153 +0,0 @@
-import React from "react"
-import ReactDOM from "react-dom"
-import {connect} from 'react-redux'
-import shallowEqual from "shallowequal"
-import {toggleEventLogFilter, toggleEventLogVisibility} from "../ducks/eventLog"
-import AutoScroll from "./helpers/AutoScroll";
-import {calcVScroll} from "./helpers/VirtualScroll"
-import {ToggleButton} from "./common";
-
-function LogIcon({event}) {
- let icon = {web: "html5", debug: "bug"}[event.level] || "info";
- return <i className={`fa fa-fw fa-${icon}`}></i>
-}
-
-function LogEntry({event, registerHeight}) {
- return <div ref={registerHeight}>
- <LogIcon event={event}/>
- {event.message}
- </div>;
-}
-
-class EventLogContents extends React.Component {
-
- static defaultProps = {
- rowHeight: 18,
- };
-
- constructor(props) {
- super(props);
-
- this.heights = {};
- this.state = {vScroll: calcVScroll()};
-
- this.onViewportUpdate = this.onViewportUpdate.bind(this);
- }
-
- componentDidMount() {
- window.addEventListener("resize", this.onViewportUpdate);
- this.onViewportUpdate();
- }
-
- componentWillUnmount() {
- window.removeEventListener("resize", this.onViewportUpdate);
- }
-
- componentDidUpdate() {
- this.onViewportUpdate();
- }
-
- onViewportUpdate() {
- const viewport = ReactDOM.findDOMNode(this);
-
- const vScroll = calcVScroll({
- itemCount: this.props.events.length,
- rowHeight: this.props.rowHeight,
- viewportTop: viewport.scrollTop,
- viewportHeight: viewport.offsetHeight,
- itemHeights: this.props.events.map(entry => this.heights[entry.id]),
- });
-
- if (!shallowEqual(this.state.vScroll, vScroll)) {
- this.setState({vScroll});
- }
- }
-
- setHeight(id, node) {
- if (node && !this.heights[id]) {
- const height = node.offsetHeight;
- if (this.heights[id] !== height) {
- this.heights[id] = height;
- this.onViewportUpdate();
- }
- }
- }
-
- render() {
- const vScroll = this.state.vScroll;
- const events = this.props.events
- .slice(vScroll.start, vScroll.end)
- .map(event =>
- <LogEntry
- event={event}
- key={event.id}
- registerHeight={(node) => this.setHeight(event.id, node)}
- />
- );
-
- return (
- <pre onScroll={this.onViewportUpdate}>
- <div style={{ height: vScroll.paddingTop }}></div>
- {events}
- <div style={{ height: vScroll.paddingBottom }}></div>
- </pre>
- );
- }
-}
-
-EventLogContents = AutoScroll(EventLogContents);
-
-
-const EventLogContentsContainer = connect(
- state => ({
- events: state.eventLog.filteredEvents
- })
-)(EventLogContents);
-
-
-export const ToggleEventLog = connect(
- state => ({
- checked: state.eventLog.visible
- }),
- dispatch => ({
- onToggle: () => dispatch(toggleEventLogVisibility())
- })
-)(ToggleButton);
-
-
-const ToggleFilter = connect(
- (state, ownProps) => ({
- checked: state.eventLog.filter[ownProps.text]
- }),
- (dispatch, ownProps) => ({
- onToggle: () => dispatch(toggleEventLogFilter(ownProps.text))
- })
-)(ToggleButton);
-
-
-const EventLog = ({close}) =>
- <div className="eventlog">
- <div>
- Eventlog
- <div className="pull-right">
- <ToggleFilter text="debug"/>
- <ToggleFilter text="info"/>
- <ToggleFilter text="web"/>
- <i onClick={close} className="fa fa-close"></i>
- </div>
- </div>
- <EventLogContentsContainer/>
- </div>;
-
-EventLog.propTypes = {
- close: React.PropTypes.func.isRequired
-};
-
-const EventLogContainer = connect(
- undefined,
- dispatch => ({
- close: () => dispatch(toggleEventLogVisibility())
- })
-)(EventLog);
-
-export default EventLogContainer;
diff --git a/web/src/js/components/flowtable-columns.js b/web/src/js/components/flowtable-columns.js
deleted file mode 100644
index 799b3f9f..00000000
--- a/web/src/js/components/flowtable-columns.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import React from "react"
-import {RequestUtils, ResponseUtils} from "../flow/utils.js"
-import {formatSize, formatTimeDelta} from "../utils.js"
-
-
-export function TLSColumn({flow}) {
- let ssl = (flow.request.scheme === "https")
- let classes
- if (ssl) {
- classes = "col-tls col-tls-https"
- } else {
- classes = "col-tls col-tls-http"
- }
- return <td className={classes}></td>
-}
-TLSColumn.Title = ({className = "", ...props}) => <th {...props} className={"col-tls " + className }></th>
-TLSColumn.sortKeyFun = flow => flow.request.scheme
-
-
-export function IconColumn({flow}) {
- let icon
- if (flow.response) {
- var contentType = ResponseUtils.getContentType(flow.response)
-
- //TODO: We should assign a type to the flow somewhere else.
- if (flow.response.status_code === 304) {
- icon = "resource-icon-not-modified"
- } else if (300 <= flow.response.status_code && flow.response.status_code < 400) {
- icon = "resource-icon-redirect"
- } else if (contentType && contentType.indexOf("image") >= 0) {
- icon = "resource-icon-image"
- } else if (contentType && contentType.indexOf("javascript") >= 0) {
- icon = "resource-icon-js"
- } else if (contentType && contentType.indexOf("css") >= 0) {
- icon = "resource-icon-css"
- } else if (contentType && contentType.indexOf("html") >= 0) {
- icon = "resource-icon-document"
- }
- }
- if (!icon) {
- icon = "resource-icon-plain"
- }
-
- icon += " resource-icon"
- return <td className="col-icon">
- <div className={icon}></div>
- </td>
-}
-IconColumn.Title = ({className = "", ...props}) => <th {...props} className={"col-icon " + className }></th>
-
-
-export function PathColumn({flow}) {
- return <td className="col-path">
- {flow.request.is_replay ? <i className="fa fa-fw fa-repeat pull-right"></i> : null}
- {flow.intercepted ? <i className="fa fa-fw fa-pause pull-right"></i> : null}
- { RequestUtils.pretty_url(flow.request) }
- </td>
-}
-PathColumn.Title = ({className = "", ...props}) =>
- <th {...props} className={"col-path " + className }>Path</th>
-PathColumn.sortKeyFun = flow => RequestUtils.pretty_url(flow.request)
-
-
-export function MethodColumn({flow}) {
- return <td className="col-method">{flow.request.method}</td>
-}
-MethodColumn.Title = ({className = "", ...props}) =>
- <th {...props} className={"col-method " + className }>Method</th>
-MethodColumn.sortKeyFun = flow => flow.request.method
-
-
-export function StatusColumn({flow}) {
- let status
- if (flow.response) {
- status = flow.response.status_code
- } else {
- status = null
- }
- return <td className="col-status">{status}</td>
-
-}
-StatusColumn.Title = ({className = "", ...props}) =>
- <th {...props} className={"col-status " + className }>Status</th>
-StatusColumn.sortKeyFun = flow => flow.response ? flow.response.status_code : undefined
-
-
-export function SizeColumn({flow}) {
- let total = flow.request.contentLength
- if (flow.response) {
- total += flow.response.contentLength || 0
- }
- let size = formatSize(total)
- return <td className="col-size">{size}</td>
-
-}
-SizeColumn.Title = ({className = "", ...props}) =>
- <th {...props} className={"col-size " + className }>Size</th>
-SizeColumn.sortKeyFun = flow => {
- let total = flow.request.contentLength
- if (flow.response) {
- total += flow.response.contentLength || 0
- }
- return total
-}
-
-
-export function TimeColumn({flow}) {
- let time
- if (flow.response) {
- time = formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
- } else {
- time = "..."
- }
- return <td className="col-time">{time}</td>
-}
-TimeColumn.Title = ({className = "", ...props}) =>
- <th {...props} className={"col-time " + className }>Time</th>
-TimeColumn.sortKeyFun = flow => flow.response.timestamp_end - flow.request.timestamp_start
-
-
-var all_columns = [
- TLSColumn,
- IconColumn,
- PathColumn,
- MethodColumn,
- StatusColumn,
- SizeColumn,
- TimeColumn
-]
-
-export default all_columns
diff --git a/web/src/js/components/flowtable.js b/web/src/js/components/flowtable.js
deleted file mode 100644
index 5a0793ba..00000000
--- a/web/src/js/components/flowtable.js
+++ /dev/null
@@ -1,201 +0,0 @@
-import React from "react";
-import ReactDOM from "react-dom";
-import {connect} from 'react-redux'
-import classNames from "classnames";
-import _ from "lodash";
-import shallowEqual from "shallowequal";
-import AutoScroll from "./helpers/AutoScroll";
-import {calcVScroll} from "./helpers/VirtualScroll";
-import flowtable_columns from "./flowtable-columns.js";
-import Filt from "../filt/filt";
-import {setSort} from "../ducks/flows";
-
-
-FlowRow.propTypes = {
- selectFlow: React.PropTypes.func.isRequired,
- columns: React.PropTypes.array.isRequired,
- flow: React.PropTypes.object.isRequired,
- highlight: React.PropTypes.string,
- selected: React.PropTypes.bool,
-};
-
-function FlowRow({flow, selected, highlight, columns, selectFlow}) {
-
- const className = classNames({
- "selected": selected,
- "highlighted": highlight && parseFilter(highlight)(flow),
- "intercepted": flow.intercepted,
- "has-request": flow.request,
- "has-response": flow.response,
- });
-
- return (
- <tr className={className} onClick={() => selectFlow(flow)}>
- {columns.map(Column => (
- <Column key={Column.name} flow={flow}/>
- ))}
- </tr>
- );
-}
-
-const FlowRowContainer = connect(
- (state, ownProps) => ({
- flow: state.flows.all.byId[ownProps.flowId],
- highlight: state.flows.highlight,
- selected: state.flows.selected.indexOf(ownProps.flowId) >= 0
- })
-)(FlowRow)
-
-function FlowTableHead({setSort, columns, sort}) {
- const sortColumn = sort.sortColumn;
- const sortType = sort.sortDesc ? "sort-desc" : "sort-asc";
-
- return (
- <tr>
- {columns.map(Column => (
- <Column.Title
- key={Column.name}
- onClick={() => setSort({sortColumn: Column.name, sortDesc: Column.name != sort.sortColumn ? false : !sort.sortDesc})}
- className={sortColumn === Column.name ? sortType : undefined}
- />
- ))}
- </tr>
- );
-}
-
-FlowTableHead.propTypes = {
- setSort: React.PropTypes.func.isRequired,
- sort: React.PropTypes.object.isRequired,
- columns: React.PropTypes.array.isRequired
-};
-
-const FlowTableHeadContainer = connect(
- state => ({
- sort: state.flows.sort
- }),
- dispatch => ({
- setSort: (sort) => dispatch(setSort(sort)),
- })
-)(FlowTableHead)
-
-class FlowTable extends React.Component {
-
- static propTypes = {
- rowHeight: React.PropTypes.number,
- };
-
- static defaultProps = {
- rowHeight: 32,
- };
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {vScroll: calcVScroll()};
-
- this.onViewportUpdate = this.onViewportUpdate.bind(this);
- }
-
- componentWillMount() {
- window.addEventListener("resize", this.onViewportUpdate);
- }
-
- componentWillUnmount() {
- window.removeEventListener("resize", this.onViewportUpdate);
- }
-
- componentWillReceiveProps(nextProps) {
- if (nextProps.selected && nextProps.selected !== this.props.selected) {
- window.setTimeout(() => this.scrollIntoView(nextProps.selected), 1)
- }
- }
-
- componentDidUpdate() {
- this.onViewportUpdate();
- }
-
- onViewportUpdate() {
- const viewport = ReactDOM.findDOMNode(this);
- const viewportTop = viewport.scrollTop;
-
- const vScroll = calcVScroll({
- viewportTop,
- viewportHeight: viewport.offsetHeight,
- itemCount: this.props.flows.length,
- rowHeight: this.props.rowHeight,
- });
-
- if (!shallowEqual(this.state.vScroll, vScroll) ||
- this.state.viewportTop !== viewportTop) {
- this.setState({vScroll, viewportTop});
- }
- }
-
- scrollIntoView(flow) {
- const viewport = ReactDOM.findDOMNode(this);
- const index = this.props.flows.indexOf(flow);
- const rowHeight = this.props.rowHeight;
- const head = ReactDOM.findDOMNode(this.refs.head);
-
- const headHeight = head ? head.offsetHeight : 0;
-
- const rowTop = (index * rowHeight) + headHeight;
- const rowBottom = rowTop + rowHeight;
-
- const viewportTop = viewport.scrollTop;
- const viewportHeight = viewport.offsetHeight;
-
- // Account for pinned thead
- if (rowTop - headHeight < viewportTop) {
- viewport.scrollTop = rowTop - headHeight;
- } else if (rowBottom > viewportTop + viewportHeight) {
- viewport.scrollTop = rowBottom - viewportHeight;
- }
- }
-
- render() {
- const vScroll = this.state.vScroll;
- const flows = this.props.flows.slice(vScroll.start, vScroll.end);
-
- const transform = `translate(0,${this.state.viewportTop}px)`;
-
- return (
- <div className="flow-table" onScroll={this.onViewportUpdate}>
- <table>
- <thead ref="head" style={{ transform }}>
- <FlowTableHeadContainer
- columns={flowtable_columns}
- setSortKeyFun={this.props.setSortKeyFun}
- setSort={this.props.setSort}
- />
- </thead>
- <tbody>
- <tr style={{ height: vScroll.paddingTop }}></tr>
- {flows.map(flow => (
- <FlowRowContainer
- key={flow.id}
- flowId={flow.id}
- columns={flowtable_columns}
- selectFlow={this.props.selectFlow}
- />
- ))}
- <tr style={{ height: vScroll.paddingBottom }}></tr>
- </tbody>
- </table>
- </div>
- );
- }
-}
-
-FlowTable = AutoScroll(FlowTable)
-
-
-const parseFilter = _.memoize(Filt.parse)
-
-const FlowTableContainer = connect(
- state => ({
- flows: state.flows.view
- })
-)(FlowTable)
-
-export default FlowTableContainer;
diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js
deleted file mode 100644
index 4152e95c..00000000
--- a/web/src/js/components/header.js
+++ /dev/null
@@ -1,453 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import $ from "jquery";
-import {connect} from 'react-redux'
-
-import Filt from "../filt/filt.js";
-import {Key} from "../utils.js";
-import {ToggleInputButton, ToggleButton} from "./common.js";
-import {SettingsActions, FlowActions} from "../actions.js";
-import {Query} from "../actions.js";
-import {SettingsState} from "./common.js";
-import {ToggleEventLog} from "./eventlog"
-
-var FilterDocs = React.createClass({
- statics: {
- xhr: false,
- doc: false
- },
- componentWillMount: function () {
- if (!FilterDocs.doc) {
- FilterDocs.xhr = $.getJSON("/filter-help").done(function (doc) {
- FilterDocs.doc = doc;
- FilterDocs.xhr = false;
- });
- }
- if (FilterDocs.xhr) {
- FilterDocs.xhr.done(function () {
- this.forceUpdate();
- }.bind(this));
- }
- },
- render: function () {
- if (!FilterDocs.doc) {
- return <i className="fa fa-spinner fa-spin"></i>;
- } else {
- var commands = FilterDocs.doc.commands.map(function (c) {
- return <tr key={c[1]}>
- <td>{c[0].replace(" ", '\u00a0')}</td>
- <td>{c[1]}</td>
- </tr>;
- });
- commands.push(<tr key="docs-link">
- <td colSpan="2">
- <a href="http://docs.mitmproxy.org/en/stable/features/filters.html"
- target="_blank">
- <i className="fa fa-external-link"></i>
- &nbsp; mitmproxy docs</a>
- </td>
- </tr>);
- return <table className="table table-condensed">
- <tbody>{commands}</tbody>
- </table>;
- }
- }
-});
-var FilterInput = React.createClass({
- contextTypes: {
- returnFocus: React.PropTypes.func
- },
- getInitialState: function () {
- // Consider both focus and mouseover for showing/hiding the tooltip,
- // because onBlur of the input is triggered before the click on the tooltip
- // finalized, hiding the tooltip just as the user clicks on it.
- return {
- value: this.props.value,
- focus: false,
- mousefocus: false
- };
- },
- componentWillReceiveProps: function (nextProps) {
- this.setState({value: nextProps.value});
- },
- onChange: function (e) {
- var nextValue = e.target.value;
- this.setState({
- value: nextValue
- });
- // Only propagate valid filters upwards.
- if (this.isValid(nextValue)) {
- this.props.onChange(nextValue);
- }
- },
- isValid: function (filt) {
- try {
- var str = filt || this.state.value;
- if(str){
- Filt.parse(filt || this.state.value);
- }
- return true;
- } catch (e) {
- return false;
- }
- },
- getDesc: function () {
- if(this.state.value) {
- try {
- return Filt.parse(this.state.value).desc;
- } catch (e) {
- return "" + e;
- }
- }
- return <FilterDocs/>;
- },
- onFocus: function () {
- this.setState({focus: true});
- },
- onBlur: function () {
- this.setState({focus: false});
- },
- onMouseEnter: function () {
- this.setState({mousefocus: true});
- },
- onMouseLeave: function () {
- this.setState({mousefocus: false});
- },
- onKeyDown: function (e) {
- if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) {
- this.blur();
- // If closed using ESC/ENTER, hide the tooltip.
- this.setState({mousefocus: false});
- }
- e.stopPropagation();
- },
- blur: function () {
- ReactDOM.findDOMNode(this.refs.input).blur();
- this.context.returnFocus();
- },
- select: function () {
- ReactDOM.findDOMNode(this.refs.input).select();
- },
- render: function () {
- var isValid = this.isValid();
- var icon = "fa fa-fw fa-" + this.props.type;
- var groupClassName = "filter-input input-group" + (isValid ? "" : " has-error");
-
- var popover;
- if (this.state.focus || this.state.mousefocus) {
- popover = (
- <div className="popover bottom" onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
- <div className="arrow"></div>
- <div className="popover-content">
- {this.getDesc()}
- </div>
- </div>
- );
- }
-
- return (
- <div className={groupClassName}>
- <span className="input-group-addon">
- <i className={icon} style={{color: this.props.color}}></i>
- </span>
- <input type="text" placeholder={this.props.placeholder} className="form-control"
- ref="input"
- onChange={this.onChange}
- onFocus={this.onFocus}
- onBlur={this.onBlur}
- onKeyDown={this.onKeyDown}
- value={this.state.value}/>
- {popover}
- </div>
- );
- }
-});
-
-export var MainMenu = React.createClass({
- propTypes: {
- settings: React.PropTypes.object.isRequired,
- },
- statics: {
- title: "Start",
- route: "flows"
- },
- onSearchChange: function (val) {
- var d = {};
- d[Query.SEARCH] = val;
- this.props.updateLocation(undefined, d);
- },
- onHighlightChange: function (val) {
- var d = {};
- d[Query.HIGHLIGHT] = val;
- this.props.updateLocation(undefined, d);
- },
- onInterceptChange: function (val) {
- SettingsActions.update({intercept: val});
- },
- render: function () {
- var search = this.props.query[Query.SEARCH] || "";
- var highlight = this.props.query[Query.HIGHLIGHT] || "";
- var intercept = this.props.settings.intercept || "";
-
- return (
- <div>
- <div className="menu-row">
- <FilterInput
- ref="search"
- placeholder="Search"
- type="search"
- color="black"
- value={search}
- onChange={this.onSearchChange} />
- <FilterInput
- ref="highlight"
- placeholder="Highlight"
- type="tag"
- color="hsl(48, 100%, 50%)"
- value={highlight}
- onChange={this.onHighlightChange}/>
- <FilterInput
- ref="intercept"
- placeholder="Intercept"
- type="pause"
- color="hsl(208, 56%, 53%)"
- value={intercept}
- onChange={this.onInterceptChange}/>
- </div>
- <div className="clearfix"></div>
- </div>
- );
- }
-});
-
-
-var ViewMenu = React.createClass({
- statics: {
- title: "View",
- route: "flows"
- },
- render: function () {
- return (
- <div>
- <div className="menu-row">
- <ToggleEventLog text="Show Event Log"/>
- </div>
- <div className="clearfix"></div>
- </div>
- );
- }
-});
-
-export const OptionMenu = (props) => {
- const {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickycookie, stickyauth, stream} = props.settings;
- return (
- <div>
- <div className="menu-row">
- <ToggleButton text="showhost"
- checked={showhost}
- onToggle={() => SettingsActions.update({showhost: !showhost})}
- />
- <ToggleButton text="no_upstream_cert"
- checked={no_upstream_cert}
- onToggle={() => SettingsActions.update({no_upstream_cert: !no_upstream_cert})}
- />
- <ToggleButton text="rawtcp"
- checked={rawtcp}
- onToggle={() => SettingsActions.update({rawtcp: !rawtcp})}
- />
- <ToggleButton text="http2"
- checked={http2}
- onToggle={() => SettingsActions.update({http2: !http2})}
- />
- <ToggleButton text="anticache"
- checked={anticache}
- onToggle={() => SettingsActions.update({anticache: !anticache})}
- />
- <ToggleButton text="anticomp"
- checked={anticomp}
- onToggle={() => SettingsActions.update({anticomp: !anticomp})}
- />
- <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
- checked={Boolean(stickyauth)}
- txt={stickyauth || ""}
- onToggleChanged={txt => SettingsActions.update({stickyauth: (!stickyauth ? txt : null)})}
- />
- <ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
- checked={Boolean(stickycookie)}
- txt={stickycookie || ""}
- onToggleChanged={txt => SettingsActions.update({stickycookie: (!stickycookie ? txt : null)})}
- />
- <ToggleInputButton name="stream" placeholder="stream..."
- checked={Boolean(stream)}
- txt={stream || ""}
- inputType = "number"
- onToggleChanged={txt => SettingsActions.update({stream: (!stream ? txt : null)})}
- />
- </div>
- <div className="clearfix"/>
- </div>
- );
-};
-OptionMenu.title = "Options";
-
-OptionMenu.propTypes = {
- settings: React.PropTypes.object.isRequired
-};
-
-var ReportsMenu = React.createClass({
- statics: {
- title: "Visualization",
- route: "reports"
- },
- render: function () {
- return <div>Reports Menu</div>;
- }
-});
-
-var FileMenu = React.createClass({
- getInitialState: function () {
- return {
- showFileMenu: false
- };
- },
- handleFileClick: function (e) {
- e.preventDefault();
- if (!this.state.showFileMenu) {
- var close = function () {
- this.setState({showFileMenu: false});
- document.removeEventListener("click", close);
- }.bind(this);
- document.addEventListener("click", close);
-
- this.setState({
- showFileMenu: true
- });
- }
- },
- handleNewClick: function (e) {
- e.preventDefault();
- if (confirm("Delete all flows?")) {
- FlowActions.clear();
- }
- },
- handleOpenClick: function (e) {
- this.fileInput.click();
- e.preventDefault();
- },
- handleOpenFile: function (e) {
- if (e.target.files.length > 0) {
- FlowActions.upload(e.target.files[0]);
- this.fileInput.value = "";
- }
- e.preventDefault();
- },
- handleSaveClick: function (e) {
- e.preventDefault();
- FlowActions.download();
- },
- handleShutdownClick: function (e) {
- e.preventDefault();
- console.error("unimplemented: handleShutdownClick");
- },
- render: function () {
- var fileMenuClass = "dropdown pull-left" + (this.state.showFileMenu ? " open" : "");
-
- return (
- <div className={fileMenuClass}>
- <a href="#" className="special" onClick={this.handleFileClick}> mitmproxy </a>
- <ul className="dropdown-menu" role="menu">
- <li>
- <a href="#" onClick={this.handleNewClick}>
- <i className="fa fa-fw fa-file"></i>
- New
- </a>
- </li>
- <li>
- <a href="#" onClick={this.handleOpenClick}>
- <i className="fa fa-fw fa-folder-open"></i>
- Open...
- </a>
- <input ref={(ref) => this.fileInput = ref} className="hidden" type="file" onChange={this.handleOpenFile}/>
-
- </li>
- <li>
- <a href="#" onClick={this.handleSaveClick}>
- <i className="fa fa-fw fa-floppy-o"></i>
- Save...
- </a>
- </li>
- <li role="presentation" className="divider"></li>
- <li>
- <a href="http://mitm.it/" target="_blank">
- <i className="fa fa-fw fa-external-link"></i>
- Install Certificates...
- </a>
- </li>
- {/*
- <li role="presentation" className="divider"></li>
- <li>
- <a href="#" onClick={this.handleShutdownClick}>
- <i className="fa fa-fw fa-plug"></i>
- Shutdown
- </a>
- </li>
- */}
- </ul>
- </div>
- );
- }
-});
-
-
-var header_entries = [MainMenu, ViewMenu, OptionMenu /*, ReportsMenu */];
-
-
-export var Header = React.createClass({
- propTypes: {
- settings: React.PropTypes.object.isRequired,
- },
- getInitialState: function () {
- return {
- active: header_entries[0]
- };
- },
- handleClick: function (active, e) {
- e.preventDefault();
- this.props.updateLocation(active.route);
- this.setState({active: active});
- },
- render: function () {
- var header = header_entries.map(function (entry, i) {
- var className;
- if (entry === this.state.active) {
- className = "active";
- } else {
- className = "";
- }
- return (
- <a key={i}
- href="#"
- className={className}
- onClick={this.handleClick.bind(this, entry)}>
- {entry.title}
- </a>
- );
- }.bind(this));
-
- return (
- <header>
- <nav className="nav-tabs nav-tabs-lg">
- <FileMenu/>
- {header}
- </nav>
- <div className="menu">
- <this.state.active
- settings={this.props.settings}
- updateLocation={this.props.updateLocation}
- query={this.props.query}
- />
- </div>
- </header>
- );
- }
-});
diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js
index e324f7d4..5ab26b82 100644
--- a/web/src/js/components/prompt.js
+++ b/web/src/js/components/prompt.js
@@ -42,7 +42,7 @@ var Prompt = React.createClass({
var opts = [];
var keyTaken = function (k) {
- return _.includes(_.pluck(opts, "key"), k);
+ return _.includes(_.map(opts, "key"), k);
};
for (var i = 0; i < this.props.options.length; i++) {
@@ -99,4 +99,4 @@ var Prompt = React.createClass({
}
});
-export default Prompt; \ No newline at end of file
+export default Prompt;