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/ContentView.jsx13
-rw-r--r--web/src/js/components/ContentView/ContentLoader.jsx156
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx10
-rw-r--r--web/src/js/components/ContentView/DownloadContentButton.jsx1
-rw-r--r--web/src/js/components/ContentView/ShowFullContentButton.jsx2
-rw-r--r--web/src/js/components/ContentView/UploadContentButton.jsx1
-rw-r--r--web/src/js/components/ContentView/ViewSelector.jsx2
-rw-r--r--web/src/js/components/EventLog.jsx4
-rw-r--r--web/src/js/components/FlowTable.jsx20
-rw-r--r--web/src/js/components/FlowView.jsx101
-rw-r--r--web/src/js/components/FlowView/Messages.jsx51
-rw-r--r--web/src/js/components/Footer.jsx10
-rw-r--r--web/src/js/components/Header.jsx5
-rw-r--r--web/src/js/components/Header/FileMenu.jsx7
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx7
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx86
-rw-r--r--web/src/js/components/MainView.jsx55
-rw-r--r--web/src/js/components/Modal/Modal.jsx24
-rw-r--r--web/src/js/components/Modal/ModalLayout.jsx16
-rw-r--r--web/src/js/components/Modal/ModalList.jsx13
-rw-r--r--web/src/js/components/Modal/Option.jsx141
-rw-r--r--web/src/js/components/Modal/OptionModal.jsx110
-rwxr-xr-xweb/src/js/components/Prompt.jsx67
-rw-r--r--web/src/js/components/ProxyApp.jsx4
-rw-r--r--web/src/js/components/common/Button.jsx2
-rw-r--r--web/src/js/components/common/HideInStatic.jsx5
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}) {
&nbsp;Save...
</a>
+ <HideInStatic>
<Divider/>
-
<a href="http://mitm.it/" target="_blank">
<i className="fa fa-fw fa-external-link"></i>
&nbsp;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]
+}