Merge pull request #1489 from mitmproxy/web_refactor

Web refactor
This commit is contained in:
Maximilian Hils
2016-08-22 20:52:03 -07:00
committed by GitHub
18 changed files with 1236 additions and 4275 deletions

View File

@@ -243,7 +243,6 @@ class FlowHandler(RequestHandler):
flow = self.flow
flow.backup()
for a, b in six.iteritems(self.json):
if a == "request":
request = flow.request
for k, v in six.iteritems(b):
@@ -260,7 +259,6 @@ class FlowHandler(RequestHandler):
elif a == "response":
response = flow.response
for k, v in six.iteritems(b):
if k == "msg":
response.msg = str(v)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
hr.divider {
margin-top: 5px;
margin-bottom: 5px;
}

View File

@@ -1,5 +1,4 @@
import React, { Component, PropTypes } from 'react'
import { render } from 'react-dom';
import React, {PropTypes} from 'react'
import Codemirror from 'react-codemirror';

View File

@@ -30,6 +30,12 @@ function Edit({ content, onChange }) {
Edit = ContentLoader(Edit)
class ViewServer extends Component {
static propTypes = {
showFullContent: PropTypes.bool.isRequired,
maxLines: PropTypes.number.isRequired,
setContentViewDescription : PropTypes.func.isRequired,
setContent: PropTypes.func.isRequired
}
componentWillMount(){
this.setContentView(this.props)
@@ -40,6 +46,7 @@ class ViewServer extends Component {
this.setContentView(nextProps)
}
}
setContentView(props){
try {
this.data = JSON.parse(props.content)
@@ -50,25 +57,31 @@ class ViewServer extends Component {
props.setContentViewDescription(props.contentView != this.data.description ? this.data.description : '')
props.setContent(this.data.lines)
}
render() {
const {content, contentView, message, maxLines} = this.props
let lines = this.props.showFullContent ? this.data.lines : this.data.lines.slice(0, maxLines)
return <div>
return (
<div>
<pre>
{lines.map((line, i) =>
<div key={`line${i}`}>
{line.map((tuple, j) =>
<span key={`tuple${j}`} className={tuple[0]}>
{tuple[1]}
</span>
)}
{line.map((element, j) => {
let [style, text] = element
return (
<span key={`tuple${j}`} className={style}>
{text}
</span>
)
})}
</div>
)}
</pre>
{ViewImage.matches(message) &&
<ViewImage {...this.props} />
}
</div>
{ViewImage.matches(message) &&
<ViewImage {...this.props} />
}
</div>
)
}
}

View File

@@ -1,28 +1,18 @@
import { PropTypes } from 'react'
import FileChooser from '../common/FileChooser'
UploadContentButton.propTypes = {
uploadContent: PropTypes.func.isRequired,
}
export default function UploadContentButton({ uploadContent }) {
let fileInput;
return (
<a className="btn btn-default btn-xs"
onClick={() => fileInput.click()}
title="Upload a file to replace the content.">
<i className="fa fa-upload"/>
<input
ref={ref => fileInput = ref}
className="hidden"
type="file"
onChange={e => {
if (e.target.files.length > 0) uploadContent(e.target.files[0])
}}
/>
</a>
<FileChooser
icon="fa-upload"
title="Upload a file to replace the content."
onOpenFile={uploadContent}
className="btn btn-default btn-xs"/>
)
}

View File

@@ -1,72 +1,36 @@
import React, { PropTypes, Component } from 'react'
import classnames from 'classnames'
import { connect } from 'react-redux'
import * as ContentViews from './ContentViews'
import { setContentView } from "../../ducks/ui/flow";
function ViewItem({ name, setContentView, children }) {
return (
<li>
<a href="#" onClick={() => setContentView(name)}>
{children}
</a>
</li>
)
}
import { setContentView } from '../../ducks/ui/flow';
import Dropdown from '../common/Dropdown'
/*ViewSelector.propTypes = {
ViewSelector.propTypes = {
contentViews: PropTypes.array.isRequired,
activeView: PropTypes.string.isRequired,
isEdit: PropTypes.bool.isRequired,
isContentViewSelectorOpen: PropTypes.bool.isRequired,
setContentViewSelectorOpen: PropTypes.func.isRequired
}*/
setContentView: PropTypes.func.isRequired
}
function ViewSelector ({contentViews, activeView, isEdit, setContentView}){
let edit = ContentViews.Edit.displayName
let inner = <span> <b>View:</b> {activeView}<span className="caret"></span> </span>
class ViewSelector extends Component {
constructor(props, context) {
super(props, context)
this.close = this.close.bind(this)
this.state = {open: false}
}
close() {
this.setState({open: false})
document.removeEventListener('click', this.close)
}
onDropdown(e){
e.preventDefault()
this.setState({open: !this.state.open})
document.addEventListener('click', this.close)
}
render() {
const {contentViews, activeView, isEdit, setContentView} = this.props
let edit = ContentViews.Edit.displayName
return (
<div className={classnames('dropup pull-left', { open: this.state.open })}>
<a className="btn btn-default btn-xs"
onClick={ e => this.onDropdown(e) }
href="#">
<b>View:</b> {activeView}<span className="caret"></span>
return (
<Dropdown dropup className="pull-left" btnClass="btn btn-default btn-xs" text={inner}>
{contentViews.map(name =>
<a href="#" key={name} onClick={e => {e.preventDefault(); setContentView(name)}}>
{name.toLowerCase().replace('_', ' ')}
</a>
<ul className="dropdown-menu" role="menu">
{contentViews.map(name =>
<ViewItem key={name} setContentView={setContentView} name={name}>
{name.toLowerCase().replace('_', ' ')}
</ViewItem>
)}
{isEdit &&
<ViewItem key={edit} setContentView={setContentView} name={edit}>
{edit.toLowerCase()}
</ViewItem>
}
</ul>
</div>
)
}
)
}
{isEdit &&
<a href="#" onClick={e => {e.preventDefault(); setContentView(edit)}}>
{edit.toLowerCase()}
</a>
}
</Dropdown>
)
}
export default connect (

View File

@@ -7,40 +7,41 @@ Footer.propTypes = {
}
function Footer({ settings }) {
let {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickyauth, stickycookie, stream} = settings;
return (
<footer>
{settings.mode && settings.mode != "regular" && (
<span className="label label-success">{settings.mode} mode</span>
{mode && mode != "regular" && (
<span className="label label-success">{mode} mode</span>
)}
{settings.intercept && (
<span className="label label-success">Intercept: {settings.intercept}</span>
{intercept && (
<span className="label label-success">Intercept: {intercept}</span>
)}
{settings.showhost && (
{showhost && (
<span className="label label-success">showhost</span>
)}
{settings.no_upstream_cert && (
{no_upstream_cert && (
<span className="label label-success">no-upstream-cert</span>
)}
{settings.rawtcp && (
{rawtcp && (
<span className="label label-success">raw-tcp</span>
)}
{!settings.http2 && (
{!http2 && (
<span className="label label-success">no-http2</span>
)}
{settings.anticache && (
{anticache && (
<span className="label label-success">anticache</span>
)}
{settings.anticomp && (
{anticomp && (
<span className="label label-success">anticomp</span>
)}
{settings.stickyauth && (
<span className="label label-success">stickyauth: {settings.stickyauth}</span>
{stickyauth && (
<span className="label label-success">stickyauth: {stickyauth}</span>
)}
{settings.stickycookie && (
<span className="label label-success">stickycookie: {settings.stickycookie}</span>
{stickycookie && (
<span className="label label-success">stickycookie: {stickycookie}</span>
)}
{settings.stream && (
<span className="label label-success">stream: {formatSize(settings.stream)}</span>
{stream && (
<span className="label label-success">stream: {formatSize(stream)}</span>
)}
</footer>
)

View File

@@ -1,103 +1,46 @@
import React, { Component } from 'react'
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import classnames from 'classnames'
import FileChooser from '../common/FileChooser'
import Dropdown, {Divider} from '../common/Dropdown'
import * as flowsActions from '../../ducks/flows'
class FileMenu extends Component {
FileMenu.propTypes = {
clearFlows: PropTypes.func.isRequired,
loadFlows: PropTypes.func.isRequired,
saveFlows: PropTypes.func.isRequired
}
constructor(props, context) {
super(props, context)
this.state = { show: false }
FileMenu.onNewClick = (e, clearFlows) => {
e.preventDefault();
if (confirm('Delete all flows?'))
clearFlows()
}
this.close = this.close.bind(this)
this.onFileClick = this.onFileClick.bind(this)
this.onNewClick = this.onNewClick.bind(this)
this.onOpenClick = this.onOpenClick.bind(this)
this.onOpenFile = this.onOpenFile.bind(this)
this.onSaveClick = this.onSaveClick.bind(this)
}
function FileMenu ({clearFlows, loadFlows, saveFlows}) {
return (
<Dropdown className="pull-left" btnClass="special" text="mitmproxy">
<a href="#" onClick={e => FileMenu.onNewClick(e, clearFlows)}>
<i className="fa fa-fw fa-file"></i>
New
</a>
<FileChooser
icon="fa-folder-open"
text="Open..."
onOpenFile={file => loadFlows(file)}
/>
<a href="#" onClick={e =>{ e.preventDefault(); saveFlows();}}>
<i className="fa fa-fw fa-floppy-o"></i>
Save...
</a>
close() {
this.setState({ show: false })
document.removeEventListener('click', this.close)
}
<Divider/>
onFileClick(e) {
e.preventDefault()
if (this.state.show) {
return
}
document.addEventListener('click', this.close)
this.setState({ show: true })
}
onNewClick(e) {
e.preventDefault()
if (confirm('Delete all flows?')) {
this.props.clearFlows()
}
}
onOpenClick(e) {
e.preventDefault()
this.fileInput.click()
}
onOpenFile(e) {
e.preventDefault()
if (e.target.files.length > 0) {
this.props.loadFlows(e.target.files[0])
this.fileInput.value = ''
}
}
onSaveClick(e) {
e.preventDefault()
this.props.saveFlows()
}
render() {
return (
<div className={classnames('dropdown pull-left', { open: this.state.show })}>
<a href="#" className="special" onClick={this.onFileClick}>mitmproxy</a>
<ul className="dropdown-menu" role="menu">
<li>
<a href="#" onClick={this.onNewClick}>
<i className="fa fa-fw fa-file"></i>
New
</a>
</li>
<li>
<a href="#" onClick={this.onOpenClick}>
<i className="fa fa-fw fa-folder-open"></i>
Open...
</a>
<input
ref={ref => this.fileInput = ref}
className="hidden"
type="file"
onChange={this.onOpenFile}
/>
</li>
<li>
<a href="#" onClick={this.onSaveClick}>
<i className="fa fa-fw fa-floppy-o"></i>
Save...
</a>
</li>
<li role="presentation" className="divider"></li>
<li>
<a href="http://mitm.it/" target="_blank">
<i className="fa fa-fw fa-external-link"></i>
Install Certificates...
</a>
</li>
</ul>
</div>
)
}
<a href="http://mitm.it/" target="_blank">
<i className="fa fa-fw fa-external-link"></i>
Install Certificates...
</a>
</Dropdown>
)
}
export default connect(

View File

@@ -8,10 +8,14 @@ FlowMenu.title = 'Flow'
FlowMenu.propTypes = {
flow: PropTypes.object.isRequired,
acceptFlow: PropTypes.func.isRequired,
replayFlow: PropTypes.func.isRequired,
duplicateFlow: PropTypes.func.isRequired,
removeFlow: PropTypes.func.isRequired,
revertFlow: PropTypes.func.isRequired
}
function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) {
return (
<div>
<div className="menu-row">

View File

@@ -41,17 +41,17 @@ function OptionMenu({ settings, updateSettings }) {
/>
<ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
checked={!!settings.stickyauth}
txt={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 || ''}
txt={settings.stickycookie}
onToggleChanged={txt => updateSettings({ stickycookie: !settings.stickycookie ? txt : null })}
/>
<ToggleInputButton name="stream" placeholder="stream..."
checked={!!settings.stream}
txt={settings.stream || ''}
txt={settings.stream}
inputType="number"
onToggleChanged={txt => updateSettings({ stream: !settings.stream ? txt : null })}
/>

View File

@@ -0,0 +1,53 @@
import React, { Component, PropTypes } from 'react'
import classnames from 'classnames'
export const Divider = () => <hr className="divider"/>
export default class Dropdown extends Component {
static propTypes = {
dropup: PropTypes.bool,
className: PropTypes.string,
btnClass: PropTypes.string.isRequired
}
static defaultProps = {
dropup: false
}
constructor(props, context) {
super(props, context)
this.state = { open: false }
this.close = this.close.bind(this)
this.open = this.open.bind(this)
}
close() {
this.setState({ open: false })
document.removeEventListener('click', this.close)
}
open(e){
e.preventDefault()
if (this.state.open) {
return
}
this.setState({open: !this.state.open})
document.addEventListener('click', this.close)
}
render() {
const {dropup, className, btnClass, text, children} = this.props
return (
<div className={classnames( (dropup ? 'dropup' : 'dropdown'), className, { open: this.state.open })}>
<a href='#' className={btnClass}
onClick={this.open}>
{text}
</a>
<ul className="dropdown-menu" role="menu">
{children.map ( (item, i) => <li key={i}> {item} </li> )}
</ul>
</div>
)
}
}

View File

@@ -0,0 +1,27 @@
import React, { PropTypes } from 'react'
FileChooser.propTypes = {
icon: PropTypes.string,
text: PropTypes.string,
className: PropTypes.string,
title: PropTypes.string,
onOpenFile: PropTypes.func.isRequired
}
export default function FileChooser({ icon, text, className, title, onOpenFile }) {
let fileInput;
return (
<a href='#' onClick={() => fileInput.click()}
className={className}
title={title}>
<i className={'fa fa-fw ' + icon}></i>
{text}
<input
ref={ref => fileInput = ref}
className="hidden"
type="file"
onChange={e => { e.preventDefault(); if(e.target.files.length > 0) onOpenFile(e.target.files[0]); fileInput = "";}}
/>
</a>
)
}

View File

@@ -6,17 +6,16 @@ export default class ToggleInputButton extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
txt: PropTypes.string.isRequired,
onToggleChanged: PropTypes.func.isRequired
txt: PropTypes.string,
onToggleChanged: PropTypes.func.isRequired,
checked: PropTypes.bool.isRequired,
placeholder: PropTypes.string.isRequired,
inputType: PropTypes.string
}
constructor(props) {
super(props)
this.state = { txt: props.txt }
}
onChange(e) {
this.setState({ txt: e.target.value })
this.state = { txt: props.txt || '' }
}
onKeyDown(e) {
@@ -27,23 +26,24 @@ export default class ToggleInputButton extends Component {
}
render() {
const {checked, onToggleChanged, name, inputType, placeholder} = this.props
return (
<div className="input-group toggle-input-btn">
<span className="input-group-btn"
onClick={() => this.props.onToggleChanged(this.state.txt)}>
<div className={classnames('btn', this.props.checked ? 'btn-primary' : 'btn-default')}>
<span className={classnames('fa', this.props.checked ? 'fa-check-square-o' : 'fa-square-o')}/>
onClick={() => onToggleChanged(this.state.txt)}>
<div className={classnames('btn', checked ? 'btn-primary' : 'btn-default')}>
<span className={classnames('fa', checked ? 'fa-check-square-o' : 'fa-square-o')}/>
&nbsp;
{this.props.name}
{name}
</div>
</span>
<input
className="form-control"
placeholder={this.props.placeholder}
disabled={this.props.checked}
placeholder={placeholder}
disabled={checked}
value={this.state.txt}
type={this.props.inputType}
onChange={e => this.onChange(e)}
type={inputType || 'text'}
onChange={e => this.setState({ txt: e.target.value })}
onKeyDown={e => this.onKeyDown(e)}
/>
</div>

View File

@@ -1,5 +1,6 @@
import { fetchApi } from '../utils'
import reduceList, * as listActions from './utils/list'
import { selectRelative } from './flowView'
import * as msgQueueActions from './msgQueue'
import * as websocketActions from './websocket'
@@ -210,5 +211,14 @@ export function updateFlow(item) {
* @private
*/
export function removeFlow(id) {
return { type: REMOVE, id }
return (dispatch, getState) => {
let currentIndex = getState().flowView.indexOf[getState().flows.selected[0]]
let maxIndex = getState().flowView.data.length - 1
let deleteLastEntry = maxIndex == 0
if (deleteLastEntry)
dispatch(select())
else
dispatch(selectRelative(currentIndex == maxIndex ? -1 : 1) )
dispatch({ type: REMOVE, id })
}
}

View File

@@ -149,6 +149,5 @@ export function setContent(content){
}
export function stopEdit(flow, modifiedFlow) {
let diff = getDiff(flow, modifiedFlow)
return flowsActions.update(flow, diff)
return flowsActions.update(flow, getDiff(flow, modifiedFlow))
}

View File

@@ -107,14 +107,15 @@ fetchApi.put = (url, json, options) => fetchApi(
...options
}
)
// deep comparison of two json objects (dicts). arrays are handeled as a single value.
// return: json object including only the changed keys value pairs.
export function getDiff(obj1, obj2) {
let result = {...obj2};
for(let key in obj1) {
if(_.isEqual(obj2[key], obj1[key]))
result[key] = undefined
else if(!(Array.isArray(obj2[key]) && Array.isArray(obj1[key])) &&
typeof obj2[key] == 'object' && typeof obj1[key] == 'object')
else if(Object.prototype.toString.call(obj2[key]) === '[object Object]' &&
Object.prototype.toString.call(obj1[key]) === '[object Object]' )
result[key] = getDiff(obj1[key], obj2[key])
}
return result