Merge pull request #4533 from mhils/pr-4188

mitmweb: Make columns configurable/customizable (#4188)
This commit is contained in:
Maximilian Hils 2021-03-30 13:10:05 +02:00 committed by GitHub
commit 04bceb0e05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 324 additions and 209 deletions

View File

@ -41,6 +41,7 @@ If you depend on these features, please raise your voice in
events have been replaced with new events, see addon documentation for details (@mhils)
* Contentviews now implement `render_priority` instead of `should_render`, allowing more specialization (@mhils)
* Addition of block_list option to block requests with a set status code (@ericbeland)
* Make mitmweb columns configurable and customizable (@gorogoroumaru)
* Automatic JSON view mode when `+json` suffix in content type (@kam800)
* Use pyca/cryptography to generate certificates, not pyOpenSSL (@mhils)
* Remove the legacy protocol stack (@Kriechi)

View File

@ -184,9 +184,6 @@ def tserver_conn() -> connection.Server:
return c
def terr(content="error"):
"""
@return: mitmproxy.proxy.protocol.primitives.Error
"""
err = flow.Error(content)
def terr(content: str = "error") -> flow.Error:
err = flow.Error(content, 946681207)
return err

View File

@ -137,6 +137,7 @@ def mitmweb(opts):
opts.make_parser(group, "web_open_browser")
opts.make_parser(group, "web_port", metavar="PORT")
opts.make_parser(group, "web_host", metavar="HOST")
opts.make_parser(group, "web_columns")
common_options(parser, opts)
group = parser.add_argument_group(

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,7 @@
import webbrowser
from mitmproxy import ctx
from typing import Sequence
class WebAddon:
@ -21,6 +22,10 @@ class WebAddon:
"web_host", str, "127.0.0.1",
"Web UI host."
)
loader.add_option(
"web_columns", Sequence[str], ["tls", "icon", "path", "method", "status", "size", "time"],
"Columns to show in the flow list"
)
def running(self):
if hasattr(ctx.options, "web_open_browser") and ctx.options.web_open_browser:

View File

@ -1,9 +1,9 @@
import asyncio
import json as _json
import logging
from unittest import mock
import os
import asyncio
import sys
from unittest import mock
import pytest
@ -331,14 +331,18 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
# Set some value as constant, so that _tflow.js would not change every time.
_tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"
_tflow['id'] = "d91165be-ca1f-4612-88a9-c0f8696f3e29"
_tflow['error']['timestamp'] = 1495370312.4814785
_tflow['response']['timestamp_end'] = 1495370312.4814625
_tflow['response']['timestamp_start'] = 1495370312.481462
_tflow['server_conn']['id'] = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8"
_tflow["request"]["trailers"] = [["trailer", "qvalue"]]
_tflow["response"]["trailers"] = [["trailer", "qvalue"]]
tflow_json = _json.dumps(_tflow, indent=4, sort_keys=True)
here = os.path.abspath(os.path.dirname(__file__))
web_root = os.path.join(here, os.pardir, os.pardir, os.pardir, os.pardir, 'web')
tflow_path = os.path.join(web_root, 'src/js/__tests__/ducks/_tflow.js')
content = f"""export default function(){{\n return {tflow_json}\n}}"""
content = (
f"/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */\n"
f"export default function(){{\n"
f" return {tflow_json}\n"
f"}}"
)
with open(tflow_path, 'w', newline="\n") as f:
f.write(content)

View File

@ -7,6 +7,10 @@ and activate your virtualenv environment before proceeding.**
- Run `yarn run gulp` to start live-compilation.
- Run `mitmweb` and open http://localhost:8081/
## Testing
- Run `yarn run test` to run the testsuite.
## Architecture
There are two components:

View File

@ -214,7 +214,7 @@ gulp.task(
gulp.task("default", gulp.series(
"dev",
function () {
function watch() {
livereload.listen({auto: true});
gulp.watch(["src/css/vendor*"], gulp.series("styles-vendor-dev"));
gulp.watch(["src/css/**"], gulp.series("styles-app-dev"));

View File

@ -125,6 +125,9 @@
.col-time {
width: 50px;
}
.col-timestamp {
width: auto;
}
td.col-time, td.col-size {
text-align: right;
}

View File

@ -105,4 +105,12 @@ describe('FlowColumns Components', () => {
tree = timeColumn.toJSON()
expect(tree).toMatchSnapshot()
})
it('should render TimeStampColumn', () => {
let timeStampColumn = renderer.create(<Columns.TimeStampColumn flow={tflow}/>),
tree = timeStampColumn.toJSON()
tflow.request.timestamp_start =
expect(tree).toMatchSnapshot()
})
})

View File

@ -1,12 +1,17 @@
import React from 'react'
import renderer from 'react-test-renderer'
import FlowRow from '../../../components/FlowTable/FlowRow'
import { TFlow } from '../../ducks/tutils'
import { TFlow, TStore } from '../../ducks/tutils'
import { Provider } from 'react-redux'
describe('FlowRow Component', () => {
let tFlow = new TFlow(),
selectFn = jest.fn(),
flowRow = renderer.create(<FlowRow flow={tFlow} onSelect={selectFn}/>),
store = TStore(),
flowRow = renderer.create(
<Provider store={store} >
<FlowRow flow={tFlow} onSelect={selectFn}/>
</Provider>),
tree = flowRow.toJSON()
it('should render correctly', () => {

View File

@ -7,7 +7,11 @@ import { TStore } from '../../ducks/tutils'
describe('FlowTableHead Component', () => {
let sortFn = jest.fn(),
flowTableHead = renderer.create(<FlowTableHead setSort={sortFn} sortDesc={true}/>),
store = TStore(),
flowTableHead = renderer.create(
<Provider store={store}>
<FlowTableHead setSort={sortFn} sortDesc={true}/>
</Provider>),
tree =flowTableHead.toJSON()
it('should render correctly', () => {

View File

@ -127,7 +127,7 @@ exports[`FlowColumns Components should render TimeColumn 1`] = `
<td
className="col-time"
>
415381h
3s
</td>
`;
@ -139,6 +139,14 @@ exports[`FlowColumns Components should render TimeColumn 2`] = `
</td>
`;
exports[`FlowColumns Components should render TimeStampColumn 1`] = `
<td
className="col-start"
>
1999-12-31 23:00:00.000
</td>
`;
exports[`FlowColumns Components should render pathColumn 1`] = `
<td
className="col-path"

View File

@ -46,7 +46,7 @@ exports[`FlowRow Component should render correctly 1`] = `
<td
className="col-time"
>
415381h
3s
</td>
</tr>
`;

View File

@ -79,6 +79,12 @@ exports[`FlowTableHead Component should render correctly 1`] = `
>
Status
</th>
<th
className="col-timestamp"
onClick={[Function]}
>
TimeStamp
</th>
<th
className="col-size"
onClick={[Function]}

View File

@ -155,6 +155,18 @@ exports[`Details Component should render correctly 1`] = `
TLSv1.2
</td>
</tr>
<tr>
<td>
<abbr
title="ALPN protocol negotiated"
>
ALPN:
</abbr>
</td>
<td>
http/1.1
</td>
</tr>
<tr>
<td>
Resolved address:
@ -182,24 +194,20 @@ exports[`Details Component should render correctly 1`] = `
className="timing-table"
>
<tbody>
<tr />
<tr />
<tr>
<td>
Server conn. initiated
:
</td>
<td>
1970-01-01 00:00:01.000
</td>
</tr>
<tr>
<td>
Client conn. established
:
</td>
<td>
1970-01-01 00:00:01.000
1999-12-31 23:00:02.000
<span
className="text-muted"
>
(
2s
)
</span>
</td>
</tr>
<tr>
@ -208,25 +216,57 @@ exports[`Details Component should render correctly 1`] = `
:
</td>
<td>
1970-01-01 00:00:02.000
1999-12-31 23:00:03.000
<span
className="text-muted"
>
(
3s
)
</span>
</td>
</tr>
<tr />
<tr>
<td>
Client conn. established
:
</td>
<td>
1999-12-31 23:00:00.000
<span
className="text-muted"
>
(
0ms
)
</span>
</td>
</tr>
<tr />
<tr>
<td>
First request byte
:
</td>
<td>
1999-12-31 23:00:00.000
</td>
</tr>
<tr>
<td>
Client conn. SSL handshake
Request complete
:
</td>
<td>
1970-01-01 00:00:02.000
</td>
</tr>
<tr>
<td>
Server conn. SSL handshake
:
</td>
<td>
1970-01-01 00:00:03.000
1999-12-31 23:00:01.000
<span
className="text-muted"
>
(
1s
)
</span>
</td>
</tr>
<tr>
@ -235,7 +275,14 @@ exports[`Details Component should render correctly 1`] = `
:
</td>
<td>
2017-05-21 12:38:32.481
1999-12-31 23:00:02.000
<span
className="text-muted"
>
(
2s
)
</span>
</td>
</tr>
<tr>
@ -244,7 +291,14 @@ exports[`Details Component should render correctly 1`] = `
:
</td>
<td>
2017-05-21 12:38:32.481
1999-12-31 23:00:03.000
<span
className="text-muted"
>
(
3s
)
</span>
</td>
</tr>
</tbody>
@ -323,8 +377,6 @@ exports[`Details Component should render correctly when server address is missin
className="timing-table"
>
<tbody>
<tr />
<tr />
<tr />
<tr />
<tr />
@ -334,16 +386,40 @@ exports[`Details Component should render correctly when server address is missin
:
</td>
<td>
1970-01-01 00:00:01.000
1999-12-31 23:00:00.000
<span
className="text-muted"
>
(
0ms
)
</span>
</td>
</tr>
<tr />
<tr>
<td>
First request byte
:
</td>
<td>
1999-12-31 23:00:00.000
</td>
</tr>
<tr>
<td>
Client conn. SSL handshake
Request complete
:
</td>
<td>
1970-01-01 00:00:02.000
1999-12-31 23:00:01.000
<span
className="text-muted"
>
(
1s
)
</span>
</td>
</tr>
<tr>
@ -352,7 +428,14 @@ exports[`Details Component should render correctly when server address is missin
:
</td>
<td>
2017-05-21 12:38:32.481
1999-12-31 23:00:02.000
<span
className="text-muted"
>
(
2s
)
</span>
</td>
</tr>
<tr>
@ -361,7 +444,14 @@ exports[`Details Component should render correctly when server address is missin
:
</td>
<td>
2017-05-21 12:38:32.481
1999-12-31 23:00:03.000
<span
className="text-muted"
>
(
3s
)
</span>
</td>
</tr>
</tbody>
@ -400,24 +490,20 @@ exports[`Timing Component should render correctly 1`] = `
className="timing-table"
>
<tbody>
<tr />
<tr />
<tr>
<td>
Server conn. initiated
:
</td>
<td>
1970-01-01 00:00:01.000
</td>
</tr>
<tr>
<td>
Client conn. established
:
</td>
<td>
1970-01-01 00:00:01.000
1999-12-31 23:00:02.000
<span
className="text-muted"
>
(
2s
)
</span>
</td>
</tr>
<tr>
@ -426,25 +512,57 @@ exports[`Timing Component should render correctly 1`] = `
:
</td>
<td>
1970-01-01 00:00:02.000
1999-12-31 23:00:03.000
<span
className="text-muted"
>
(
3s
)
</span>
</td>
</tr>
<tr />
<tr>
<td>
Client conn. established
:
</td>
<td>
1999-12-31 23:00:00.000
<span
className="text-muted"
>
(
0ms
)
</span>
</td>
</tr>
<tr />
<tr>
<td>
First request byte
:
</td>
<td>
1999-12-31 23:00:00.000
</td>
</tr>
<tr>
<td>
Client conn. SSL handshake
Request complete
:
</td>
<td>
1970-01-01 00:00:02.000
</td>
</tr>
<tr>
<td>
Server conn. SSL handshake
:
</td>
<td>
1970-01-01 00:00:03.000
1999-12-31 23:00:01.000
<span
className="text-muted"
>
(
1s
)
</span>
</td>
</tr>
<tr>
@ -453,7 +571,14 @@ exports[`Timing Component should render correctly 1`] = `
:
</td>
<td>
2017-05-21 12:38:32.481
1999-12-31 23:00:02.000
<span
className="text-muted"
>
(
2s
)
</span>
</td>
</tr>
<tr>
@ -462,7 +587,14 @@ exports[`Timing Component should render correctly 1`] = `
:
</td>
<td>
2017-05-21 12:38:32.481
1999-12-31 23:00:03.000
<span
className="text-muted"
>
(
3s
)
</span>
</td>
</tr>
</tbody>

View File

@ -10,7 +10,7 @@ exports[`Error Component should render correctly 1`] = `
error
<div>
<small>
2017-05-21 12:38:32.481
1999-12-31 23:00:07.000
</small>
</div>
</div>
@ -253,55 +253,6 @@ exports[`Request Component should render correctly 1`] = `
/>
</td>
</tr>
<tr>
<td
className="header-name"
>
<div
className="inline-input readonly"
contentEditable={undefined}
dangerouslySetInnerHTML={
Object {
"__html": "content-length",
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
tabIndex={undefined}
/>
<span
className="header-colon"
>
:
</span>
</td>
<td
className="header-value"
>
<div
className="inline-input readonly"
contentEditable={undefined}
dangerouslySetInnerHTML={
Object {
"__html": "7",
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
tabIndex={undefined}
/>
</td>
</tr>
</tbody>
</table>
</article>
@ -621,55 +572,6 @@ exports[`Response Component should render correctly 1`] = `
/>
</td>
</tr>
<tr>
<td
className="header-name"
>
<div
className="inline-input readonly"
contentEditable={undefined}
dangerouslySetInnerHTML={
Object {
"__html": "content-length",
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
tabIndex={undefined}
/>
<span
className="header-colon"
>
:
</span>
</td>
<td
className="header-value"
>
<div
className="inline-input readonly"
contentEditable={undefined}
dangerouslySetInnerHTML={
Object {
"__html": "7",
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
tabIndex={undefined}
/>
</td>
</tr>
</tbody>
</table>
</article>

View File

@ -1,3 +1,4 @@
/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */
export default function(){
return {
"client_conn": {
@ -7,21 +8,21 @@ export default function(){
],
"alpn_proto_negotiated": "http/1.1",
"cipher_name": "cipher",
"clientcert": null,
"id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
"sni": "address",
"ssl_established": false,
"timestamp_end": 3.0,
"timestamp_ssl_setup": 2.0,
"timestamp_start": 1.0,
"timestamp_end": 946681206,
"timestamp_start": 946681200,
"timestamp_tls_setup": 946681201,
"tls_established": true,
"tls_version": "TLSv1.2"
},
"error": {
"msg": "error",
"timestamp": 1495370312.4814785
"timestamp": 946681207.0
},
"id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
"intercepted": false,
"is_replay": null,
"marked": false,
"modified": false,
"request": {
@ -45,16 +46,12 @@ export default function(){
"port": 22,
"pretty_host": "address",
"scheme": "http",
"timestamp_end": null,
"timestamp_start": null,
"timestamp_end": 946681201,
"timestamp_start": 946681200,
"trailers": [
[
"trailer",
"qvalue"
],
[
"content-length",
"7"
]
]
},
@ -75,16 +72,12 @@ export default function(){
"is_replay": false,
"reason": "OK",
"status_code": 200,
"timestamp_end": 1495370312.4814625,
"timestamp_start": 1495370312.481462,
"timestamp_end": 946681203,
"timestamp_start": 946681202,
"trailers": [
[
"trailer",
"qvalue"
],
[
"content-length",
"7"
]
]
},
@ -93,7 +86,7 @@ export default function(){
"address",
22
],
"alpn_proto_negotiated": null,
"alpn_proto_negotiated": "http/1.1",
"id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
"ip_address": [
"192.168.0.1",
@ -104,13 +97,12 @@ export default function(){
"address",
22
],
"ssl_established": false,
"timestamp_end": 4.0,
"timestamp_ssl_setup": 3.0,
"timestamp_start": 1.0,
"timestamp_tcp_setup": 2.0,
"tls_version": "TLSv1.2",
"via": null
"timestamp_end": 946681205,
"timestamp_start": 946681202,
"timestamp_tcp_setup": 946681203,
"timestamp_tls_setup": 946681204,
"tls_established": true,
"tls_version": "TLSv1.2"
},
"type": "http"
}

View File

@ -1,7 +1,9 @@
import React, { Component } from 'react'
import classnames from 'classnames'
import { RequestUtils, ResponseUtils } from '../../flow/utils.js'
import { formatSize, formatTimeDelta } from '../../utils.js'
import { formatSize, formatTimeDelta, formatTimeStamp } from '../../utils.js'
export const defaultColumnNames = ["tls", "icon", "path", "method", "status", "size", "time"]
export function TLSColumn({ flow }) {
return (
@ -148,12 +150,28 @@ export function TimeColumn({ flow }) {
TimeColumn.headerClass = 'col-time'
TimeColumn.headerName = 'Time'
export function TimeStampColumn({ flow }) {
return (
<td className="col-start">
{flow.request.timestamp_start ? (
formatTimeStamp(flow.request.timestamp_start)
) : (
'...'
)}
</td>
)
}
TimeStampColumn.headerClass = 'col-timestamp'
TimeStampColumn.headerName = 'TimeStamp'
export default [
TLSColumn,
IconColumn,
PathColumn,
MethodColumn,
StatusColumn,
TimeStampColumn,
SizeColumn,
TimeColumn,
]

View File

@ -1,8 +1,10 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import columns from './FlowColumns'
import {defaultColumnNames} from './FlowColumns'
import { pure } from '../../utils'
import {getDisplayColumns} from './FlowTableHead'
import { connect } from 'react-redux'
FlowRow.propTypes = {
onSelect: PropTypes.func.isRequired,
@ -11,7 +13,7 @@ FlowRow.propTypes = {
selected: PropTypes.bool,
}
function FlowRow({ flow, selected, highlighted, onSelect }) {
function FlowRow({ flow, selected, highlighted, onSelect, displayColumnNames }) {
const className = classnames({
'selected': selected,
'highlighted': highlighted,
@ -20,13 +22,19 @@ function FlowRow({ flow, selected, highlighted, onSelect }) {
'has-response': flow.response,
})
const displayColumns = getDisplayColumns(displayColumnNames)
return (
<tr className={className} onClick={() => onSelect(flow.id)}>
{columns.map(Column => (
{displayColumns.map(Column => (
<Column key={Column.name} flow={flow}/>
))}
</tr>
)
}
export default pure(FlowRow)
export default connect(
state => ({
displayColumnNames: state.options["web_columns"] ? state.options["web_columns"].value : defaultColumnNames,
})
)(pure(FlowRow))

View File

@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
import columns from './FlowColumns'
import columns, {defaultColumnNames} from './FlowColumns'
import { setSort } from '../../ducks/flows'
@ -10,14 +10,30 @@ FlowTableHead.propTypes = {
setSort: PropTypes.func.isRequired,
sortDesc: PropTypes.bool.isRequired,
sortColumn: PropTypes.string,
displayColumnNames: PropTypes.array,
}
export function FlowTableHead({ sortColumn, sortDesc, setSort }) {
export function getDisplayColumns(displayColumnNames) {
let displayColumns = []
if (typeof displayColumnNames == "undefined") {
return columns
}
for (const column of columns) {
if (displayColumnNames.includes(column.name.slice(0,-6).toLowerCase())) {
displayColumns.push(column)
}
}
return displayColumns
}
export function FlowTableHead({ sortColumn, sortDesc, setSort, displayColumnNames}) {
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
const displayColumns = getDisplayColumns(displayColumnNames)
return (
<tr>
{columns.map(Column => (
{displayColumns.map(Column => (
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
key={Column.name}
onClick={() => setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc)}>
@ -32,6 +48,7 @@ export default connect(
state => ({
sortDesc: state.flows.sort.desc,
sortColumn: state.flows.sort.column,
displayColumnNames: state.options["web_columns"] ? state.options["web_columns"].value : defaultColumnNames,
}),
{
setSort