diff options
| author | Maximilian Hils <git@maximilianhils.com> | 2016-06-17 01:53:02 -0700 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-06-17 01:53:02 -0700 | 
| commit | fcf5dc8728816bae73a175ee021f8a11a1591567 (patch) | |
| tree | 56ff25784a659f0e54894ca616e198cfe1a13ea0 /web/src/js/components/FlowView | |
| parent | 78785df16be237bfdbf4ee485639b61f06b4a47e (diff) | |
| parent | 034287edcf00eb734cb67e62de58c3bfebf6bb44 (diff) | |
| download | mitmproxy-fcf5dc8728816bae73a175ee021f8a11a1591567.tar.gz mitmproxy-fcf5dc8728816bae73a175ee021f8a11a1591567.tar.bz2 mitmproxy-fcf5dc8728816bae73a175ee021f8a11a1591567.zip  | |
Merge pull request #1267 from gzzhanghao/components
[web] Working on components
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..880eeda1 --- /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 '../ValueEditor' +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..ba6a5f2b --- /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 '../ValueEditor' +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> +    ) +}  | 
