diff --git a/web/.editorconfig b/web/.editorconfig new file mode 100644 index 000000000..9acd1b0f4 --- /dev/null +++ b/web/.editorconfig @@ -0,0 +1,5 @@ +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/web/src/js/app.js b/web/src/js/app.js index b49de0027..8fa52a008 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -1,33 +1,41 @@ import React from "react" -import {render} from 'react-dom' -import {applyMiddleware, createStore} from 'redux' -import {Provider} from 'react-redux' +import { render } from 'react-dom' +import { applyMiddleware, createStore } from 'redux' +import { Provider } from 'react-redux' import createLogger from 'redux-logger' import thunkMiddleware from 'redux-thunk' - +import { Route, Router as ReactRouter, hashHistory, Redirect } from "react-router" import Connection from "./connection" -import {App} from "./components/proxyapp.js" -import rootReducer from './ducks/index'; -import {addLogEntry} from "./ducks/eventLog"; +import ProxyApp from "./components/ProxyApp" +import MainView from './components/MainView' +import rootReducer from './ducks/index' +import { addLogEntry } from "./ducks/eventLog" // logger must be last -const logger = createLogger(); const store = createStore( rootReducer, - applyMiddleware(thunkMiddleware, logger) -); + applyMiddleware(thunkMiddleware, createLogger()) +) -window.onerror = function (msg) { - store.dispatch(addLogEntry(msg)); -}; +window.addEventListener('error', msg => { + store.dispatch(addLogEntry(msg)) +}) +// @todo remove this document.addEventListener('DOMContentLoaded', () => { - window.ws = new Connection("/updates", store.dispatch); + window.ws = new Connection("/updates", store.dispatch) render( - {App}, + + + + + + + + + , document.getElementById("mitmproxy") - ); - -}); + ) +}) diff --git a/web/src/js/components/MainView.js b/web/src/js/components/MainView.js new file mode 100644 index 000000000..19ff5e4d9 --- /dev/null +++ b/web/src/js/components/MainView.js @@ -0,0 +1,191 @@ +import React, { Component } from "react" + +import { FlowActions } from "../actions.js" +import { Query } from "../actions.js" +import { Key } from "../utils.js" +import { Splitter } from "./common.js" +import FlowTable from "./flowtable.js" +import FlowView from "./flowview/index.js" +import { connect } from 'react-redux' +import { selectFlow, setFilter, setHighlight } from "../ducks/flows" + +class MainView extends Component { + + /** + * @todo move to actions + * @todo replace with mapStateToProps + */ + componentWillReceiveProps(nextProps) { + // Update redux store with route changes + if (nextProps.routeParams.flowId !== (nextProps.selectedFlow || {}).id) { + this.props.selectFlow(nextProps.routeParams.flowId) + } + if (nextProps.location.query[Query.SEARCH] !== nextProps.filter) { + this.props.setFilter(nextProps.location.query[Query.SEARCH], false) + } + if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) { + this.props.setHighlight(nextProps.location.query[Query.HIGHLIGHT], false) + } + } + + /** + * @todo move to actions + */ + selectFlow(flow) { + if (flow) { + this.props.updateLocation(`/flows/${flow.id}/${this.props.routeParams.detailTab || "request"}`) + } else { + this.props.updateLocation("/flows") + } + } + + /** + * @todo move to actions + */ + selectFlowRelative(shift) { + const { flows, routeParams, selectedFlow } = this.props + let index = 0 + if (!routeParams.flowId) { + if (shift < 0) { + index = flows.length - 1 + } + } else { + index = Math.min( + Math.max(0, flows.indexOf(selectedFlow) + shift), + flows.length - 1 + ) + } + this.selectFlow(flows[index]) + } + + /** + * @todo move to actions + */ + onMainKeyDown(e) { + var flow = this.props.selectedFlow + if (e.ctrlKey) { + return + } + switch (e.keyCode) { + case Key.K: + case Key.UP: + this.selectFlowRelative(-1) + break + case Key.J: + case Key.DOWN: + this.selectFlowRelative(+1) + break + case Key.SPACE: + case Key.PAGE_DOWN: + this.selectFlowRelative(+10) + break + case Key.PAGE_UP: + this.selectFlowRelative(-10) + break + case Key.END: + this.selectFlowRelative(+1e10) + break + case Key.HOME: + this.selectFlowRelative(-1e10) + break + case Key.ESC: + this.selectFlow(null) + break + case Key.H: + case Key.LEFT: + if (this.refs.flowDetails) { + this.refs.flowDetails.nextTab(-1) + } + break + case Key.L: + case Key.TAB: + case Key.RIGHT: + if (this.refs.flowDetails) { + this.refs.flowDetails.nextTab(+1) + } + break + case Key.C: + if (e.shiftKey) { + FlowActions.clear() + } + break + case Key.D: + if (flow) { + if (e.shiftKey) { + FlowActions.duplicate(flow) + } else { + FlowActions.delete(flow) + } + } + break + case Key.A: + if (e.shiftKey) { + FlowActions.accept_all() + } else if (flow && flow.intercepted) { + FlowActions.accept(flow) + } + break + case Key.R: + if (!e.shiftKey && flow) { + FlowActions.replay(flow) + } + break + case Key.V: + if (e.shiftKey && flow && flow.modified) { + FlowActions.revert(flow) + } + break + case Key.E: + if (this.refs.flowDetails) { + this.refs.flowDetails.promptEdit() + } + break + case Key.SHIFT: + break + default: + console.debug("keydown", e.keyCode) + return + } + e.preventDefault() + } + + render() { + const { selectedFlow } = this.props + return ( +
+ + {selectedFlow && [ + , + + ]} +
+ ) + } +} + +export default connect( + state => ({ + flows: state.flows.view, + filter: state.flows.filter, + highlight: state.flows.highlight, + selectedFlow: state.flows.all.byId[state.flows.selected[0]] + }), + dispatch => ({ + selectFlow: flowId => dispatch(selectFlow(flowId)), + setFilter: filter => dispatch(setFilter(filter)), + setHighlight: highlight => dispatch(setHighlight(highlight)) + }), + undefined, + { withRef: true } +)(MainView) diff --git a/web/src/js/components/ProxyApp.js b/web/src/js/components/ProxyApp.js new file mode 100644 index 000000000..33443dcd3 --- /dev/null +++ b/web/src/js/components/ProxyApp.js @@ -0,0 +1,163 @@ +import React, { Component, PropTypes } from "react" +import ReactDOM from "react-dom" +import _ from "lodash" +import { connect } from 'react-redux' + +import { Splitter } from "./common.js" +import { Header, MainMenu } from "./header.js" +import EventLog from "./eventlog.js" +import Footer from "./footer.js" +import { SettingsStore } from "../store/store.js" +import { Key } from "../utils.js" + +class ProxyAppMain extends Component { + + static childContextTypes = { + returnFocus: PropTypes.func.isRequired, + location: PropTypes.object.isRequired, + } + + static contextTypes = { + router: PropTypes.object.isRequired, + } + + constructor() { + this.settingsStore = new SettingsStore() + + // Default Settings before fetch + _.extend(this.settingsStore.dict, {}) + + this.state = { settings: this.settingsStore.dict } + } + + /** + * @todo move to actions + */ + updateLocation(pathname, queryUpdate) { + if (pathname === undefined) { + pathname = this.props.location.pathname + } + const query = this.props.location.query + for (const key of Object.keys(queryUpdate || {})) { + query[i] = queryUpdate[i] || undefined + } + this.context.router.replace({pathname, query}) + } + + /** + * @todo pass in with props + */ + getQuery() { + // For whatever reason, react-router always returns the same object, which makes comparing + // the current props with nextProps impossible. As a workaround, we just clone the query object. + return _.clone(this.props.location.query) + } + + /** + * @todo remove settings store + * @todo connect websocket here + * @todo listen to window's key events + */ + componentDidMount() { + this.focus() + this.settingsStore.addListener("recalculate", this.onSettingsChange) + } + + /** + * @todo remove settings store + * @todo disconnect websocket here + * @todo stop listening to window's key events + */ + componentWillUnmount() { + this.settingsStore.removeListener("recalculate", this.onSettingsChange) + } + + /** + * @todo move to actions + */ + onSettingsChange() { + this.setState({ settings: this.settingsStore.dict }) + } + + /** + * @todo use props + */ + getChildContext() { + return { + returnFocus: this.focus, + location: this.props.location + } + } + + /** + * @todo remove it + */ + focus() { + document.activeElement.blur() + window.getSelection().removeAllRanges() + ReactDOM.findDOMNode(this).focus() + } + + /** + * @todo move to actions + */ + onKeydown(e) { + let name = null + + switch (e.keyCode) { + case Key.I: + name = "intercept" + break + case Key.L: + name = "search" + break + case Key.H: + name = "highlight" + break + default: + let main = this.refs.view + if (this.refs.view.getWrappedInstance) { + main = this.refs.view.getWrappedInstance() + } + if (main.onMainKeyDown) { + main.onMainKeyDown(e) + } + return // don't prevent default then + } + + if (name) { + const headerComponent = this.refs.header + headerComponent.setState({active: MainMenu}, function () { + headerComponent.refs.active.refs[name].select() + }) + } + + e.preventDefault() + } + + render() { + const { showEventLog, location, children } = this.props + const { settings } = this.state + const query = this.getQuery() + return ( +
+
+ {React.cloneElement( + children, + { ref: "view", location, query, updateLocation: this.updateLocation } + )} + {showEventLog && [ + , + + ]} +
+ ) + } +}) + +export default connect( + state => ({ + showEventLog: state.eventLog.visible + }) +)(ProxyAppMain) diff --git a/web/src/js/components/mainview.js b/web/src/js/components/mainview.js deleted file mode 100644 index 5915c9fcd..000000000 --- a/web/src/js/components/mainview.js +++ /dev/null @@ -1,184 +0,0 @@ -import React from "react"; - -import {FlowActions} from "../actions.js"; -import {Query} from "../actions.js"; -import {Key} from "../utils.js"; -import {Splitter} from "./common.js" -import FlowTable from "./flowtable.js"; -import FlowView from "./flowview/index.js"; -import {connect} from 'react-redux' -import {selectFlow, setFilter, setHighlight} from "../ducks/flows"; - - -var MainView = React.createClass({ - componentWillReceiveProps: function (nextProps) { - // Update redux store with route changes - if(nextProps.routeParams.flowId !== (nextProps.selectedFlow || {}).id) { - this.props.selectFlow(nextProps.routeParams.flowId) - } - if(nextProps.location.query[Query.SEARCH] !== nextProps.filter) { - this.props.setFilter(nextProps.location.query[Query.SEARCH], false) - } - if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) { - this.props.setHighlight(nextProps.location.query[Query.HIGHLIGHT], false) - } - }, - selectFlow: function (flow) { - // TODO: This belongs into redux - if (flow) { - let tab = this.props.routeParams.detailTab || "request"; - this.props.updateLocation(`/flows/${flow.id}/${tab}`); - } else { - this.props.updateLocation("/flows"); - } - }, - selectFlowRelative: function (shift) { - // TODO: This belongs into redux - let flows = this.props.flows, - index - if (!this.props.routeParams.flowId) { - if (shift < 0) { - index = flows.length - 1 - } else { - index = 0 - } - } else { - index = flows.indexOf(this.props.selectedFlow) - index = Math.min( - Math.max(0, index + shift), - flows.length - 1 - ) - } - this.selectFlow(flows[index]) - }, - onMainKeyDown: function (e) { - var flow = this.props.selectedFlow; - if (e.ctrlKey) { - return; - } - switch (e.keyCode) { - case Key.K: - case Key.UP: - this.selectFlowRelative(-1); - break; - case Key.J: - case Key.DOWN: - this.selectFlowRelative(+1); - break; - case Key.SPACE: - case Key.PAGE_DOWN: - this.selectFlowRelative(+10); - break; - case Key.PAGE_UP: - this.selectFlowRelative(-10); - break; - case Key.END: - this.selectFlowRelative(+1e10); - break; - case Key.HOME: - this.selectFlowRelative(-1e10); - break; - case Key.ESC: - this.selectFlow(null); - break; - case Key.H: - case Key.LEFT: - if (this.refs.flowDetails) { - this.refs.flowDetails.nextTab(-1); - } - break; - case Key.L: - case Key.TAB: - case Key.RIGHT: - if (this.refs.flowDetails) { - this.refs.flowDetails.nextTab(+1); - } - break; - case Key.C: - if (e.shiftKey) { - FlowActions.clear(); - } - break; - case Key.D: - if (flow) { - if (e.shiftKey) { - FlowActions.duplicate(flow); - } else { - FlowActions.delete(flow); - } - } - break; - case Key.A: - if (e.shiftKey) { - FlowActions.accept_all(); - } else if (flow && flow.intercepted) { - FlowActions.accept(flow); - } - break; - case Key.R: - if (!e.shiftKey && flow) { - FlowActions.replay(flow); - } - break; - case Key.V: - if (e.shiftKey && flow && flow.modified) { - FlowActions.revert(flow); - } - break; - case Key.E: - if (this.refs.flowDetails) { - this.refs.flowDetails.promptEdit(); - } - break; - case Key.SHIFT: - break; - default: - console.debug("keydown", e.keyCode); - return; - } - e.preventDefault(); - }, - render: function () { - - var details = null; - if (this.props.selectedFlow) { - details = [ - , - - ] - } - - return ( -
- - {details} -
- ); - } -}); - -const MainViewContainer = connect( - state => ({ - flows: state.flows.view, - filter: state.flows.filter, - highlight: state.flows.highlight, - selectedFlow: state.flows.all.byId[state.flows.selected[0]] - }), - dispatch => ({ - selectFlow: flowId => dispatch(selectFlow(flowId)), - setFilter: filter => dispatch(setFilter(filter)), - setHighlight: highlight => dispatch(setHighlight(highlight)) - }), - undefined, - {withRef: true} -)(MainView); - -export default MainViewContainer; diff --git a/web/src/js/components/proxyapp.js b/web/src/js/components/proxyapp.js deleted file mode 100644 index e4489e185..000000000 --- a/web/src/js/components/proxyapp.js +++ /dev/null @@ -1,154 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import _ from "lodash"; -import {connect} from 'react-redux' -import { Route, Router as ReactRouter, hashHistory, Redirect} from "react-router" - -import {Splitter} from "./common.js" -import MainView from "./mainview.js"; -import Footer from "./footer.js"; -import {Header, MainMenu} from "./header.js"; -import EventLog from "./eventlog.js" -import {SettingsStore} from "../store/store.js"; -import {Key} from "../utils.js"; - - -//TODO: Move out of here, just a stub. -var Reports = React.createClass({ - render: function () { - return
ReportEditor
; - } -}); - - -var ProxyAppMain = React.createClass({ - childContextTypes: { - returnFocus: React.PropTypes.func.isRequired, - location: React.PropTypes.object.isRequired, - }, - contextTypes: { - router: React.PropTypes.object.isRequired - }, - updateLocation: function (pathname, queryUpdate) { - if (pathname === undefined) { - pathname = this.props.location.pathname; - } - var query = this.props.location.query; - if (queryUpdate !== undefined) { - for (var i in queryUpdate) { - if (queryUpdate.hasOwnProperty(i)) { - query[i] = queryUpdate[i] || undefined; //falsey values shall be removed. - } - } - } - this.context.router.replace({pathname, query}); - }, - getQuery: function () { - // For whatever reason, react-router always returns the same object, which makes comparing - // the current props with nextProps impossible. As a workaround, we just clone the query object. - return _.clone(this.props.location.query); - }, - componentDidMount: function () { - this.focus(); - this.settingsStore.addListener("recalculate", this.onSettingsChange); - }, - componentWillUnmount: function () { - this.settingsStore.removeListener("recalculate", this.onSettingsChange); - }, - onSettingsChange: function () { - this.setState({ settings: this.settingsStore.dict }); - }, - getChildContext: function () { - return { - returnFocus: this.focus, - location: this.props.location - }; - }, - getInitialState: function () { - var settingsStore = new SettingsStore(); - - this.settingsStore = settingsStore; - // Default Settings before fetch - _.extend(settingsStore.dict, {}); - return { - settings: settingsStore.dict, - }; - }, - focus: function () { - document.activeElement.blur(); - window.getSelection().removeAllRanges(); - ReactDOM.findDOMNode(this).focus(); - }, - getMainComponent: function () { - return this.refs.view.getWrappedInstance ? this.refs.view.getWrappedInstance() : this.refs.view; - }, - onKeydown: function (e) { - - var selectFilterInput = function (name) { - var headerComponent = this.refs.header; - headerComponent.setState({active: MainMenu}, function () { - headerComponent.refs.active.refs[name].select(); - }); - }.bind(this); - - switch (e.keyCode) { - case Key.I: - selectFilterInput("intercept"); - break; - case Key.L: - selectFilterInput("search"); - break; - case Key.H: - selectFilterInput("highlight"); - break; - default: - var main = this.getMainComponent(); - if (main.onMainKeyDown) { - main.onMainKeyDown(e); - } - return; // don't prevent default then - } - e.preventDefault(); - }, - render: function () { - var query = this.getQuery(); - var eventlog; - if (this.props.showEventLog) { - eventlog = [ - , - - ]; - } else { - eventlog = null; - } - return ( -
-
- {React.cloneElement( - this.props.children, - { ref: "view", location: this.props.location , updateLocation: this.updateLocation, query } - )} - {eventlog} -
-
- ); - } -}); - -const AppContainer = connect( - state => ({ - showEventLog: state.eventLog.visible - }) -)(ProxyAppMain); - - -export var App = ( - - - - - - - - -);