diff options
author | Jason <jason.daurus@gmail.com> | 2016-06-14 23:52:00 +0800 |
---|---|---|
committer | Jason <jason.daurus@gmail.com> | 2016-06-17 04:37:57 +0800 |
commit | e5bf1e930a5b6ba0b3300b02daf792d65d795202 (patch) | |
tree | f032716e6f31d204ee3b781279c5a9e8444e1e49 /web/src/js | |
parent | 1fc2db85fa339f9b134d45c15d2ad4cf3d681070 (diff) | |
download | mitmproxy-e5bf1e930a5b6ba0b3300b02daf792d65d795202.tar.gz mitmproxy-e5bf1e930a5b6ba0b3300b02daf792d65d795202.tar.bz2 mitmproxy-e5bf1e930a5b6ba0b3300b02daf792d65d795202.zip |
[web] FlowView and ContentView
Diffstat (limited to 'web/src/js')
20 files changed, 944 insertions, 988 deletions
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx new file mode 100644 index 00000000..af3bffc1 --- /dev/null +++ b/web/src/js/components/ContentView.jsx @@ -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}/> + + <a className="btn btn-default btn-xs" href={MessageUtils.getContentURL(flow, message)}> + <i className="fa fa-download"/> + </a> + </div> + </div> + ) + } +} diff --git a/web/src/js/components/ContentView/ContentErrors.jsx b/web/src/js/components/ContentView/ContentErrors.jsx new file mode 100644 index 00000000..11594c7f --- /dev/null +++ b/web/src/js/components/ContentView/ContentErrors.jsx @@ -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> + ) +} diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx new file mode 100644 index 00000000..f346dc01 --- /dev/null +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -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> + ) + } +} diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx new file mode 100644 index 00000000..b0297dcc --- /dev/null +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -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 diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx new file mode 100644 index 00000000..df3a5b83 --- /dev/null +++ b/web/src/js/components/ContentView/ViewSelector.jsx @@ -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> + ) +} diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx index 58cecd1a..d9211e11 100644 --- a/web/src/js/components/EventLog.jsx +++ b/web/src/js/components/EventLog.jsx @@ -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 { + + 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) + } -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> + 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( diff --git a/web/src/js/components/FlowTable/FlowTableHead.jsx b/web/src/js/components/FlowTable/FlowTableHead.jsx index 1df38aba..840f6a34 100644 --- a/web/src/js/components/FlowTable/FlowTableHead.jsx +++ b/web/src/js/components/FlowTable/FlowTableHead.jsx @@ -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( diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx new file mode 100644 index 00000000..be2cb460 --- /dev/null +++ b/web/src/js/components/FlowView.jsx @@ -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> + ) + } +} diff --git a/web/src/js/components/FlowView/Details.jsx b/web/src/js/components/FlowView/Details.jsx new file mode 100644 index 00000000..78e68ecf --- /dev/null +++ b/web/src/js/components/FlowView/Details.jsx @@ -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> + ) +} diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx new file mode 100644 index 00000000..b8f9b50f --- /dev/null +++ b/web/src/js/components/FlowView/Headers.jsx @@ -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> + ) + } +} diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx new file mode 100644 index 00000000..ce17c294 --- /dev/null +++ b/web/src/js/components/FlowView/Messages.jsx @@ -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 + /> + + <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 + /> + + <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 + /> + + <ValueEditor + ref="code" + content={flow.response.status_code + ''} + onDone={code => FlowActions.update(flow, { response: { code: parseInt(code) } })} + isValid={code => /^\d+$/.test(code)} + inline + /> + + <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> + ) +} diff --git a/web/src/js/components/FlowView/Nav.jsx b/web/src/js/components/FlowView/Nav.jsx new file mode 100644 index 00000000..386c3a6c --- /dev/null +++ b/web/src/js/components/FlowView/Nav.jsx @@ -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> + ) +} diff --git a/web/src/js/components/Header.js b/web/src/js/components/Header.jsx index 93ca5154..93ca5154 100644 --- a/web/src/js/components/Header.js +++ b/web/src/js/components/Header.jsx diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx index 8c6ed6d0..78a7f9bf 100644 --- a/web/src/js/components/MainView.jsx +++ b/web/src/js/components/MainView.jsx @@ -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 { diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx index 81272268..967cc921 100644 --- a/web/src/js/components/ProxyApp.jsx +++ b/web/src/js/components/ProxyApp.jsx @@ -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> ) diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js deleted file mode 100644 index cbac9a75..00000000 --- a/web/src/js/components/flowview/contentview.js +++ /dev/null @@ -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}/> - - <a className="btn btn-default btn-xs" href={downloadUrl}> - <i className="fa fa-download"/> - </a> - </div> - </div>; - } -}); - -export default ContentView; diff --git a/web/src/js/components/flowview/details.js b/web/src/js/components/flowview/details.js deleted file mode 100644 index 45fe1292..00000000 --- a/web/src/js/components/flowview/details.js +++ /dev/null @@ -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;
\ No newline at end of file diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js deleted file mode 100644 index 7f5e9768..00000000 --- a/web/src/js/components/flowview/index.js +++ /dev/null @@ -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; diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js deleted file mode 100644 index 2885b3b1..00000000 --- a/web/src/js/components/flowview/messages.js +++ /dev/null @@ -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/> - - <ValueEditor - ref="url" - content={url} - onDone={this.onUrlChange} - isValid={this.isValidUrl} - inline/> - - <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/> - - <ValueEditor - ref="code" - content={flow.response.status_code + ""} - onDone={this.onCodeChange} - isValid={this.isValidCode} - inline/> - - <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> - ); - } -}); diff --git a/web/src/js/components/flowview/nav.js b/web/src/js/components/flowview/nav.js deleted file mode 100644 index a12fd1fd..00000000 --- a/web/src/js/components/flowview/nav.js +++ /dev/null @@ -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;
\ No newline at end of file |