[web] FlowView and ContentView

This commit is contained in:
Jason 2016-06-14 23:52:00 +08:00
parent 1fc2db85fa
commit e5bf1e930a
24 changed files with 1457 additions and 62674 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,8 @@
background-color: #F2F2F2;
padding: 0 5px;
flex: 0 0 auto;
border-top: 1px solid #aaa;
cursor: row-resize;
}
> pre {
@ -48,4 +50,4 @@
margin-top: -2px;
margin-left: 3px;
}
}
}

View File

@ -0,0 +1,78 @@
import React, { Component, PropTypes } from 'react'
import { MessageUtils } from '../flow/utils.js'
import { ViewAuto, ViewImage } from './ContentView/ContentViews'
import * as ContentErrors from './ContentView/ContentErrors'
import ContentLoader from './ContentView/ContentLoader'
import ViewSelector from './ContentView/ViewSelector'
export default class ContentView extends Component {
static propTypes = {
// It may seem a bit weird at the first glance:
// Every view takes the flow and the message as props, e.g.
// <Auto flow={flow} message={flow.request}/>
flow: React.PropTypes.object.isRequired,
message: React.PropTypes.object.isRequired,
}
constructor(props, context) {
super(props, context)
this.state = { displayLarge: false, View: ViewAuto }
this.selectView = this.selectView.bind(this)
}
selectView(View) {
this.setState({ View })
}
displayLarge() {
this.setState({ displayLarge: true })
}
componentWillReceiveProps(nextProps) {
if (nextProps.message !== this.props.message) {
this.setState({ displayLarge: false, View: ViewAuto })
}
}
isContentTooLarge(msg) {
return msg.contentLength > 1024 * 1024 * (ViewImage.matches(msg) ? 10 : 0.2)
}
render() {
const { flow, message } = this.props
const { displayLarge, View } = this.state
if (message.contentLength === 0) {
return <ContentErrors.ContentEmpty {...this.props}/>
}
if (message.contentLength === null) {
return <ContentErrors.ContentMissing {...this.props}/>
}
if (!displayLarge && this.isContentTooLarge(message)) {
return <ContentErrors.ContentTooLarge {...this.props} onClick={this.displayLarge}/>
}
return (
<div>
{View.textView ? (
<ContentLoader flow={flow} message={message}>
<this.state.View content="" />
</ContentLoader>
) : (
<View flow={flow} message={message} />
)}
<div className="view-options text-center">
<ViewSelector onSelectView={this.selectView} active={View} message={message}/>
&nbsp;
<a className="btn btn-default btn-xs" href={MessageUtils.getContentURL(flow, message)}>
<i className="fa fa-download"/>
</a>
</div>
</div>
)
}
}

View File

@ -0,0 +1,28 @@
import React from 'react'
import { ViewImage } from './ContentViews'
import {formatSize} from '../../utils.js'
export function ContentEmpty({ flow, message }) {
return (
<div className="alert alert-info">
No {flow.request === message ? 'request' : 'response'} content.
</div>
)
}
export function ContentMissing({ flow, message }) {
return (
<div className="alert alert-info">
{flow.request === message ? 'Request' : 'Response'} content missing.
</div>
)
}
export function ContentTooLarge({ message, onClick }) {
return (
<div className="alert alert-warning">
<button onClick={onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
{formatSize(message.contentLength)} content size.
</div>
)
}

View File

@ -0,0 +1,67 @@
import React, { Component, PropTypes } from 'react'
import { MessageUtils } from '../../flow/utils.js'
export default class ContentLoader extends Component {
static propTypes = {
flow: PropTypes.object.isRequired,
message: PropTypes.object.isRequired,
}
constructor(props, context) {
super(props, context)
this.state = { content: null, request: null }
}
requestContent(nextProps) {
if (this.state.request) {
this.state.request.abort()
}
const request = MessageUtils.getContent(nextProps.flow, nextProps.message)
this.setState({ content: null, request })
request
.done(content => {
this.setState({ content })
})
.fail((xhr, textStatus, errorThrown) => {
if (textStatus === 'abort') {
return
}
this.setState({ content: `AJAX Error: ${textStatus}\r\n${errorThrown}` })
})
.always(() => {
this.setState({ request: null })
})
}
componentWillMount() {
this.requestContent(this.props)
}
componentWillReceiveProps(nextProps) {
if (nextProps.message !== this.props.message) {
this.requestContent(nextProps)
}
}
componentWillUnmount() {
if (this.state.request) {
this.state.request.abort()
}
}
render() {
return this.state.content ? (
React.cloneElement(this.props.children, {
content: this.state.content
})
) : (
<div className="text-center">
<i className="fa fa-spinner fa-spin"></i>
</div>
)
}
}

View File

@ -0,0 +1,70 @@
import React, { PropTypes } from 'react'
import ContentLoader from './ContentLoader'
import { MessageUtils } from '../../flow/utils.js'
const views = [ViewAuto, ViewImage, ViewJSON, ViewRaw]
ViewImage.regex = /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i
ViewImage.matches = msg => ViewImage.regex.test(MessageUtils.getContentType(msg))
ViewImage.propTypes = {
flow: PropTypes.object.isRequired,
message: PropTypes.object.isRequired,
}
export function ViewImage({ flow, message }) {
return (
<div className="flowview-image">
<img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
</div>
)
}
ViewRaw.textView = true
ViewRaw.matches = () => true
ViewRaw.propTypes = {
content: React.PropTypes.string.isRequired,
}
export function ViewRaw({ content }) {
return <pre>{content}</pre>
}
ViewJSON.textView = true
ViewJSON.regex = /^application\/json$/i
ViewJSON.matches = msg => ViewJSON.regex.test(MessageUtils.getContentType(msg))
ViewJSON.propTypes = {
content: React.PropTypes.string.isRequired,
}
export function ViewJSON({ content }) {
let json = content
try {
json = JSON.stringify(JSON.parse(content), null, 2);
} catch (e) {
// @noop
}
return <pre>{json}</pre>
}
ViewAuto.matches = () => false
ViewAuto.findView = msg => views.find(v => v.matches(msg)) || views[views.length - 1]
ViewAuto.propTypes = {
message: React.PropTypes.object.isRequired,
flow: React.PropTypes.object.isRequired,
}
export function ViewAuto({ message, flow }) {
const View = ViewAuto.findView(message)
if (View.textView) {
return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader>
} else {
return <View message={message} flow={flow} />
}
}
export default views

View File

@ -0,0 +1,28 @@
import React, { PropTypes } from 'react'
import classnames from 'classnames'
import views, { ViewAuto } from './ContentViews'
ViewSelector.propTypes = {
active: PropTypes.func.isRequired,
message: PropTypes.object.isRequired,
onSelectView: PropTypes.func.isRequired,
}
export default function ViewSelector({ active, message, onSelectView }) {
return (
<div className="view-selector btn-group btn-group-xs">
{views.map(View => (
<button
key={View.name}
onClick={() => onSelectView(View)}
className={classnames('btn btn-default', { active: View === active })}>
{View === ViewAuto ? (
`auto: ${ViewAuto.findView(message).name.toLowerCase().replace('view', '')}`
) : (
View.name.toLowerCase().replace('view', '')
)}
</button>
))}
</div>
)
}

View File

@ -1,31 +1,70 @@
import React, { PropTypes } from 'react'
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { toggleEventLogFilter, toggleEventLogVisibility } from '../ducks/eventLog'
import { ToggleButton } from './common'
import EventList from './EventLog/EventList'
EventLog.propTypes = {
filters: PropTypes.object.isRequired,
events: PropTypes.array.isRequired,
onToggleFilter: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
}
class EventLog extends Component {
function EventLog({ filters, events, onToggleFilter, onClose }) {
return (
<div className="eventlog">
<div>
Eventlog
<div className="pull-right">
{['debug', 'info', 'web'].map(type => (
<ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => onToggleFilter(type)}/>
))}
<i onClick={onClose} className="fa fa-close"></i>
static propTypes = {
filters: PropTypes.object.isRequired,
events: PropTypes.array.isRequired,
onToggleFilter: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
defaultHeight: PropTypes.number,
}
static defaultProps = {
defaultHeight: 200,
}
constructor(props, context) {
super(props, context)
this.state = { height: this.props.defaultHeight }
this.onDragStart = this.onDragStart.bind(this)
this.onDragMove = this.onDragMove.bind(this)
this.onDragStop = this.onDragStop.bind(this)
}
onDragStart(event) {
event.preventDefault()
this.dragStart = this.state.height + event.pageY
window.addEventListener('mousemove', this.onDragMove)
window.addEventListener('mouseup', this.onDragStop)
window.addEventListener('dragend', this.onDragStop)
}
onDragMove(event) {
event.preventDefault()
this.setState({ height: this.dragStart - event.pageY })
}
onDragStop(event) {
event.preventDefault()
window.removeEventListener('mousemove', this.onDragMove)
}
render() {
const { height } = this.state
const { filters, events, onToggleFilter, onClose } = this.props
return (
<div className="eventlog" style={{ height }}>
<div onMouseDown={this.onDragStart}>
Eventlog
<div className="pull-right">
{['debug', 'info', 'web'].map(type => (
<ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => onToggleFilter(type)}/>
))}
<i onClick={onClose} className="fa fa-close"></i>
</div>
</div>
<EventList events={events} />
</div>
<EventList events={events} />
</div>
)
)
}
}
export default connect(

View File

@ -19,16 +19,12 @@ function FlowTableHead({ sortColumn, sortDesc, onSort }) {
{columns.map(Column => (
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
key={Column.name}
onClick={() => onClick(Column)}>
onClick={() => onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })}>
{Column.headerName}
</th>
))}
</tr>
)
function onClick(Column) {
onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })
}
}
export default connect(

View File

@ -0,0 +1,107 @@
import React, { Component } from 'react'
import _ from 'lodash'
import Nav from './FlowView/Nav'
import { Request, Response, Error } from './FlowView/Messages'
import Details from './FlowView/Details'
import Prompt from './prompt'
export default class FlowView extends Component {
static allTabs = { Request, Response, Error, Details }
constructor(props, context) {
super(props, context)
this.state = { prompt: false }
this.closePrompt = this.closePrompt.bind(this)
this.selectTab = this.selectTab.bind(this)
}
getTabs() {
return ['request', 'response', 'error'].filter(k => this.props.flow[k]).concat(['details'])
}
nextTab(increment) {
const tabs = this.getTabs()
// JS modulo operator doesn't correct negative numbers, make sure that we are positive.
this.selectTab(tabs[(tabs.indexOf(this.props.tab) + increment + tabs.length) % tabs.length])
}
selectTab(panel) {
this.props.updateLocation(`/flows/${this.props.flow.id}/${panel}`)
}
closePrompt(edit) {
this.setState({ prompt: false })
if (edit) {
this.refs.tab.edit(edit)
}
}
promptEdit() {
let options
switch (this.props.tab) {
case 'request':
options = [
'method',
'url',
{ text: 'http version', key: 'v' },
'header'
]
break
case 'response':
options = [
{ text: 'http version', key: 'v' },
'code',
'message',
'header'
]
break
case 'details':
return
default:
throw 'Unknown tab for edit: ' + this.props.tab
}
this.setState({ prompt: { options, done: this.closePrompt } })
}
render() {
const tabs = this.getTabs()
let { flow, tab: active } = this.props
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" onScroll={this.adjustHead}>
<Nav
flow={flow}
tabs={tabs}
active={active}
onSelectTab={this.selectTab}
/>
<Tab flow={flow}/>
{this.state.prompt && (
<Prompt {...this.state.prompt}/>
)}
</div>
)
}
}

View File

@ -0,0 +1,133 @@
import React from 'react'
import _ from 'lodash'
import { formatTimeStamp, formatTimeDelta } from '../../utils.js'
export function TimeStamp({ t, deltaTo, title }) {
return t ? (
<tr>
<td>{title}:</td>
<td>
{formatTimeStamp(t)}
{deltaTo && (
<span className="text-muted">
({formatTimeDelta(1000 * (t - deltaTo))})
</span>
)}
</td>
</tr>
) : (
<tr></tr>
)
}
export function ConnectionInfo({ conn }) {
return (
<table className="connection-table">
<tbody>
<tr key="address">
<td>Address:</td>
<td>{conn.address.address.join(':')}</td>
</tr>
{conn.sni ? (
<tr key="sni"></tr>
) : (
<tr key="sni">
<td>
<abbr title="TLS Server Name Indication">TLS SNI:</abbr>
</td>
<td>{conn.sni}</td>
</tr>
)}
</tbody>
</table>
)
}
export function CertificateInfo({ flow }) {
// @todo We should fetch human-readable certificate representation from the server
return (
<div>
{flow.client_conn.cert && [
<h4 key="name">Client Certificate</h4>,
<pre key="value" style={{ maxHeight: 100 }}>{flow.client_conn.cert}</pre>
]}
{flow.server_conn.cert && [
<h4 key="name">Server Certificate</h4>,
<pre key="value" style={{ maxHeight: 100 }}>{flow.server_conn.cert}</pre>
]}
</div>
)
}
export function Timing({ flow }) {
const { server_conn: sc, client_conn: cc, request: req, response: res } = flow
const timestamps = [
{
title: "Server conn. initiated",
t: sc.timestamp_start,
deltaTo: req.timestamp_start
}, {
title: "Server conn. TCP handshake",
t: sc.timestamp_tcp_setup,
deltaTo: req.timestamp_start
}, {
title: "Server conn. SSL handshake",
t: sc.timestamp_ssl_setup,
deltaTo: req.timestamp_start
}, {
title: "Client conn. established",
t: cc.timestamp_start,
deltaTo: req.timestamp_start
}, {
title: "Client conn. SSL handshake",
t: cc.timestamp_ssl_setup,
deltaTo: req.timestamp_start
}, {
title: "First request byte",
t: req.timestamp_start,
}, {
title: "Request complete",
t: req.timestamp_end,
deltaTo: req.timestamp_start
}, res && {
title: "First response byte",
t: res.timestamp_start,
deltaTo: req.timestamp_start
}, res && {
title: "Response complete",
t: res.timestamp_end,
deltaTo: req.timestamp_start
}
]
return (
<div>
<h4>Timing</h4>
<table className="timing-table">
<tbody>
{timestamps.filter(v => v).sort((a, b) => a.t - b.t).map(item => (
<TimeStamp key={item.title} {...item}/>
))}
</tbody>
</table>
</div>
)
}
export default function Details({ flow }) {
return (
<section>
<h4>Client Connection</h4>
<ConnectionInfo conn={flow.client_conn}/>
<h4>Server Connection</h4>
<ConnectionInfo conn={flow.server_conn}/>
<CertificateInfo flow={flow}/>
<Timing flow={flow}/>
</section>
)
}

View File

@ -0,0 +1,130 @@
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import { ValueEditor } from '../editor'
import { Key } from '../../utils.js'
class HeaderEditor extends Component {
render() {
return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>
}
focus() {
ReactDOM.findDOMNode(this).focus()
}
onKeyDown(e) {
switch (e.keyCode) {
case Key.BACKSPACE:
var s = window.getSelection().getRangeAt(0)
if (s.startOffset === 0 && s.endOffset === 0) {
this.props.onRemove(e)
}
break
case Key.TAB:
if (!e.shiftKey) {
this.props.onTab(e)
}
break
}
}
}
export default class Headers extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
message: PropTypes.object.isRequired,
}
onChange(row, col, val) {
const nextHeaders = _.cloneDeep(this.props.message.headers)
nextHeaders[row][col] = val
if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
// do not delete last row
if (nextHeaders.length === 1) {
nextHeaders[0][0] = 'Name'
nextHeaders[0][1] = 'Value'
} else {
nextHeaders.splice(row, 1)
// manually move selection target if this has been the last row.
if (row === nextHeaders.length) {
this._nextSel = `${row - 1}-value`
}
}
}
this.props.onChange(nextHeaders)
}
edit() {
this.refs['0-key'].focus()
}
onTab(row, col, e) {
const headers = this.props.message.headers
if (row !== headers.length - 1 || col !== 1) {
return
}
e.preventDefault()
const nextHeaders = _.cloneDeep(this.props.message.headers)
nextHeaders.push(['Name', 'Value'])
this.props.onChange(nextHeaders)
this._nextSel = `${row + 1}-key`
}
componentDidUpdate() {
if (this._nextSel && this.refs[this._nextSel]) {
this.refs[this._nextSel].focus()
this._nextSel = undefined
}
}
onRemove(row, col, e) {
if (col === 1) {
e.preventDefault()
this.refs[`${row}-key`].focus()
} else if (row > 0) {
e.preventDefault()
this.refs[`${row - 1}-value`].focus()
}
}
render() {
const { message } = this.props
return (
<table className="header-table">
<tbody>
{message.headers.map((header, i) => (
<tr key={i}>
<td className="header-name">
<HeaderEditor
ref={`${i}-key`}
content={header[0]}
onDone={val => this.onChange(i, 0, val)}
onRemove={event => this.onRemove(i, 0, event)}
onTab={event => this.onTab(i, 0, event)}
/>:
</td>
<td className="header-value">
<HeaderEditor
ref={`${i}-value`}
content={header[1]}
onDone={val => this.onChange(i, 1, val)}
onRemove={event => this.onRemove(i, 1, event)}
onTab={event => this.onTab(i, 1, event)}
/>
</td>
</tr>
))}
</tbody>
</table>
)
}
}

View File

@ -0,0 +1,168 @@
import React, { Component } from 'react'
import _ from 'lodash'
import { FlowActions } from '../../actions.js'
import { RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion } from '../../flow/utils.js'
import { Key, formatTimeStamp } from '../../utils.js'
import ContentView from '../ContentView'
import { ValueEditor } from '../editor'
import Headers from './Headers'
class RequestLine extends Component {
render() {
const { flow } = this.props
return (
<div className="first-line request-line">
<ValueEditor
ref="method"
content={flow.request.method}
onDone={method => FlowActions.update(flow, { request: { method } })}
inline
/>
&nbsp;
<ValueEditor
ref="url"
content={RequestUtils.pretty_url(flow.request)}
onDone={url => FlowActions.update(flow, { request: Object.assign({ path: '' }, parseUrl(url)) })}
isValid={url => !!parseUrl(url).host}
inline
/>
&nbsp;
<ValueEditor
ref="httpVersion"
content={flow.request.http_version}
onDone={ver => FlowActions.update(flow, { request: { http_version: parseHttpVersion(ver) } })}
isValid={isValidHttpVersion}
inline
/>
</div>
)
}
}
class ResponseLine extends Component {
render() {
const { flow } = this.props
return (
<div className="first-line response-line">
<ValueEditor
ref="httpVersion"
content={flow.response.http_version}
onDone={nextVer => FlowActions.update(flow, { response: { http_version: parseHttpVersion(nextVer) } })}
isValid={isValidHttpVersion}
inline
/>
&nbsp;
<ValueEditor
ref="code"
content={flow.response.status_code + ''}
onDone={code => FlowActions.update(flow, { response: { code: parseInt(code) } })}
isValid={code => /^\d+$/.test(code)}
inline
/>
&nbsp;
<ValueEditor
ref="msg"
content={flow.response.reason}
onDone={msg => FlowActions.update(flow, { response: { msg } })}
inline
/>
</div>
)
}
}
export class Request extends Component {
render() {
const { flow } = this.props
return (
<section className="request">
<RequestLine ref="requestLine" flow={flow}/>
<Headers
ref="headers"
message={flow.request}
onChange={headers => FlowActions.update(flow, { request: { headers } })}
/>
<hr/>
<ContentView flow={flow} message={flow.request}/>
</section>
)
}
edit(k) {
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}`)
}
}
}
export class Response extends Component {
render() {
const { flow } = this.props
return (
<section className="response">
<ResponseLine ref="responseLine" flow={flow}/>
<Headers
ref="headers"
message={flow.response}
onChange={headers => FlowActions.update(flow, { response: { headers } })}
/>
<hr/>
<ContentView flow={flow} message={flow.response}/>
</section>
)
}
edit(k) {
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}`)
}
}
}
export function Error({ flow }) {
return (
<section>
<div className="alert alert-warning">
{flow.error.msg}
<div>
<small>{formatTimeStamp(flow.error.timestamp)}</small>
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,57 @@
import React, { PropTypes } from 'react'
import classnames from 'classnames'
import { FlowActions } from '../../actions.js'
NavAction.propTypes = {
icon: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
}
function NavAction({ icon, title, onClick }) {
return (
<a title={title}
href="#"
className="nav-action"
onClick={event => {
event.preventDefault()
onClick(event)
}}>
<i className={`fa fa-fw ${icon}`}></i>
</a>
)
}
Nav.propTypes = {
flow: PropTypes.object.isRequired,
active: PropTypes.string.isRequired,
tabs: PropTypes.array.isRequired,
onSelectTab: PropTypes.func.isRequired,
}
export default function Nav({ flow, active, tabs, onSelectTab }) {
return (
<nav className="nav-tabs nav-tabs-sm">
{tabs.map(tab => (
<a key={tab}
href="#"
className={classnames({ active: active === tab })}
onClick={event => {
event.preventDefault()
onSelectTab(tab)
}}>
{_.capitalize(tab)}
</a>
))}
<NavAction title="[d]elete flow" icon="fa-trash" onClick={() => FlowActions.delete(flow)} />
<NavAction title="[D]uplicate flow" icon="fa-copy" onClick={() => FlowActions.duplicate(flow)} />
<NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={() => FlowActions.replay(flow)} />
{flow.intercepted && (
<NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={() => FlowActions.accept(flow)} />
)}
{flow.modified && (
<NavAction title="revert changes to flow [V]" icon="fa-history" onClick={() => FlowActions.revert(flow)} />
)}
</nav>
)
}

View File

@ -5,7 +5,7 @@ import { Query } from '../actions.js'
import { Key } from '../utils.js'
import { Splitter } from './common.js'
import FlowTable from './FlowTable'
import FlowView from './flowview/index.js'
import FlowView from './FlowView'
import { selectFlow, setFilter, setHighlight } from '../ducks/flows'
class MainView extends Component {

View File

@ -1,14 +1,13 @@
import React, { Component, PropTypes } from "react"
import ReactDOM from "react-dom"
import _ from "lodash"
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 from "./Header"
import EventLog from "./EventLog"
import Footer from "./Footer"
import { SettingsStore } from "../store/store.js"
import { Key } from "../utils.js"
import Header from './Header'
import EventLog from './EventLog'
import Footer from './Footer'
import { SettingsStore } from '../store/store.js'
import { Key } from '../utils.js'
class ProxyAppMain extends Component {
@ -67,7 +66,7 @@ class ProxyAppMain extends Component {
*/
componentDidMount() {
this.focus()
this.settingsStore.addListener("recalculate", this.onSettingsChange)
this.settingsStore.addListener('recalculate', this.onSettingsChange)
}
/**
@ -76,7 +75,7 @@ class ProxyAppMain extends Component {
* @todo stop listening to window's key events
*/
componentWillUnmount() {
this.settingsStore.removeListener("recalculate", this.onSettingsChange)
this.settingsStore.removeListener('recalculate', this.onSettingsChange)
}
/**
@ -113,13 +112,13 @@ class ProxyAppMain extends Component {
switch (e.keyCode) {
case Key.I:
name = "intercept"
name = 'intercept'
break
case Key.L:
name = "search"
name = 'search'
break
case Key.H:
name = "highlight"
name = 'highlight'
break
default:
let main = this.refs.view
@ -134,7 +133,7 @@ class ProxyAppMain extends Component {
if (name) {
const headerComponent = this.refs.header
headerComponent.setState({ active: Header.entries.MainMenu }, () => {
headerComponent.setState({ active: Header.entries[0] }, () => {
headerComponent.refs.active.refs[name].select()
})
}
@ -151,12 +150,11 @@ class ProxyAppMain extends Component {
<Header ref="header" settings={settings} updateLocation={this.updateLocation} query={query} />
{React.cloneElement(
children,
{ ref: "view", location, query, updateLocation: this.updateLocation }
{ ref: 'view', location, query, updateLocation: this.updateLocation }
)}
{showEventLog && [
<Splitter key="splitter" axis="y"/>,
{showEventLog && (
<EventLog key="eventlog"/>
]}
)}
<Footer settings={settings}/>
</div>
)

View File

@ -1,267 +0,0 @@
import React from "react";
import _ from "lodash";
import {MessageUtils} from "../../flow/utils.js";
import {formatSize} from "../../utils.js";
var ViewImage = React.createClass({
propTypes: {
flow: React.PropTypes.object.isRequired,
message: React.PropTypes.object.isRequired,
},
statics: {
regex: /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i,
matches: function (message) {
return ViewImage.regex.test(MessageUtils.getContentType(message));
}
},
render: function () {
var url = MessageUtils.getContentURL(this.props.flow, this.props.message);
return <div className="flowview-image">
<img src={url} alt="preview" className="img-thumbnail"/>
</div>;
}
});
var ContentLoader = React.createClass({
propTypes: {
flow: React.PropTypes.object.isRequired,
message: React.PropTypes.object.isRequired,
},
getInitialState: function () {
return {
content: undefined,
request: undefined
}
},
requestContent: function (nextProps) {
if (this.state.request) {
this.state.request.abort();
}
var request = MessageUtils.getContent(nextProps.flow, nextProps.message);
this.setState({
content: undefined,
request: request
});
request.done(function (data) {
this.setState({content: data});
}.bind(this)).fail(function (jqXHR, textStatus, errorThrown) {
if (textStatus === "abort") {
return;
}
this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown});
}.bind(this)).always(function () {
this.setState({request: undefined});
}.bind(this));
},
componentWillMount: function () {
this.requestContent(this.props);
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.message !== this.props.message) {
this.requestContent(nextProps);
}
},
componentWillUnmount: function () {
if (this.state.request) {
this.state.request.abort();
}
},
render: function () {
if (!this.state.content) {
return <div className="text-center">
<i className="fa fa-spinner fa-spin"></i>
</div>;
}
return React.cloneElement(this.props.children, {
content: this.state.content
})
}
});
var ViewRaw = React.createClass({
propTypes: {
content: React.PropTypes.string.isRequired,
},
statics: {
textView: true,
matches: function (message) {
return true;
}
},
render: function () {
return <pre>{this.props.content}</pre>;
}
});
var ViewJSON = React.createClass({
propTypes: {
content: React.PropTypes.string.isRequired,
},
statics: {
textView: true,
regex: /^application\/json$/i,
matches: function (message) {
return ViewJSON.regex.test(MessageUtils.getContentType(message));
}
},
render: function () {
var json = this.props.content;
try {
json = JSON.stringify(JSON.parse(json), null, 2);
} catch (e) {
// @noop
}
return <pre>{json}</pre>;
}
});
var ViewAuto = React.createClass({
propTypes: {
message: React.PropTypes.object.isRequired,
flow: React.PropTypes.object.isRequired,
},
statics: {
matches: function () {
return false; // don't match itself
},
findView: function (message) {
for (var i = 0; i < all.length; i++) {
if (all[i].matches(message)) {
return all[i];
}
}
return all[all.length - 1];
}
},
render: function () {
var { message, flow } = this.props
var View = ViewAuto.findView(this.props.message);
if (View.textView) {
return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader>
} else {
return <View message={message} flow={flow} />
}
}
});
var all = [ViewAuto, ViewImage, ViewJSON, ViewRaw];
var ContentEmpty = React.createClass({
render: function () {
var message_name = this.props.flow.request === this.props.message ? "request" : "response";
return <div className="alert alert-info">No {message_name} content.</div>;
}
});
var ContentMissing = React.createClass({
render: function () {
var message_name = this.props.flow.request === this.props.message ? "Request" : "Response";
return <div className="alert alert-info">{message_name} content missing.</div>;
}
});
var TooLarge = React.createClass({
statics: {
isTooLarge: function (message) {
var max_mb = ViewImage.matches(message) ? 10 : 0.2;
return message.contentLength > 1024 * 1024 * max_mb;
}
},
render: function () {
var size = formatSize(this.props.message.contentLength);
return <div className="alert alert-warning">
<button onClick={this.props.onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
{size} content size.
</div>;
}
});
var ViewSelector = React.createClass({
render: function () {
var views = [];
for (var i = 0; i < all.length; i++) {
var view = all[i];
var className = "btn btn-default";
if (view === this.props.active) {
className += " active";
}
var text;
if (view === ViewAuto) {
text = "auto: " + ViewAuto.findView(this.props.message).displayName.toLowerCase().replace("view", "");
} else {
text = view.displayName.toLowerCase().replace("view", "");
}
views.push(
<button
key={view.displayName}
onClick={this.props.selectView.bind(null, view)}
className={className}>
{text}
</button>
);
}
return <div className="view-selector btn-group btn-group-xs">{views}</div>;
}
});
var ContentView = React.createClass({
getInitialState: function () {
return {
displayLarge: false,
View: ViewAuto
};
},
propTypes: {
// It may seem a bit weird at the first glance:
// Every view takes the flow and the message as props, e.g.
// <Auto flow={flow} message={flow.request}/>
flow: React.PropTypes.object.isRequired,
message: React.PropTypes.object.isRequired,
},
selectView: function (view) {
this.setState({
View: view
});
},
displayLarge: function () {
this.setState({displayLarge: true});
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.message !== this.props.message) {
this.setState(this.getInitialState());
}
},
render: function () {
var { flow, message } = this.props
var message = this.props.message;
if (message.contentLength === 0) {
return <ContentEmpty {...this.props}/>;
} else if (message.contentLength === null) {
return <ContentMissing {...this.props}/>;
} else if (!this.state.displayLarge && TooLarge.isTooLarge(message)) {
return <TooLarge {...this.props} onClick={this.displayLarge}/>;
}
var downloadUrl = MessageUtils.getContentURL(this.props.flow, message);
return <div>
{this.state.View.textView ? (
<ContentLoader flow={flow} message={message}><this.state.View content="" /></ContentLoader>
) : (
<this.state.View flow={flow} message={message} />
)}
<div className="view-options text-center">
<ViewSelector selectView={this.selectView} active={this.state.View} message={message}/>
&nbsp;
<a className="btn btn-default btn-xs" href={downloadUrl}>
<i className="fa fa-download"/>
</a>
</div>
</div>;
}
});
export default ContentView;

View File

@ -1,181 +0,0 @@
import React from "react";
import _ from "lodash";
import {formatTimeStamp, formatTimeDelta} from "../../utils.js";
var TimeStamp = React.createClass({
render: function () {
if (!this.props.t) {
//should be return null, but that triggers a React bug.
return <tr></tr>;
}
var ts = formatTimeStamp(this.props.t);
var delta;
if (this.props.deltaTo) {
delta = formatTimeDelta(1000 * (this.props.t - this.props.deltaTo));
delta = <span className="text-muted">{"(" + delta + ")"}</span>;
} else {
delta = null;
}
return <tr>
<td>{this.props.title + ":"}</td>
<td>{ts} {delta}</td>
</tr>;
}
});
var ConnectionInfo = React.createClass({
render: function () {
var conn = this.props.conn;
var address = conn.address.address.join(":");
var sni = <tr key="sni"></tr>; //should be null, but that triggers a React bug.
if (conn.sni) {
sni = <tr key="sni">
<td>
<abbr title="TLS Server Name Indication">TLS SNI:</abbr>
</td>
<td>{conn.sni}</td>
</tr>;
}
return (
<table className="connection-table">
<tbody>
<tr key="address">
<td>Address:</td>
<td>{address}</td>
</tr>
{sni}
</tbody>
</table>
);
}
});
var CertificateInfo = React.createClass({
render: function () {
//TODO: We should fetch human-readable certificate representation
// from the server
var flow = this.props.flow;
var client_conn = flow.client_conn;
var server_conn = flow.server_conn;
var preStyle = {maxHeight: 100};
return (
<div>
{client_conn.cert ? <h4>Client Certificate</h4> : null}
{client_conn.cert ? <pre style={preStyle}>{client_conn.cert}</pre> : null}
{server_conn.cert ? <h4>Server Certificate</h4> : null}
{server_conn.cert ? <pre style={preStyle}>{server_conn.cert}</pre> : null}
</div>
);
}
});
var Timing = React.createClass({
render: function () {
var flow = this.props.flow;
var sc = flow.server_conn;
var cc = flow.client_conn;
var req = flow.request;
var resp = flow.response;
var timestamps = [
{
title: "Server conn. initiated",
t: sc.timestamp_start,
deltaTo: req.timestamp_start
}, {
title: "Server conn. TCP handshake",
t: sc.timestamp_tcp_setup,
deltaTo: req.timestamp_start
}, {
title: "Server conn. SSL handshake",
t: sc.timestamp_ssl_setup,
deltaTo: req.timestamp_start
}, {
title: "Client conn. established",
t: cc.timestamp_start,
deltaTo: req.timestamp_start
}, {
title: "Client conn. SSL handshake",
t: cc.timestamp_ssl_setup,
deltaTo: req.timestamp_start
}, {
title: "First request byte",
t: req.timestamp_start,
}, {
title: "Request complete",
t: req.timestamp_end,
deltaTo: req.timestamp_start
}
];
if (flow.response) {
timestamps.push(
{
title: "First response byte",
t: resp.timestamp_start,
deltaTo: req.timestamp_start
}, {
title: "Response complete",
t: resp.timestamp_end,
deltaTo: req.timestamp_start
}
);
}
//Add unique key for each row.
timestamps.forEach(function (e) {
e.key = e.title;
});
timestamps = _.sortBy(timestamps, 't');
var rows = timestamps.map(function (e) {
return <TimeStamp {...e}/>;
});
return (
<div>
<h4>Timing</h4>
<table className="timing-table">
<tbody>
{rows}
</tbody>
</table>
</div>
);
}
});
var Details = React.createClass({
render: function () {
var flow = this.props.flow;
var client_conn = flow.client_conn;
var server_conn = flow.server_conn;
return (
<section>
<h4>Client Connection</h4>
<ConnectionInfo conn={client_conn}/>
<h4>Server Connection</h4>
<ConnectionInfo conn={server_conn}/>
<CertificateInfo flow={flow}/>
<Timing flow={flow}/>
</section>
);
}
});
export default Details;

View File

@ -1,114 +0,0 @@
import React from "react";
import Nav from "./nav.js";
import {Request, Response, Error} from "./messages.js";
import Details from "./details.js";
import Prompt from "../prompt.js";
var allTabs = {
request: Request,
response: Response,
error: Error,
details: Details
};
var FlowView = React.createClass({
getInitialState: function () {
return {
prompt: false
};
},
getTabs: function (flow) {
var tabs = [];
["request", "response", "error"].forEach(function (e) {
if (flow[e]) {
tabs.push(e);
}
});
tabs.push("details");
return tabs;
},
nextTab: function (i) {
var tabs = this.getTabs(this.props.flow);
var currentIndex = tabs.indexOf(this.props.tab);
// JS modulo operator doesn't correct negative numbers, make sure that we are positive.
var nextIndex = (currentIndex + i + tabs.length) % tabs.length;
this.selectTab(tabs[nextIndex]);
},
selectTab: function (panel) {
this.props.updateLocation(`/flows/${this.props.flow.id}/${panel}`);
},
promptEdit: function () {
var options;
switch (this.props.tab) {
case "request":
options = [
"method",
"url",
{text: "http version", key: "v"},
"header"
/*, "content"*/];
break;
case "response":
options = [
{text: "http version", key: "v"},
"code",
"message",
"header"
/*, "content"*/];
break;
case "details":
return;
default:
throw "Unknown tab for edit: " + this.props.tab;
}
this.setState({
prompt: {
done: function (k) {
this.setState({prompt: false});
if (k) {
this.refs.tab.edit(k);
}
}.bind(this),
options: options
}
});
},
render: function () {
var flow = this.props.flow;
var tabs = this.getTabs(flow);
var active = this.props.tab;
if (tabs.indexOf(active) < 0) {
if (active === "response" && flow.error) {
active = "error";
} else if (active === "error" && flow.response) {
active = "response";
} else {
active = tabs[0];
}
}
var prompt = null;
if (this.state.prompt) {
prompt = <Prompt {...this.state.prompt}/>;
}
var Tab = allTabs[active];
return (
<div className="flow-detail" onScroll={this.adjustHead}>
<Nav ref="head"
flow={flow}
tabs={tabs}
active={active}
selectTab={this.selectTab}/>
<Tab ref="tab" flow={flow}/>
{prompt}
</div>
);
}
});
export default FlowView;

View File

@ -1,320 +0,0 @@
import React from "react";
import ReactDOM from 'react-dom';
import _ from "lodash";
import {FlowActions} from "../../actions.js";
import {RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion} from "../../flow/utils.js";
import {Key, formatTimeStamp} from "../../utils.js";
import ContentView from "./contentview.js";
import {ValueEditor} from "../editor.js";
var Headers = React.createClass({
propTypes: {
onChange: React.PropTypes.func.isRequired,
message: React.PropTypes.object.isRequired
},
onChange: function (row, col, val) {
var nextHeaders = _.cloneDeep(this.props.message.headers);
nextHeaders[row][col] = val;
if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
// do not delete last row
if (nextHeaders.length === 1) {
nextHeaders[0][0] = "Name";
nextHeaders[0][1] = "Value";
} else {
nextHeaders.splice(row, 1);
// manually move selection target if this has been the last row.
if (row === nextHeaders.length) {
this._nextSel = (row - 1) + "-value";
}
}
}
this.props.onChange(nextHeaders);
},
edit: function () {
this.refs["0-key"].focus();
},
onTab: function (row, col, e) {
var headers = this.props.message.headers;
if (row === headers.length - 1 && col === 1) {
e.preventDefault();
var nextHeaders = _.cloneDeep(this.props.message.headers);
nextHeaders.push(["Name", "Value"]);
this.props.onChange(nextHeaders);
this._nextSel = (row + 1) + "-key";
}
},
componentDidUpdate: function () {
if (this._nextSel && this.refs[this._nextSel]) {
this.refs[this._nextSel].focus();
this._nextSel = undefined;
}
},
onRemove: function (row, col, e) {
if (col === 1) {
e.preventDefault();
this.refs[row + "-key"].focus();
} else if (row > 0) {
e.preventDefault();
this.refs[(row - 1) + "-value"].focus();
}
},
render: function () {
var rows = this.props.message.headers.map(function (header, i) {
var kEdit = <HeaderEditor
ref={i + "-key"}
content={header[0]}
onDone={this.onChange.bind(null, i, 0)}
onRemove={this.onRemove.bind(null, i, 0)}
onTab={this.onTab.bind(null, i, 0)}/>;
var vEdit = <HeaderEditor
ref={i + "-value"}
content={header[1]}
onDone={this.onChange.bind(null, i, 1)}
onRemove={this.onRemove.bind(null, i, 1)}
onTab={this.onTab.bind(null, i, 1)}/>;
return (
<tr key={i}>
<td className="header-name">{kEdit}:</td>
<td className="header-value">{vEdit}</td>
</tr>
);
}.bind(this));
return (
<table className="header-table">
<tbody>
{rows}
</tbody>
</table>
);
}
});
var HeaderEditor = React.createClass({
render: function () {
return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>;
},
focus: function () {
ReactDOM.findDOMNode(this).focus();
},
onKeyDown: function (e) {
switch (e.keyCode) {
case Key.BACKSPACE:
var s = window.getSelection().getRangeAt(0);
if (s.startOffset === 0 && s.endOffset === 0) {
this.props.onRemove(e);
}
break;
case Key.TAB:
if (!e.shiftKey) {
this.props.onTab(e);
}
break;
}
}
});
var RequestLine = React.createClass({
render: function () {
var flow = this.props.flow;
var url = RequestUtils.pretty_url(flow.request);
var httpver = flow.request.http_version;
return <div className="first-line request-line">
<ValueEditor
ref="method"
content={flow.request.method}
onDone={this.onMethodChange}
inline/>
&nbsp;
<ValueEditor
ref="url"
content={url}
onDone={this.onUrlChange}
isValid={this.isValidUrl}
inline/>
&nbsp;
<ValueEditor
ref="httpVersion"
content={httpver}
onDone={this.onHttpVersionChange}
isValid={isValidHttpVersion}
inline/>
</div>
},
isValidUrl: function (url) {
var u = parseUrl(url);
return !!u.host;
},
onMethodChange: function (nextMethod) {
FlowActions.update(
this.props.flow,
{request: {method: nextMethod}}
);
},
onUrlChange: function (nextUrl) {
var props = parseUrl(nextUrl);
props.path = props.path || "";
FlowActions.update(
this.props.flow,
{request: props}
);
},
onHttpVersionChange: function (nextVer) {
var ver = parseHttpVersion(nextVer);
FlowActions.update(
this.props.flow,
{request: {http_version: ver}}
);
}
});
var ResponseLine = React.createClass({
render: function () {
var flow = this.props.flow;
var httpver = flow.response.http_version;
return <div className="first-line response-line">
<ValueEditor
ref="httpVersion"
content={httpver}
onDone={this.onHttpVersionChange}
isValid={isValidHttpVersion}
inline/>
&nbsp;
<ValueEditor
ref="code"
content={flow.response.status_code + ""}
onDone={this.onCodeChange}
isValid={this.isValidCode}
inline/>
&nbsp;
<ValueEditor
ref="msg"
content={flow.response.reason}
onDone={this.onMsgChange}
inline/>
</div>;
},
isValidCode: function (code) {
return /^\d+$/.test(code);
},
onHttpVersionChange: function (nextVer) {
var ver = parseHttpVersion(nextVer);
FlowActions.update(
this.props.flow,
{response: {http_version: ver}}
);
},
onMsgChange: function (nextMsg) {
FlowActions.update(
this.props.flow,
{response: {msg: nextMsg}}
);
},
onCodeChange: function (nextCode) {
nextCode = parseInt(nextCode);
FlowActions.update(
this.props.flow,
{response: {code: nextCode}}
);
}
});
export var Request = React.createClass({
render: function () {
var flow = this.props.flow;
return (
<section className="request">
<RequestLine ref="requestLine" flow={flow}/>
{/*<ResponseLine flow={flow}/>*/}
<Headers ref="headers" message={flow.request} onChange={this.onHeaderChange}/>
<hr/>
<ContentView flow={flow} message={flow.request}/>
</section>
);
},
edit: function (k) {
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 "Unimplemented: " + k;
}
},
onHeaderChange: function (nextHeaders) {
FlowActions.update(this.props.flow, {
request: {
headers: nextHeaders
}
});
}
});
export var Response = React.createClass({
render: function () {
var flow = this.props.flow;
return (
<section className="response">
{/*<RequestLine flow={flow}/>*/}
<ResponseLine ref="responseLine" flow={flow}/>
<Headers ref="headers" message={flow.response} onChange={this.onHeaderChange}/>
<hr/>
<ContentView flow={flow} message={flow.response}/>
</section>
);
},
edit: function (k) {
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 "Unimplemented: " + k;
}
},
onHeaderChange: function (nextHeaders) {
FlowActions.update(this.props.flow, {
response: {
headers: nextHeaders
}
});
}
});
export var Error = React.createClass({
render: function () {
var flow = this.props.flow;
return (
<section>
<div className="alert alert-warning">
{flow.error.msg}
<div>
<small>{ formatTimeStamp(flow.error.timestamp) }</small>
</div>
</div>
</section>
);
}
});

View File

@ -1,61 +0,0 @@
import React from "react";
import {FlowActions} from "../../actions.js";
var NavAction = React.createClass({
onClick: function (e) {
e.preventDefault();
this.props.onClick();
},
render: function () {
return (
<a title={this.props.title}
href="#"
className="nav-action"
onClick={this.onClick}>
<i className={"fa fa-fw " + this.props.icon}></i>
</a>
);
}
});
var Nav = React.createClass({
render: function () {
var flow = this.props.flow;
var tabs = this.props.tabs.map(function (e) {
var str = e.charAt(0).toUpperCase() + e.slice(1);
var className = this.props.active === e ? "active" : "";
var onClick = function (event) {
this.props.selectTab(e);
event.preventDefault();
}.bind(this);
return <a key={e}
href="#"
className={className}
onClick={onClick}>{str}</a>;
}.bind(this));
var acceptButton = null;
if(flow.intercepted){
acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={FlowActions.accept.bind(null, flow)} />;
}
var revertButton = null;
if(flow.modified){
revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={FlowActions.revert.bind(null, flow)} />;
}
return (
<nav ref="head" className="nav-tabs nav-tabs-sm">
{tabs}
<NavAction title="[d]elete flow" icon="fa-trash" onClick={FlowActions.delete.bind(null, flow)} />
<NavAction title="[D]uplicate flow" icon="fa-copy" onClick={FlowActions.duplicate.bind(null, flow)} />
<NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={FlowActions.replay.bind(null, flow)} />
{acceptButton}
{revertButton}
</nav>
);
}
});
export default Nav;