Merge pull request #1441 from mitmproxy/integrate_mitmproxy_contentviews

Integrate mitmproxy contentviews
This commit is contained in:
Maximilian Hils 2016-08-13 10:27:53 -07:00 committed by GitHub
commit b39c6e0883
19 changed files with 64334 additions and 740 deletions

View File

@ -16,9 +16,11 @@ from mitmproxy.flow import FlowWriter, FlowReader
from mitmproxy import filt
from mitmproxy import models
from mitmproxy import contentviews
from netlib import version
def convert_flow_to_json_dict(flow):
# type: (models.Flow) -> dict
"""
@ -338,6 +340,22 @@ class FlowContent(RequestHandler):
self.set_header("X-Frame-Options", "DENY")
self.write(message.raw_content)
class FlowContentView(RequestHandler):
def get(self, flow_id, message, content_view):
message = getattr(self.flow, message)
description, lines, error = contentviews.get_message_content_view(
contentviews.get(content_view.replace('_', ' ')), message
)
# if error:
# add event log
self.write(dict(
lines=list(lines),
description=description
))
class Events(RequestHandler):
@ -364,7 +382,8 @@ class Settings(RequestHandler):
anticomp=self.master.options.anticomp,
stickyauth=self.master.options.stickyauth,
stickycookie=self.master.options.stickycookie,
stream= self.master.options.stream_large_bodies
stream= self.master.options.stream_large_bodies,
contentViews= [v.name.replace(' ', '_') for v in contentviews.views]
)
))
@ -429,6 +448,7 @@ class Application(tornado.web.Application):
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content", FlowContent),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)", FlowContentView),
(r"/settings", Settings),
(r"/clear", ClearAll),
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,3 +18,4 @@ html {
@import (less) "eventlog.less";
@import (less) "footer.less";
@import (less) "codemirror.less";
@import (less) "contentview.less";

View File

@ -0,0 +1,17 @@
.contentview {
.header {
font-weight: bold;
}
.highlight{
font-weight: bold;
}
.offset{
color: blue
}
.text{
}
.codeeditor{
margin-bottom: 12px;
}
}

View File

@ -5,17 +5,35 @@
.flow-detail {
width: 100%;
overflow-x: auto;
overflow-y: scroll;
overflow:hidden;
display: flex;
flex-direction: column;
nav {
background-color: #F2F2F2;
}
section {
padding: 5px 12px;
display: flex;
flex-direction: column;
>article{
overflow: auto;
padding: 5px 12px 0;
}
>footer {
box-shadow: 0 0 3px gray;
padding: 2px;
margin: 0;
height:23px;
}
}
section.detail, section.error{
overflow: auto;
padding: 5px 12px 0;
}
.first-line {
.monospace();
background-color: #428bca;
@ -38,9 +56,9 @@
hr {
margin: 0 0 5px;
}
}
.inline-input {
display: inline;
margin: 0 -3px;
@ -64,8 +82,9 @@
}
}
.view-options {
margin-top: 10px;
.view-all-content-btn{
float: right;
margin-bottom: 12px;
}
.flow-detail table {

View File

@ -2,11 +2,10 @@ import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import * as ContentViews from './ContentView/ContentViews'
import * as MetaViews from './ContentView/MetaViews'
import ViewSelector from './ContentView/ViewSelector'
import UploadContentButton from './ContentView/UploadContentButton'
import DownloadContentButton from './ContentView/DownloadContentButton'
import ShowFullContentButton from './ContentView/ShowFullContentButton'
import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow'
import { displayLarge, updateEdit } from '../ducks/ui/flow'
ContentView.propTypes = {
// It may seem a bit weird at the first glance:
@ -19,7 +18,7 @@ ContentView.propTypes = {
ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2)
function ContentView(props) {
const { flow, message, contentView, isDisplayLarge, displayLarge, uploadContent, onContentChange, readonly } = props
const { flow, message, contentView, isDisplayLarge, displayLarge, onContentChange, readonly } = props
if (message.contentLength === 0 && readonly) {
return <MetaViews.ContentEmpty {...props}/>
@ -33,18 +32,11 @@ function ContentView(props) {
return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/>
}
const View = ContentViews[contentView]
const View = ContentViews[contentView] || ContentViews['ViewServer']
return (
<div>
<View flow={flow} message={message} readonly={readonly} onChange={onContentChange}/>
<div className="view-options text-center">
<ViewSelector message={message}/>
&nbsp;
<DownloadContentButton flow={flow} message={message}/>
&nbsp;
<UploadContentButton uploadContent={uploadContent}/>
</div>
<div className="contentview">
<View flow={flow} message={message} contentView={contentView} readonly={readonly} onChange={onContentChange}/>
<ShowFullContentButton/>
</div>
)
}

View File

@ -14,7 +14,7 @@ export default function CodeEditor ( { content, onChange} ){
lineNumbers: true
};
return (
<div onKeyDown={e => e.stopPropagation()}>
<div className="codeeditor" onKeyDown={e => e.stopPropagation()}>
<Codemirror value={content} onChange={onChange} options={options}/>
</div>
)

View File

@ -28,7 +28,8 @@ export default View => class extends React.Component {
componentWillReceiveProps(nextProps) {
if (
nextProps.message.content !== this.props.message.content ||
nextProps.message.contentHash !== this.props.message.contentHash
nextProps.message.contentHash !== this.props.message.contentHash ||
nextProps.contentView !== this.props.contentView
) {
this.updateContent(nextProps)
}
@ -52,7 +53,7 @@ export default View => class extends React.Component {
return this.setState({request: undefined, content: ""})
}
let requestUrl = MessageUtils.getContentURL(props.flow, props.message)
let requestUrl = MessageUtils.getContentURL(props.flow, props.message, (View.name == 'ViewServer' ? props.contentView : undefined))
// We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable.
let request = new XMLHttpRequest();

View File

@ -0,0 +1,31 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import ViewSelector from './ViewSelector'
import UploadContentButton from './UploadContentButton'
import DownloadContentButton from './DownloadContentButton'
ContentViewOptions.propTypes = {
flow: React.PropTypes.object.isRequired,
message: React.PropTypes.object.isRequired,
}
function ContentViewOptions(props) {
const { flow, message, uploadContent, readonly, contentViewDescription } = props
return (
<div className="view-options">
<ViewSelector message={message}/>
&nbsp;
<DownloadContentButton flow={flow} message={message}/>
&nbsp;
<UploadContentButton uploadContent={uploadContent}/>
&nbsp;
<span>{contentViewDescription}</span>
</div>
)
}
export default connect(
state => ({
contentViewDescription: state.ui.flow.viewDescription
})
)(ContentViewOptions)

View File

@ -1,4 +1,6 @@
import React, { PropTypes } from 'react'
import React, { PropTypes, Component } from 'react'
import { connect } from 'react-redux'
import { setContentViewDescription, setContent } from '../../ducks/ui/flow'
import ContentLoader from './ContentLoader'
import { MessageUtils } from '../../flow/utils'
import CodeEditor from './CodeEditor'
@ -18,43 +20,68 @@ function ViewImage({ flow, message }) {
)
}
ViewRaw.matches = () => true
ViewRaw.propTypes = {
Edit.propTypes = {
content: React.PropTypes.string.isRequired,
}
function ViewRaw({ content, readonly, onChange }) {
return readonly ? <pre>{content}</pre> : <CodeEditor content={content} onChange={onChange}/>
}
ViewRaw = ContentLoader(ViewRaw)
const isJSON = /^application\/json$/i
ViewJSON.matches = msg => isJSON.test(MessageUtils.getContentType(msg))
ViewJSON.propTypes = {
content: React.PropTypes.string.isRequired,
function Edit({ content, onChange }) {
return <CodeEditor content={content} onChange={onChange}/>
}
function ViewJSON({ content }) {
let json = content
try {
json = JSON.stringify(JSON.parse(content), null, 2);
} catch (e) {
// @noop
Edit = ContentLoader(Edit)
class ViewServer extends Component {
componentWillMount(){
this.setContentView(this.props)
}
return <pre>{json}</pre>
}
ViewJSON = ContentLoader(ViewJSON)
componentWillReceiveProps(nextProps){
if (nextProps.content != this.props.content) {
this.setContentView(nextProps)
}
}
setContentView(props){
try {
this.data = JSON.parse(props.content)
}catch(err) {
this.data = {lines: [], description: err.message}
}
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>
<pre>
{lines.map((line, i) =>
<div key={`line${i}`}>
{line.map((tuple, j) =>
<span key={`tuple${j}`} className={tuple[0]}>
{tuple[1]}
</span>
)}
</div>
)}
</pre>
{ViewImage.matches(message) &&
<ViewImage {...this.props} />
}
</div>
}
ViewAuto.matches = () => false
ViewAuto.findView = msg => [ViewImage, ViewJSON, ViewRaw].find(v => v.matches(msg)) || ViewRaw
ViewAuto.propTypes = {
message: React.PropTypes.object.isRequired,
flow: React.PropTypes.object.isRequired,
}
function ViewAuto({ message, flow, readonly, onChange }) {
const View = ViewAuto.findView(message)
return <View message={message} flow={flow} readonly={readonly} onChange={onChange}/>
}
export { ViewImage, ViewRaw, ViewAuto, ViewJSON }
ViewServer = connect(
state => ({
showFullContent: state.ui.flow.showFullContent,
maxLines: state.ui.flow.maxContentLines
}),
{
setContentViewDescription,
setContent
}
)(ContentLoader(ViewServer))
export { Edit, ViewServer, ViewImage }

View File

@ -0,0 +1,36 @@
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { render } from 'react-dom';
import Button from '../common/Button';
import { setShowFullContent } from '../../ducks/ui/flow'
ShowFullContentButton.propTypes = {
setShowFullContent: PropTypes.func.isRequired,
showFullContent: PropTypes.bool.isRequired
}
function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLines, contentLines} ){
return (
!showFullContent &&
<div>
<Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent(true)} text="Show full content"/>
<span className="pull-right"> {visibleLines}/{contentLines} are visible &nbsp; </span>
</div>
)
}
export default connect(
state => ({
showFullContent: state.ui.flow.showFullContent,
visibleLines: state.ui.flow.maxContentLines,
contentLines: state.ui.flow.content.length
}),
{
setShowFullContent
}
)(ShowFullContentButton)

View File

@ -1,47 +1,80 @@
import React, { PropTypes } from 'react'
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 ViewButton({ name, setContentView, children, activeView }) {
function ViewItem({ name, setContentView, children }) {
return (
<button
onClick={() => setContentView(name)}
className={classnames('btn btn-default', { active: name === activeView })}>
{children}
</button>
<li>
<a href="#" onClick={() => setContentView(name)}>
{children}
</a>
</li>
)
}
ViewButton = connect(state => ({
activeView: state.ui.flow.contentView
}), {
setContentView
})(ViewButton)
ViewSelector.propTypes = {
message: PropTypes.object.isRequired,
/*ViewSelector.propTypes = {
contentViews: PropTypes.array.isRequired,
activeView: PropTypes.string.isRequired,
isEdit: PropTypes.bool.isRequired,
isContentViewSelectorOpen: PropTypes.bool.isRequired,
setContentViewSelectorOpen: PropTypes.func.isRequired
}*/
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>
</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>
)
}
}
export default function ViewSelector({ message }) {
let autoView = ContentViews.ViewAuto.findView(message)
let autoViewName = (autoView.displayName || autoView.name)
.toLowerCase()
.replace('view', '')
.replace(/ContentLoader\((.+)\)/,"$1")
return (
<div className="view-selector btn-group btn-group-xs">
<ViewButton name="ViewAuto">auto: {autoViewName}</ViewButton>
{Object.keys(ContentViews).map(name =>
name !== "ViewAuto" &&
<ViewButton key={name} name={name}>{name.toLowerCase().replace('view', '')}</ViewButton>
)}
</div>
)
}
export default connect (
state => ({
contentViews: state.settings.contentViews,
activeView: state.ui.flow.contentView,
isEdit: !!state.ui.flow.modifiedFlow,
}), {
setContentView,
}
)(ViewSelector)

View File

@ -71,7 +71,7 @@ export default class FlowView extends Component {
const Tab = FlowView.allTabs[_.capitalize(active)]
return (
<div className="flow-detail" onScroll={this.adjustHead}>
<div className="flow-detail">
<Nav
flow={flow}
tabs={tabs}

View File

@ -114,7 +114,7 @@ export function Timing({ flow }) {
export default function Details({ flow }) {
return (
<section>
<section className="detail">
<h4>Client Connection</h4>
<ConnectionInfo conn={flow.client_conn}/>

View File

@ -5,6 +5,7 @@ import { connect } from 'react-redux'
import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils.js'
import { formatTimeStamp } from '../../utils.js'
import ContentView from '../ContentView'
import ContentViewOptions from '../ContentView/ContentViewOptions'
import ValidateEditor from '../ValueEditor/ValidateEditor'
import ValueEditor from '../ValueEditor/ValueEditor'
@ -81,27 +82,37 @@ const Message = connect(
export class Request extends Component {
render() {
const { flow, isEdit, updateFlow, uploadContent } = this.props
let noContent = !isEdit && (flow.request.contentLength == 0 || flow.request.contentLength == null)
return (
<section className="request">
<ToggleEdit/>
<RequestLine
flow={flow}
readonly={!isEdit}
updateFlow={updateFlow}/>
<Headers
message={flow.request}
readonly={!isEdit}
onChange={headers => updateFlow({ request: { headers } })}
/>
<article>
<ToggleEdit/>
<RequestLine
flow={flow}
readonly={!isEdit}
updateFlow={updateFlow}/>
<Headers
message={flow.request}
readonly={!isEdit}
onChange={headers => updateFlow({ request: { headers } })}
/>
<hr/>
<ContentView
readonly={!isEdit}
flow={flow}
onContentChange={content => updateFlow({ request: {content}})}
uploadContent={content => uploadContent(flow, content, "request")}
message={flow.request}/>
<hr/>
<ContentView
readonly={!isEdit}
flow={flow}
onContentChange={content => updateFlow({ request: {content}})}
message={flow.request}/>
</article>
{!noContent &&
<footer>
<ContentViewOptions
flow={flow}
readonly={!isEdit}
message={flow.request}
uploadContent={content => uploadContent(flow, content, "request")}/>
</footer>
}
</section>
)
}
@ -137,27 +148,38 @@ Request = Message(Request)
export class Response extends Component {
render() {
const { flow, isEdit, updateFlow, uploadContent } = this.props
let noContent = !isEdit && (flow.response.contentLength == 0 || flow.response.contentLength == null)
return (
<section className="response">
<ToggleEdit/>
<ResponseLine
flow={flow}
readonly={!isEdit}
updateFlow={updateFlow}/>
<Headers
message={flow.response}
readonly={!isEdit}
onChange={headers => updateFlow({ response: { headers } })}
/>
<hr/>
<ContentView
readonly={!isEdit}
flow={flow}
onContentChange={content => updateFlow({ response: {content}})}
uploadContent={content => uploadContent(flow, content, "response")}
message={flow.response}
/>
<article>
<ToggleEdit/>
<ResponseLine
flow={flow}
readonly={!isEdit}
updateFlow={updateFlow}/>
<Headers
message={flow.response}
readonly={!isEdit}
onChange={headers => updateFlow({ response: { headers } })}
/>
<hr/>
<ContentView
readonly={!isEdit}
flow={flow}
onContentChange={content => updateFlow({ response: {content}})}
message={flow.response}
/>
</article>
{!noContent &&
<footer >
<ContentViewOptions
flow={flow}
message={flow.response}
uploadContent={content => uploadContent(flow, content, "response")}
readonly={!isEdit}/>
</footer>
}
</section>
)
}
@ -194,7 +216,7 @@ ErrorView.propTypes = {
export function ErrorView({ flow }) {
return (
<section>
<section className="error">
<div className="alert alert-warning">
{flow.error.msg}
<div>

View File

@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'
import classnames from 'classnames'
Button.propTypes = {
onClick: PropTypes.func.isRequired,
@ -6,9 +7,9 @@ Button.propTypes = {
icon: PropTypes.string
}
export default function Button({ onClick, text, icon, disabled }) {
export default function Button({ onClick, text, icon, disabled, className }) {
return (
<div className={"btn btn-default"}
<div className={classnames(className, 'btn btn-default')}
onClick={onClick}
disabled={disabled}>
{icon && (<i className={"fa fa-fw " + icon}/> )}

View File

@ -3,28 +3,38 @@ import { getDiff } from "../../utils"
import _ from 'lodash'
export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW',
DISPLAY_LARGE = 'UI_FLOWVIEW_DISPLAY_LARGE',
SET_TAB = "UI_FLOWVIEW_SET_TAB",
START_EDIT = 'UI_FLOWVIEW_START_EDIT',
UPDATE_EDIT = 'UI_FLOWVIEW_UPDATE_EDIT',
UPLOAD_CONTENT = 'UI_FLOWVIEW_UPLOAD_CONTENT'
export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW',
DISPLAY_LARGE = 'UI_FLOWVIEW_DISPLAY_LARGE',
SET_TAB = "UI_FLOWVIEW_SET_TAB",
START_EDIT = 'UI_FLOWVIEW_START_EDIT',
UPDATE_EDIT = 'UI_FLOWVIEW_UPDATE_EDIT',
UPLOAD_CONTENT = 'UI_FLOWVIEW_UPLOAD_CONTENT',
SET_SHOW_FULL_CONTENT = 'UI_SET_SHOW_FULL_CONTENT',
SET_CONTENT_VIEW_DESCRIPTION = "UI_SET_CONTENT_VIEW_DESCRIPTION",
SET_CONTENT = "UI_SET_CONTENT"
const defaultState = {
displayLarge: false,
contentViewDescription: '',
showFullContent: false,
modifiedFlow: false,
contentView: 'ViewAuto',
contentView: 'Auto',
tab: 'request',
content: [],
maxContentLines: 80,
}
export default function reducer(state = defaultState, action) {
let wasInEditMode = !!(state.modifiedFlow)
switch (action.type) {
case START_EDIT:
return {
...state,
modifiedFlow: action.flow,
contentView: 'Edit',
showFullContent: true
}
case UPDATE_EDIT:
@ -38,6 +48,9 @@ export default function reducer(state = defaultState, action) {
...state,
modifiedFlow: false,
displayLarge: false,
contentView: (wasInEditMode ? 'Auto' : state.contentView),
viewDescription: '',
showFullContent: false,
}
case flowsActions.UPDATE:
@ -49,23 +62,47 @@ export default function reducer(state = defaultState, action) {
...state,
modifiedFlow: false,
displayLarge: false,
contentView: (wasInEditMode ? 'Auto' : state.contentView),
viewDescription: '',
showFullContent: false
}
} else {
return state
}
case SET_CONTENT_VIEW_DESCRIPTION:
return {
...state,
viewDescription: action.description
}
case SET_SHOW_FULL_CONTENT:
return {
...state,
showFullContent: action.show
}
case SET_TAB:
return {
...state,
tab: action.tab,
displayLarge: false,
showFullContent: false
}
case SET_CONTENT_VIEW:
return {
...state,
contentView: action.contentView,
showFullContent: action.contentView == 'Edit'
}
case SET_CONTENT:
let isFullContentShown = action.content.length < state.maxContentLines
return {
...state,
content: action.content,
showFullContent: isFullContentShown
}
case DISPLAY_LARGE:
@ -98,6 +135,22 @@ export function updateEdit(update) {
return { type: UPDATE_EDIT, update }
}
export function setContentViewDescription(description) {
return { type: SET_CONTENT_VIEW_DESCRIPTION, description }
}
export function setShowFullContent(show) {
return { type: SET_SHOW_FULL_CONTENT, show }
}
export function updateEdit(update) {
return { type: UPDATE_EDIT, update }
}
export function setContent(content){
return { type: SET_CONTENT, content}
}
export function stopEdit(flow, modifiedFlow) {
let diff = getDiff(flow, modifiedFlow)
return flowsActions.update(flow, diff)

View File

@ -43,14 +43,14 @@ export var MessageUtils = {
}
return false;
},
getContentURL: function (flow, message) {
getContentURL: function (flow, message, view) {
if (message === flow.request) {
message = "request";
} else if (message === flow.response) {
message = "response";
}
return "/flows/" + flow.id + "/" + message + "/content";
},
return `/flows/${flow.id}/${message}/content` + (view ? `/${view}` : '');
}
};
export var RequestUtils = _.extend(MessageUtils, {