diff options
Diffstat (limited to 'web/src/js/components/Header')
-rw-r--r-- | web/src/js/components/Header/FileMenu.jsx | 100 | ||||
-rw-r--r-- | web/src/js/components/Header/FilterDocs.jsx | 56 | ||||
-rw-r--r-- | web/src/js/components/Header/FilterInput.jsx | 133 | ||||
-rw-r--r-- | web/src/js/components/Header/MainMenu.jsx | 73 | ||||
-rw-r--r-- | web/src/js/components/Header/OptionMenu.jsx | 60 | ||||
-rw-r--r-- | web/src/js/components/Header/ViewMenu.jsx | 33 |
6 files changed, 455 insertions, 0 deletions
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> +   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) |