diff options
Diffstat (limited to 'web/src/js/components')
26 files changed, 536 insertions, 377 deletions
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index a79bf9e5..cb4749c5 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' -import * as ContentViews from './ContentView/ContentViews' +import { Edit, ViewServer, ViewImage } from './ContentView/ContentViews' import * as MetaViews from './ContentView/MetaViews' import ShowFullContentButton from './ContentView/ShowFullContentButton' @@ -16,7 +16,7 @@ ContentView.propTypes = { message: PropTypes.object.isRequired, } -ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2) +ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ViewImage.matches(msg) ? 10 : 0.2) function ContentView(props) { const { flow, message, contentView, isDisplayLarge, displayLarge, onContentChange, readonly } = props @@ -33,10 +33,15 @@ function ContentView(props) { return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/> } - const View = ContentViews[contentView] || ContentViews['ViewServer'] + let view; + if(contentView === "Edit") { + view = <Edit flow={flow} message={message} onChange={onContentChange}/> + } else { + view = <ViewServer flow={flow} message={message} contentView={contentView}/> + } return ( <div className="contentview"> - <View flow={flow} message={message} contentView={contentView} readonly={readonly} onChange={onContentChange}/> + {view} <ShowFullContentButton/> </div> ) diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index 4cafde28..44716e12 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -2,98 +2,100 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { MessageUtils } from '../../flow/utils.js' -export default View => class extends React.Component { +export default function withContentLoader(View) { + + return class extends React.Component { + static displayName = View.displayName || View.name + static matches = View.matches - static displayName = View.displayName || View.name - static matches = View.matches - - static propTypes = { - ...View.propTypes, - content: PropTypes.string, // mark as non-required - flow: PropTypes.object.isRequired, - message: PropTypes.object.isRequired, - } - - constructor(props) { - super(props) - this.state = { - content: undefined, - request: undefined, + static propTypes = { + ...View.propTypes, + content: PropTypes.string, // mark as non-required + flow: PropTypes.object.isRequired, + message: PropTypes.object.isRequired, } - } - componentWillMount() { - this.updateContent(this.props) - } - - componentWillReceiveProps(nextProps) { - if ( - nextProps.message.content !== this.props.message.content || - nextProps.message.contentHash !== this.props.message.contentHash || - nextProps.contentView !== this.props.contentView - ) { - this.updateContent(nextProps) + constructor(props) { + super(props) + this.state = { + content: undefined, + request: undefined, + } } - } - componentWillUnmount() { - if (this.state.request) { - this.state.request.abort() + componentWillMount() { + this.updateContent(this.props) } - } - updateContent(props) { - if (this.state.request) { - this.state.request.abort() + componentWillReceiveProps(nextProps) { + if ( + nextProps.message.content !== this.props.message.content || + nextProps.message.contentHash !== this.props.message.contentHash || + nextProps.contentView !== this.props.contentView + ) { + this.updateContent(nextProps) + } } - // We have a few special cases where we do not need to make an HTTP request. - if(props.message.content !== undefined) { - return this.setState({request: undefined, content: props.message.content}) - } - if(props.message.contentLength === 0 || props.message.contentLength === null){ - return this.setState({request: undefined, content: ""}) + + componentWillUnmount() { + if (this.state.request) { + this.state.request.abort() + } } - let requestUrl = MessageUtils.getContentURL(props.flow, props.message, (View.name == 'ViewServer' ? props.contentView : undefined)) + updateContent(props) { + if (this.state.request) { + this.state.request.abort() + } + // We have a few special cases where we do not need to make an HTTP request. + if (props.message.content !== undefined) { + return this.setState({request: undefined, content: props.message.content}) + } + if (props.message.contentLength === 0 || props.message.contentLength === null) { + return this.setState({request: undefined, content: ""}) + } - // We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable. - let request = new XMLHttpRequest(); - request.addEventListener("load", this.requestComplete.bind(this, request)); - request.addEventListener("error", this.requestFailed.bind(this, request)); - request.open("GET", requestUrl); - request.send(); - this.setState({ request, content: undefined }) - } + let requestUrl = MessageUtils.getContentURL(props.flow, props.message, props.contentView) - requestComplete(request, e) { - if (request !== this.state.request) { - return // Stale request + // We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable. + let request = new XMLHttpRequest(); + request.addEventListener("load", this.requestComplete.bind(this, request)); + request.addEventListener("error", this.requestFailed.bind(this, request)); + request.open("GET", requestUrl); + request.send(); + this.setState({request, content: undefined}) } - this.setState({ - content: request.responseText, - request: undefined - }) - } - requestFailed(request, e) { - if (request !== this.state.request) { - return // Stale request + requestComplete(request, e) { + if (request !== this.state.request) { + return // Stale request + } + this.setState({ + content: request.responseText, + request: undefined + }) + } + + requestFailed(request, e) { + if (request !== this.state.request) { + return // Stale request + } + console.error(e) + // FIXME: Better error handling + this.setState({ + content: "Error getting content.", + request: undefined + }) } - console.error(e) - // FIXME: Better error handling - this.setState({ - content: "Error getting content.", - request: undefined - }) - } - render() { - return this.state.content !== undefined ? ( - <View content={this.state.content} {...this.props}/> - ) : ( - <div className="text-center"> - <i className="fa fa-spinner fa-spin"></i> - </div> - ) + render() { + return this.state.content !== undefined ? ( + <View content={this.state.content} {...this.props}/> + ) : ( + <div className="text-center"> + <i className="fa fa-spinner fa-spin"></i> + </div> + ) + } } }; diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index 136188d4..387c940a 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -2,7 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import { setContentViewDescription, setContent } from '../../ducks/ui/flow' -import ContentLoader from './ContentLoader' +import withContentLoader from './ContentLoader' import { MessageUtils } from '../../flow/utils' import CodeEditor from './CodeEditor' @@ -28,9 +28,9 @@ Edit.propTypes = { function Edit({ content, onChange }) { return <CodeEditor content={content} onChange={onChange}/> } -Edit = ContentLoader(Edit) +Edit = withContentLoader(Edit) -class ViewServer extends Component { +export class PureViewServer extends Component { static propTypes = { showFullContent: PropTypes.bool.isRequired, maxLines: PropTypes.number.isRequired, @@ -85,7 +85,7 @@ class ViewServer extends Component { } -ViewServer = connect( +const ViewServer = connect( state => ({ showFullContent: state.ui.flow.showFullContent, maxLines: state.ui.flow.maxContentLines @@ -94,6 +94,6 @@ ViewServer = connect( setContentViewDescription, setContent } -)(ContentLoader(ViewServer)) +)(withContentLoader(PureViewServer)) export { Edit, ViewServer, ViewImage } diff --git a/web/src/js/components/ContentView/DownloadContentButton.jsx b/web/src/js/components/ContentView/DownloadContentButton.jsx index 447db211..f32a19ca 100644 --- a/web/src/js/components/ContentView/DownloadContentButton.jsx +++ b/web/src/js/components/ContentView/DownloadContentButton.jsx @@ -1,3 +1,4 @@ +import React from 'react' import { MessageUtils } from "../../flow/utils" import PropTypes from 'prop-types' diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx index fd627ad9..c6d8c2f2 100644 --- a/web/src/js/components/ContentView/ShowFullContentButton.jsx +++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx @@ -12,7 +12,7 @@ ShowFullContentButton.propTypes = { showFullContent: PropTypes.bool.isRequired } -function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLines, contentLines} ){ +export function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLines, contentLines} ){ return ( !showFullContent && diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx index 0021593f..847d4eb0 100644 --- a/web/src/js/components/ContentView/UploadContentButton.jsx +++ b/web/src/js/components/ContentView/UploadContentButton.jsx @@ -1,3 +1,4 @@ +import React from 'react' import PropTypes from 'prop-types' import FileChooser from '../common/FileChooser' diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx index 4c99d5ed..812d87e5 100644 --- a/web/src/js/components/ContentView/ViewSelector.jsx +++ b/web/src/js/components/ContentView/ViewSelector.jsx @@ -11,7 +11,7 @@ ViewSelector.propTypes = { setContentView: PropTypes.func.isRequired } -function ViewSelector ({contentViews, activeView, setContentView}){ +export function ViewSelector ({contentViews, activeView, setContentView}){ let inner = <span> <b>View:</b> {activeView.toLowerCase()} <span className="caret"></span> </span> return ( diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx index a83cdb28..40fe900e 100644 --- a/web/src/js/components/EventLog.jsx +++ b/web/src/js/components/EventLog.jsx @@ -5,7 +5,7 @@ import { toggleFilter, toggleVisibility } from '../ducks/eventLog' import ToggleButton from './common/ToggleButton' import EventList from './EventLog/EventList' -class EventLog extends Component { +export class PureEventLog extends Component { static propTypes = { filters: PropTypes.object.isRequired, @@ -77,4 +77,4 @@ export default connect( close: toggleVisibility, toggleFilter: toggleFilter, } -)(EventLog) +)(PureEventLog) diff --git a/web/src/js/components/FlowTable.jsx b/web/src/js/components/FlowTable.jsx index 24c1f3a1..a6381d0d 100644 --- a/web/src/js/components/FlowTable.jsx +++ b/web/src/js/components/FlowTable.jsx @@ -1,17 +1,20 @@ import React from 'react' import PropTypes from 'prop-types' import ReactDOM from 'react-dom' +import { connect } from 'react-redux' 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" +import * as flowsActions from '../ducks/flows' + class FlowTable extends React.Component { static propTypes = { - onSelect: PropTypes.func.isRequired, + selectFlow: PropTypes.func.isRequired, flows: PropTypes.array.isRequired, rowHeight: PropTypes.number, highlight: PropTypes.string, @@ -107,7 +110,7 @@ class FlowTable extends React.Component { flow={flow} selected={flow === selected} highlighted={isHighlighted(flow)} - onSelect={this.props.onSelect} + onSelect={this.props.selectFlow} /> ))} <tr style={{ height: vScroll.paddingBottom }}></tr> @@ -118,4 +121,15 @@ class FlowTable extends React.Component { } } -export default AutoScroll(FlowTable) +export const PureFlowTable = AutoScroll(FlowTable) + +export default connect( + state => ({ + flows: state.flows.view, + highlight: state.flows.highlight, + selected: state.flows.byId[state.flows.selected[0]], + }), + { + selectFlow: flowsActions.select, + } +)(PureFlowTable) diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx index d03d681a..25e7bb9f 100644 --- a/web/src/js/components/FlowView.jsx +++ b/web/src/js/components/FlowView.jsx @@ -3,94 +3,47 @@ import { connect } from 'react-redux' import _ from 'lodash' import Nav from './FlowView/Nav' -import { Request, Response, ErrorView as Error } from './FlowView/Messages' +import { ErrorView as Error, Request, Response } from './FlowView/Messages' import Details from './FlowView/Details' -import Prompt from './Prompt' import { selectTab } from '../ducks/ui/flow' -class FlowView extends Component { +export const allTabs = { Request, Response, Error, Details } - static allTabs = { Request, Response, Error, Details } +function FlowView({ flow, tabName, selectTab }) { - constructor(props, context) { - super(props, context) - this.onPromptFinish = this.onPromptFinish.bind(this) - } + // only display available tab names + const tabs = ['request', 'response', 'error'].filter(k => flow[k]) + tabs.push("details") - onPromptFinish(edit) { - this.props.setPrompt(false) - if (edit && this.tabComponent) { - this.tabComponent.edit(edit) + if (tabs.indexOf(tabName) < 0) { + if (tabName === 'response' && flow.error) { + tabName = 'error' + } else if (tabName === 'error' && flow.response) { + tabName = 'response' + } else { + tabName = tabs[0] } } - getPromptOptions() { - switch (this.props.tab) { - - case 'request': - return [ - 'method', - 'url', - { text: 'http version', key: 'v' }, - 'header' - ] - break - - case 'response': - return [ - { text: 'http version', key: 'v' }, - 'code', - 'message', - 'header' - ] - break - - case 'details': - return - - default: - throw 'Unknown tab for edit: ' + this.props.tab - } - } - - render() { - let { flow, tab: active, updateFlow } = this.props - const tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']) - - if (tabs.indexOf(active) < 0) { - if (active === 'response' && flow.error) { - active = 'error' - } else if (active === 'error' && flow.response) { - active = 'response' - } else { - active = tabs[0] - } - } - - const Tab = FlowView.allTabs[_.capitalize(active)] - - return ( - <div className="flow-detail"> - <Nav - flow={flow} - tabs={tabs} - active={active} - onSelectTab={this.props.selectTab} - /> - <Tab ref={ tab => this.tabComponent = tab } flow={flow} updateFlow={updateFlow} /> - {this.props.promptOpen && ( - <Prompt options={this.getPromptOptions()} done={this.onPromptFinish} /> - )} - </div> - ) - } + const Tab = allTabs[_.capitalize(tabName)] + + return ( + <div className="flow-detail"> + <Nav + tabs={tabs} + active={tabName} + onSelectTab={selectTab} + /> + <Tab flow={flow}/> + </div> + ) } export default connect( state => ({ - promptOpen: state.ui.promptOpen, - tab: state.ui.flow.tab + flow: state.flows.byId[state.flows.selected[0]], + tabName: state.ui.flow.tab, }), { selectTab, diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx index 4a31faf4..c1af36c5 100644 --- a/web/src/js/components/FlowView/Messages.jsx +++ b/web/src/js/components/FlowView/Messages.jsx @@ -9,6 +9,7 @@ import ContentView from '../ContentView' import ContentViewOptions from '../ContentView/ContentViewOptions' import ValidateEditor from '../ValueEditor/ValidateEditor' import ValueEditor from '../ValueEditor/ValueEditor' +import HideInStatic from '../common/HideInStatic' import Headers from './Headers' import { startEdit, updateEdit } from '../../ducks/ui/flow' @@ -105,6 +106,7 @@ export class Request extends Component { onContentChange={content => updateFlow({ request: {content}})} message={flow.request}/> </article> + <HideInStatic> {!noContent && <footer> <ContentViewOptions @@ -114,33 +116,10 @@ export class Request extends Component { uploadContent={content => uploadContent(flow, content, "request")}/> </footer> } + </HideInStatic> </section> ) } - - - edit(k) { - throw "unimplemented" - /* - switch (k) { - case 'm': - this.refs.requestLine.refs.method.focus() - break - case 'u': - this.refs.requestLine.refs.url.focus() - break - case 'v': - this.refs.requestLine.refs.httpVersion.focus() - break - case 'h': - this.refs.headers.edit() - break - default: - throw new Error(`Unimplemented: ${k}`) - } - */ - } - } Request = Message(Request) @@ -172,6 +151,7 @@ export class Response extends Component { message={flow.response} /> </article> + <HideInStatic> {!noContent && <footer > <ContentViewOptions @@ -181,31 +161,10 @@ export class Response extends Component { readonly={!isEdit}/> </footer> } + </HideInStatic> </section> ) } - - edit(k) { - throw "unimplemented" - /* - switch (k) { - case 'c': - this.refs.responseLine.refs.status_code.focus() - break - case 'm': - this.refs.responseLine.refs.msg.focus() - break - case 'v': - this.refs.responseLine.refs.httpVersion.focus() - break - case 'h': - this.refs.headers.edit() - break - default: - throw new Error(`'Unimplemented: ${k}`) - } - */ - } } Response = Message(Response) diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx index 08d15496..db9afe6f 100644 --- a/web/src/js/components/Footer.jsx +++ b/web/src/js/components/Footer.jsx @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import { formatSize } from '../utils.js' +import HideInStatic from '../components/common/HideInStatic' Footer.propTypes = { settings: PropTypes.object.isRequired, @@ -49,11 +50,14 @@ function Footer({ settings }) { <span className="label label-success">stream: {formatSize(stream_large_bodies)}</span> )} <div className="pull-right"> - {server && ( + <HideInStatic> + { + server && ( <span className="label label-primary" title="HTTP Proxy Server Address"> {listen_host||"*"}:{listen_port} - </span> - )} + </span>) + } + </HideInStatic> <span className="label label-info" title="Mitmproxy Version"> v{version} </span> diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx index ebe7453c..9b7354eb 100644 --- a/web/src/js/components/Header.jsx +++ b/web/src/js/components/Header.jsx @@ -8,6 +8,7 @@ import FileMenu from './Header/FileMenu' import FlowMenu from './Header/FlowMenu' import {setActiveMenu} from '../ducks/ui/header' import ConnectionIndicator from "./Header/ConnectionIndicator" +import HideInStatic from './common/HideInStatic' class Header extends Component { static entries = [MainMenu, OptionMenu] @@ -40,7 +41,9 @@ class Header extends Component { {Entry.title} </a> ))} - <ConnectionIndicator/> + <HideInStatic> + <ConnectionIndicator/> + </HideInStatic> </nav> <div> <Active/> diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx index 70fbb2c3..c88efcd8 100644 --- a/web/src/js/components/Header/FileMenu.jsx +++ b/web/src/js/components/Header/FileMenu.jsx @@ -4,11 +4,13 @@ import { connect } from 'react-redux' import FileChooser from '../common/FileChooser' import Dropdown, {Divider} from '../common/Dropdown' import * as flowsActions from '../../ducks/flows' +import * as modalActions from '../../ducks/ui/modal' +import HideInStatic from "../common/HideInStatic"; FileMenu.propTypes = { clearFlows: PropTypes.func.isRequired, loadFlows: PropTypes.func.isRequired, - saveFlows: PropTypes.func.isRequired + saveFlows: PropTypes.func.isRequired, } FileMenu.onNewClick = (e, clearFlows) => { @@ -34,12 +36,13 @@ export function FileMenu ({clearFlows, loadFlows, saveFlows}) { Save... </a> + <HideInStatic> <Divider/> - <a href="http://mitm.it/" target="_blank"> <i className="fa fa-fw fa-external-link"></i> Install Certificates... </a> + </HideInStatic> </Dropdown> ) } diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx index 8f104213..70c8bfcf 100644 --- a/web/src/js/components/Header/FlowMenu.jsx +++ b/web/src/js/components/Header/FlowMenu.jsx @@ -4,6 +4,7 @@ import { connect } from "react-redux" import Button from "../common/Button" import { MessageUtils } from "../../flow/utils.js" import * as flowsActions from "../../ducks/flows" +import HideInStatic from "../common/HideInStatic"; FlowMenu.title = 'Flow' @@ -22,6 +23,7 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow return <div/> return ( <div> + <HideInStatic> <div className="menu-group"> <div className="menu-content"> <Button title="[r]eplay flow" icon="fa-repeat text-primary" @@ -43,6 +45,8 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow </div> <div className="menu-legend">Flow Modification</div> </div> + </HideInStatic> + <div className="menu-group"> <div className="menu-content"> <Button title="download" icon="fa-download" @@ -52,6 +56,8 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow </div> <div className="menu-legend">Export</div> </div> + + <HideInStatic> <div className="menu-group"> <div className="menu-content"> <Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow" @@ -65,6 +71,7 @@ export function FlowMenu({ flow, resumeFlow, killFlow, replayFlow, duplicateFlow </div> <div className="menu-legend">Interception</div> </div> + </HideInStatic> </div> diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx index b33d578d..765129ed 100644 --- a/web/src/js/components/Header/OptionMenu.jsx +++ b/web/src/js/components/Header/OptionMenu.jsx @@ -1,66 +1,56 @@ -import React from "react" -import PropTypes from 'prop-types' +import React from "react" import { connect } from "react-redux" -import { SettingsToggle, EventlogToggle } from "./MenuToggle" +import { EventlogToggle, SettingsToggle } from "./MenuToggle" +import Button from "../common/Button" import DocsLink from "../common/DocsLink" +import HideInStatic from "../common/HideInStatic"; +import * as modalActions from "../../ducks/ui/modal" OptionMenu.title = 'Options' -export default function OptionMenu() { +function OptionMenu({ openOptions }) { return ( <div> - <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> + <HideInStatic> + <div className="menu-group"> + <div className="menu-content"> + <Button title="Open Options" icon="fa-cogs text-primary" + onClick={openOptions}> + Edit Options <sup>alpha</sup> + </Button> + </div> + <div className="menu-legend">Options Editor</div> </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 className="menu-group"> + <div className="menu-content"> + <SettingsToggle setting="anticache"> + Strip cache headers <DocsLink resource="features/anticache.html"/> + </SettingsToggle> + <SettingsToggle setting="showhost"> + Use host header for display + </SettingsToggle> + <SettingsToggle setting="ssl_insecure"> + Verify server certificates + </SettingsToggle> + </div> + <div className="menu-legend">Quick Options</div> </div> - <div className="menu-legend">HTTP Options</div> - </div> + </HideInStatic> + <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( + null, + { + openOptions: () => modalActions.setActiveModal('OptionModal') + } +)(OptionMenu) diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx index e2bedc88..03bfce7f 100644 --- a/web/src/js/components/MainView.jsx +++ b/web/src/js/components/MainView.jsx @@ -1,54 +1,27 @@ -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import Splitter from './common/Splitter' import FlowTable from './FlowTable' import FlowView from './FlowView' -import * as flowsActions from '../ducks/flows' -class MainView extends Component { - - static propTypes = { - highlight: PropTypes.string, - sort: PropTypes.object, - } +MainView.propTypes = { + hasSelection: PropTypes.bool.isRequired, +} - render() { - const { flows, selectedFlow, highlight } = this.props - return ( - <div className="main-view"> - <FlowTable - ref="flowTable" - flows={flows} - selected={selectedFlow} - highlight={highlight} - onSelect={this.props.selectFlow} - /> - {selectedFlow && [ - <Splitter key="splitter"/>, - <FlowView - key="flowDetails" - ref="flowDetails" - tab={this.props.tab} - updateFlow={data => this.props.updateFlow(selectedFlow, data)} - flow={selectedFlow} - /> - ]} - </div> - ) - } +function MainView({ hasSelection }) { + return ( + <div className="main-view"> + <FlowTable/> + {hasSelection && <Splitter key="splitter"/>} + {hasSelection && <FlowView key="flowDetails"/>} + </div> + ) } export default connect( state => ({ - flows: state.flows.view, - filter: state.flows.filter, - highlight: state.flows.highlight, - selectedFlow: state.flows.byId[state.flows.selected[0]], - tab: state.ui.flow.tab, + hasSelection: !!state.flows.byId[state.flows.selected[0]] }), - { - selectFlow: flowsActions.select, - updateFlow: flowsActions.update, - } + {} )(MainView) diff --git a/web/src/js/components/Modal/Modal.jsx b/web/src/js/components/Modal/Modal.jsx new file mode 100644 index 00000000..88e81156 --- /dev/null +++ b/web/src/js/components/Modal/Modal.jsx @@ -0,0 +1,24 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import ModalList from './ModalList' + +class PureModal extends Component { + + constructor(props, context) { + super(props, context) + } + + render() { + const { activeModal } = this.props + const ActiveModal = ModalList.find(m => m.name === activeModal ) + return( + activeModal ? <ActiveModal/> : <div/> + ) + } +} + +export default connect( + state => ({ + activeModal: state.ui.modal.activeModal + }) +)(PureModal) diff --git a/web/src/js/components/Modal/ModalLayout.jsx b/web/src/js/components/Modal/ModalLayout.jsx new file mode 100644 index 00000000..cf357b2b --- /dev/null +++ b/web/src/js/components/Modal/ModalLayout.jsx @@ -0,0 +1,16 @@ +import React from 'react' + +export default function ModalLayout ({ children }) { + return ( + <div> + <div className="modal-backdrop fade in"></div> + <div className="modal modal-visible" id="optionsModal" tabIndex="-1" role="dialog" aria-labelledby="options"> + <div className="modal-dialog modal-lg" role="document"> + <div className="modal-content"> + {children} + </div> + </div> + </div> + </div> + ) +} diff --git a/web/src/js/components/Modal/ModalList.jsx b/web/src/js/components/Modal/ModalList.jsx new file mode 100644 index 00000000..1175d5ea --- /dev/null +++ b/web/src/js/components/Modal/ModalList.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import ModalLayout from './ModalLayout' +import OptionContent from './OptionModal' + +function OptionModal() { + return ( + <ModalLayout> + <OptionContent/> + </ModalLayout> + ) +} + +export default [ OptionModal ] diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx new file mode 100644 index 00000000..38e2f239 --- /dev/null +++ b/web/src/js/components/Modal/Option.jsx @@ -0,0 +1,141 @@ +import React, { Component } from "react" +import PropTypes from "prop-types" +import { connect } from "react-redux" +import { update as updateOptions } from "../../ducks/options" +import { Key } from "../../utils" +import classnames from 'classnames' + +const stopPropagation = e => { + if (e.keyCode !== Key.ESC) { + e.stopPropagation() + } +} + +BooleanOption.PropTypes = { + value: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, +} +function BooleanOption({ value, onChange, ...props }) { + return ( + <div className="checkbox"> + <label> + <input type="checkbox" + checked={value} + onChange={e => onChange(e.target.checked)} + {...props} + /> + Enable + </label> + </div> + ) +} + +StringOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +function StringOption({ value, onChange, ...props }) { + return ( + <input type="text" + value={value || ""} + onChange={e => onChange(e.target.value)} + {...props} + /> + ) +} +function Optional(Component) { + return function ({ onChange, ...props }) { + return <Component + onChange={x => onChange(x ? x : null)} + {...props} + /> + } +} + +NumberOption.PropTypes = { + value: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, +} +function NumberOption({ value, onChange, ...props }) { + return ( + <input type="number" + value={value} + onChange={(e) => onChange(parseInt(e.target.value))} + {...props} + /> + ) +} + +ChoicesOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +export function ChoicesOption({ value, onChange, choices, ...props }) { + return ( + <select + onChange={(e) => onChange(e.target.value)} + value={value} + {...props} + > + { choices.map( + choice => ( + <option key={choice} value={choice}>{choice}</option> + ) + )} + </select> + ) +} + +StringSequenceOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +function StringSequenceOption({ value, onChange, ...props }) { + const height = Math.max(value.length, 1) + return <textarea + rows={height} + value={value.join('\n')} + onChange={e => onChange(e.target.value.split("\n"))} + {...props} + /> +} + +export const Options = { + "bool": BooleanOption, + "str": StringOption, + "int": NumberOption, + "optional str": Optional(StringOption), + "sequence of str": StringSequenceOption, +} + +function PureOption({ choices, type, value, onChange, name, error }) { + let Opt, props = {} + if (choices) { + Opt = ChoicesOption; + props.choices = choices + } else { + Opt = Options[type] + } + if (Opt !== BooleanOption) { + props.className = "form-control" + } + + return <div className={classnames({'has-error':error})}> + <Opt + name={name} + value={value} + onChange={onChange} + onKeyDown={stopPropagation} + {...props} + /> + </div> +} +export default connect( + (state, { name }) => ({ + ...state.options[name], + ...state.ui.optionsEditor[name] + }), + (dispatch, { name }) => ({ + onChange: value => dispatch(updateOptions(name, value)) + }) +)(PureOption) diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx new file mode 100644 index 00000000..fed0048d --- /dev/null +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -0,0 +1,110 @@ +import React, { Component } from "react" +import { connect } from "react-redux" +import * as modalAction from "../../ducks/ui/modal" +import * as optionAction from "../../ducks/options" +import Option from "./Option" +import _ from "lodash" + +function PureOptionHelp({help}){ + return <div className="help-block small">{help}</div>; +} +const OptionHelp = connect((state, {name}) => ({ + help: state.options[name].help, +}))(PureOptionHelp); + +function PureOptionError({error}){ + if(!error) return null; + return <div className="small text-danger">{error}</div>; +} +const OptionError = connect((state, {name}) => ({ + error: state.ui.optionsEditor[name] && state.ui.optionsEditor[name].error +}))(PureOptionError); + +export function PureOptionDefault({value, defaultVal}){ + if( value === defaultVal ) { + return null + } else { + if (typeof(defaultVal) === 'boolean') { + defaultVal = defaultVal ? 'true' : 'false' + } else if (Array.isArray(defaultVal)){ + if (_.isEmpty(_.compact(value)) && // filter the empty string in array + _.isEmpty(defaultVal)){ + return null + } + defaultVal = '[ ]' + } else if (defaultVal === ''){ + defaultVal = '\"\"' + } else if (defaultVal === null){ + defaultVal = 'null' + } + return <div className="small">Default: <strong> {defaultVal} </strong> </div> + } +} +const OptionDefault = connect((state, {name}) => ({ + value: state.options[name].value, + defaultVal: state.options[name].default +}))(PureOptionDefault) + +class PureOptionModal extends Component { + + constructor(props, context) { + super(props, context) + this.state = { title: 'Options' } + } + + componentWillUnmount(){ + // this.props.save() + } + + render() { + const { hideModal, options } = this.props + const { title } = this.state + return ( + <div> + <div className="modal-header"> + <button type="button" className="close" data-dismiss="modal" onClick={() => { + hideModal() + }}> + <i className="fa fa-fw fa-times"></i> + </button> + <div className="modal-title"> + <h4>{ title }</h4> + </div> + </div> + + <div className="modal-body"> + <div className="form-horizontal"> + { + options.map(name => + <div key={name} className="form-group"> + <div className="col-xs-6"> + <label htmlFor={name}>{name}</label> + <OptionHelp name={name}/> + </div> + <div className="col-xs-6"> + <Option name={name}/> + <OptionError name={name}/> + <OptionDefault name={name}/> + </div> + </div> + ) + } + </div> + </div> + + <div className="modal-footer"> + </div> + </div> + ) + } +} + +export default connect( + state => ({ + options: Object.keys(state.options).sort() + }), + { + hideModal: modalAction.hideModal, + save: optionAction.save, + } +)(PureOptionModal) diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx deleted file mode 100755 index 77b07027..00000000 --- a/web/src/js/components/Prompt.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import ReactDOM from 'react-dom' -import _ from 'lodash' - -import {Key} from '../utils.js' - -Prompt.propTypes = { - options: PropTypes.array.isRequired, - done: PropTypes.func.isRequired, - prompt: PropTypes.string, -} - -export default function Prompt({ prompt, done, options }) { - const opts = [] - - for (let i = 0; i < options.length; i++) { - let opt = options[i] - if (_.isString(opt)) { - let str = opt - while (str.length > 0 && keyTaken(str[0])) { - str = str.substr(1) - } - opt = { text: opt, key: str[0] } - } - if (!opt.text || !opt.key || keyTaken(opt.key)) { - throw 'invalid options' - } - opts.push(opt) - } - - function keyTaken(k) { - return _.map(opts, 'key').includes(k) - } - - function onKeyDown(event) { - event.stopPropagation() - event.preventDefault() - const key = opts.find(opt => Key[opt.key.toUpperCase()] === event.keyCode) - if (!key && event.keyCode !== Key.ESC && event.keyCode !== Key.ENTER) { - return - } - done(key.key || false) - } - - return ( - <div tabIndex="0" onKeyDown={onKeyDown} className="prompt-dialog"> - <div className="prompt-content"> - {prompt || <strong>Select: </strong> } - {opts.map(opt => { - const idx = opt.text.indexOf(opt.key) - function onClick(event) { - done(opt.key) - event.stopPropagation() - } - return ( - <span key={opt.key} className="option" onClick={onClick}> - {idx !== -1 ? opt.text.substring(0, idx) : opt.text + '('} - <strong className="text-primary">{opt.key}</strong> - {idx !== -1 ? opt.text.substring(idx + 1) : ')'} - </span> - ) - })} - </div> - </div> - ) -} diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx index af5b3caa..15384e02 100644 --- a/web/src/js/components/ProxyApp.jsx +++ b/web/src/js/components/ProxyApp.jsx @@ -7,6 +7,7 @@ import MainView from './MainView' import Header from './Header' import EventLog from './EventLog' import Footer from './Footer' +import Modal from './Modal/Modal' class ProxyAppMain extends Component { @@ -19,7 +20,7 @@ class ProxyAppMain extends Component { } render() { - const { showEventLog, location, filter, highlight } = this.props + const { showEventLog } = this.props return ( <div id="container" tabIndex="0"> <Header/> @@ -28,6 +29,7 @@ class ProxyAppMain extends Component { <EventLog key="eventlog"/> )} <Footer /> + <Modal/> </div> ) } diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx index e02ae010..02dab305 100644 --- a/web/src/js/components/common/Button.jsx +++ b/web/src/js/components/common/Button.jsx @@ -12,7 +12,7 @@ Button.propTypes = { export default function Button({ onClick, children, icon, disabled, className, title }) { return ( <div className={classnames(className, 'btn btn-default')} - onClick={!disabled && onClick} + onClick={disabled ? undefined : onClick} disabled={disabled} title={title}> {icon && (<i className={"fa fa-fw " + icon}/> )} diff --git a/web/src/js/components/common/HideInStatic.jsx b/web/src/js/components/common/HideInStatic.jsx new file mode 100644 index 00000000..c5f3bf47 --- /dev/null +++ b/web/src/js/components/common/HideInStatic.jsx @@ -0,0 +1,5 @@ +import React from 'react' + +export default function HideInStatic({ children }) { + return global.MITMWEB_STATIC ? null : [children] +} |