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/components/FlowView | |
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/components/FlowView')
-rw-r--r-- | web/src/js/components/FlowView/Details.jsx | 133 | ||||
-rw-r--r-- | web/src/js/components/FlowView/Headers.jsx | 130 | ||||
-rw-r--r-- | web/src/js/components/FlowView/Messages.jsx | 168 | ||||
-rw-r--r-- | web/src/js/components/FlowView/Nav.jsx | 57 |
4 files changed, 488 insertions, 0 deletions
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> + ) +} |