mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-21 22:58:24 +00:00
Merge pull request #4779 from mhils/mitmweb-tcp
web: add support for viewing tcp flows
This commit is contained in:
parent
550e1a4ab3
commit
c0fd6cfc09
@ -96,31 +96,30 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
if flow.error:
|
if flow.error:
|
||||||
f["error"] = flow.error.get_state()
|
f["error"] = flow.error.get_state()
|
||||||
|
|
||||||
if isinstance(flow, http.HTTPFlow):
|
if isinstance(flow, HTTPFlow):
|
||||||
content_length: Optional[int]
|
content_length: Optional[int]
|
||||||
content_hash: Optional[str]
|
content_hash: Optional[str]
|
||||||
if flow.request:
|
|
||||||
if flow.request.raw_content is not None:
|
if flow.request.raw_content is not None:
|
||||||
content_length = len(flow.request.raw_content)
|
content_length = len(flow.request.raw_content)
|
||||||
content_hash = hashlib.sha256(flow.request.raw_content).hexdigest()
|
content_hash = hashlib.sha256(flow.request.raw_content).hexdigest()
|
||||||
else:
|
else:
|
||||||
content_length = None
|
content_length = None
|
||||||
content_hash = None
|
content_hash = None
|
||||||
f["request"] = {
|
f["request"] = {
|
||||||
"method": flow.request.method,
|
"method": flow.request.method,
|
||||||
"scheme": flow.request.scheme,
|
"scheme": flow.request.scheme,
|
||||||
"host": flow.request.host,
|
"host": flow.request.host,
|
||||||
"port": flow.request.port,
|
"port": flow.request.port,
|
||||||
"path": flow.request.path,
|
"path": flow.request.path,
|
||||||
"http_version": flow.request.http_version,
|
"http_version": flow.request.http_version,
|
||||||
"headers": tuple(flow.request.headers.items(True)),
|
"headers": tuple(flow.request.headers.items(True)),
|
||||||
"contentLength": content_length,
|
"contentLength": content_length,
|
||||||
"contentHash": content_hash,
|
"contentHash": content_hash,
|
||||||
"timestamp_start": flow.request.timestamp_start,
|
"timestamp_start": flow.request.timestamp_start,
|
||||||
"timestamp_end": flow.request.timestamp_end,
|
"timestamp_end": flow.request.timestamp_end,
|
||||||
"is_replay": flow.is_replay == "request", # TODO: remove, use flow.is_replay instead.
|
"pretty_host": flow.request.pretty_host,
|
||||||
"pretty_host": flow.request.pretty_host,
|
}
|
||||||
}
|
|
||||||
if flow.response:
|
if flow.response:
|
||||||
if flow.response.raw_content is not None:
|
if flow.response.raw_content is not None:
|
||||||
content_length = len(flow.response.raw_content)
|
content_length = len(flow.response.raw_content)
|
||||||
@ -137,7 +136,6 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
"contentHash": content_hash,
|
"contentHash": content_hash,
|
||||||
"timestamp_start": flow.response.timestamp_start,
|
"timestamp_start": flow.response.timestamp_start,
|
||||||
"timestamp_end": flow.response.timestamp_end,
|
"timestamp_end": flow.response.timestamp_end,
|
||||||
"is_replay": flow.is_replay == "response", # TODO: remove, use flow.is_replay instead.
|
|
||||||
}
|
}
|
||||||
if flow.response.data.trailers:
|
if flow.response.data.trailers:
|
||||||
f["response"]["trailers"] = tuple(flow.response.data.trailers.items(True))
|
f["response"]["trailers"] = tuple(flow.response.data.trailers.items(True))
|
||||||
@ -145,6 +143,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
if flow.websocket:
|
if flow.websocket:
|
||||||
f["websocket"] = {
|
f["websocket"] = {
|
||||||
"messages_meta": {
|
"messages_meta": {
|
||||||
|
"contentLength": sum(len(x.content) for x in flow.websocket.messages),
|
||||||
"count": len(flow.websocket.messages),
|
"count": len(flow.websocket.messages),
|
||||||
"timestamp_last": flow.websocket.messages[-1].timestamp if flow.websocket.messages else None,
|
"timestamp_last": flow.websocket.messages[-1].timestamp if flow.websocket.messages else None,
|
||||||
},
|
},
|
||||||
@ -153,6 +152,12 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
|||||||
"close_reason": flow.websocket.close_reason,
|
"close_reason": flow.websocket.close_reason,
|
||||||
"timestamp_end": flow.websocket.timestamp_end,
|
"timestamp_end": flow.websocket.timestamp_end,
|
||||||
}
|
}
|
||||||
|
elif isinstance(flow, TCPFlow):
|
||||||
|
f["messages_meta"] = {
|
||||||
|
"contentLength": sum(len(x.content) for x in flow.messages),
|
||||||
|
"count": len(flow.messages),
|
||||||
|
"timestamp_last": flow.messages[-1].timestamp if flow.messages else None,
|
||||||
|
}
|
||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
2
mitmproxy/tools/web/static/app.css
vendored
2
mitmproxy/tools/web/static/app.css
vendored
File diff suppressed because one or more lines are too long
86
mitmproxy/tools/web/static/app.js
vendored
86
mitmproxy/tools/web/static/app.js
vendored
File diff suppressed because one or more lines are too long
BIN
mitmproxy/tools/web/static/images/resourceTcpIcon.png
vendored
Normal file
BIN
mitmproxy/tools/web/static/images/resourceTcpIcon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@ -1,9 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import json as _json
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import textwrap
|
||||||
import typing
|
import typing
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -33,39 +32,43 @@ def no_tornado_logging():
|
|||||||
|
|
||||||
|
|
||||||
def get_json(resp: httpclient.HTTPResponse):
|
def get_json(resp: httpclient.HTTPResponse):
|
||||||
return _json.loads(resp.body.decode())
|
return json.loads(resp.body.decode())
|
||||||
|
|
||||||
|
|
||||||
def test_generate_tflow_js(tdata):
|
def test_generate_tflow_js(tdata):
|
||||||
tf = tflow.tflow(resp=True, err=True, ws=True)
|
tf_http = tflow.tflow(resp=True, err=True, ws=True)
|
||||||
tf.server_conn.certificate_list = [
|
tf_http.id = "d91165be-ca1f-4612-88a9-c0f8696f3e29"
|
||||||
|
tf_http.client_conn.id = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"
|
||||||
|
tf_http.server_conn.id = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8"
|
||||||
|
tf_http.server_conn.certificate_list = [
|
||||||
certs.Cert.from_pem(
|
certs.Cert.from_pem(
|
||||||
Path(tdata.path("mitmproxy/net/data/verificationcerts/self-signed.pem")).read_bytes()
|
Path(tdata.path("mitmproxy/net/data/verificationcerts/self-signed.pem")).read_bytes()
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
tf.request.trailers = Headers(trailer="qvalue")
|
tf_http.request.trailers = Headers(trailer="qvalue")
|
||||||
tf.response.trailers = Headers(trailer="qvalue")
|
tf_http.response.trailers = Headers(trailer="qvalue")
|
||||||
|
|
||||||
_tflow = app.flow_to_json(tf)
|
tf_tcp = tflow.ttcpflow(err=True)
|
||||||
# Set some value as constant, so that _tflow.js would not change every time.
|
tf_tcp.id = "2ea7012b-21b5-4f8f-98cd-d49819954001"
|
||||||
_tflow['id'] = "d91165be-ca1f-4612-88a9-c0f8696f3e29"
|
tf_tcp.client_conn.id = "8be32b99-a0b3-446e-93bc-b29982fe1322"
|
||||||
_tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"
|
tf_tcp.server_conn.id = "e33bb2cd-c07e-4214-9a8e-3a8f85f25200"
|
||||||
_tflow['server_conn']['id'] = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8"
|
|
||||||
tflow_json = _json.dumps(_tflow, indent=4, sort_keys=True)
|
|
||||||
|
|
||||||
tflow_json = re.sub(
|
|
||||||
r'( {8}"(address|is_replay|alpn_proto_negotiated)":)',
|
|
||||||
r" //@ts-ignore\n\1",
|
|
||||||
tflow_json
|
|
||||||
).replace(": null", ": undefined")
|
|
||||||
|
|
||||||
|
# language=TypeScript
|
||||||
content = (
|
content = (
|
||||||
"/** Auto-generated by test_app.py:test_generate_tflow_js */\n"
|
"/** Auto-generated by test_app.py:test_generate_tflow_js */\n"
|
||||||
"import {HTTPFlow} from '../../flow';\n"
|
"import {HTTPFlow, TCPFlow} from '../../flow';\n"
|
||||||
"export default function(): Required<HTTPFlow> {\n"
|
"export function THTTPFlow(): Required<HTTPFlow> {\n"
|
||||||
f" return {tflow_json}\n"
|
" return %s\n"
|
||||||
"}"
|
"}\n"
|
||||||
|
"export function TTCPFlow(): Required<TCPFlow> {\n"
|
||||||
|
" return %s\n"
|
||||||
|
"}" % (
|
||||||
|
textwrap.indent(json.dumps(app.flow_to_json(tf_http), indent=4, sort_keys=True), " "),
|
||||||
|
textwrap.indent(json.dumps(app.flow_to_json(tf_tcp), indent=4, sort_keys=True), " "),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
content = content.replace(": null", ": undefined")
|
||||||
|
|
||||||
(Path(__file__).parent / "../../../../web/src/js/__tests__/ducks/_tflow.ts").write_bytes(
|
(Path(__file__).parent / "../../../../web/src/js/__tests__/ducks/_tflow.ts").write_bytes(
|
||||||
content.encode()
|
content.encode()
|
||||||
)
|
)
|
||||||
@ -145,7 +148,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
return self.fetch(
|
return self.fetch(
|
||||||
url,
|
url,
|
||||||
method="PUT",
|
method="PUT",
|
||||||
body=_json.dumps(data),
|
body=json.dumps(data),
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -413,7 +416,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
|||||||
self.master.options.anticomp = True
|
self.master.options.anticomp = True
|
||||||
|
|
||||||
r1 = yield ws_client.read_message()
|
r1 = yield ws_client.read_message()
|
||||||
response = _json.loads(r1)
|
response = json.loads(r1)
|
||||||
assert response == {
|
assert response == {
|
||||||
"resource": "options",
|
"resource": "options",
|
||||||
"cmd": "update",
|
"cmd": "update",
|
||||||
|
@ -64,7 +64,7 @@ function scripts_prod() {
|
|||||||
return esbuild(false);
|
return esbuild(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const copy_src = ["src/images/**", "src/fonts/fontawesome-webfont.*"];
|
const copy_src = ["src/images/**", "src/fonts/fontawesome-webfont.*", "!**/*.psd"];
|
||||||
|
|
||||||
function copy() {
|
function copy() {
|
||||||
return gulp.src(copy_src, {base: "src/"})
|
return gulp.src(copy_src, {base: "src/"})
|
||||||
|
@ -184,16 +184,12 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:hover, &.open {
|
&:hover {
|
||||||
background-color: rgba(0, 0, 0, 5%);
|
background-color: rgba(0, 0, 0, 5%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-quickactions .fa-ellipsis-h {
|
|
||||||
transform: translate(0, 3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-quickactions .fa-play {
|
.col-quickactions .fa-play {
|
||||||
transform: translate(1px, 2px);
|
transform: translate(1px, 2px);
|
||||||
}
|
}
|
||||||
|
@ -48,3 +48,7 @@
|
|||||||
.resource-icon-websocket {
|
.resource-icon-websocket {
|
||||||
background-image: url(images/resourceWebSocketIcon.png);
|
background-image: url(images/resourceWebSocketIcon.png);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resource-icon-tcp {
|
||||||
|
background-image: url(images/resourceTcpIcon.png);
|
||||||
|
}
|
||||||
|
BIN
web/src/images/resourceIcon.psd
Normal file
BIN
web/src/images/resourceIcon.psd
Normal file
Binary file not shown.
BIN
web/src/images/resourceTcpIcon.png
Normal file
BIN
web/src/images/resourceTcpIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import renderer from 'react-test-renderer'
|
import renderer from 'react-test-renderer'
|
||||||
import FlowColumns from '../../../components/FlowTable/FlowColumns'
|
import FlowColumns from '../../../components/FlowTable/FlowColumns'
|
||||||
import {TFlow} from '../../ducks/tutils'
|
import {TFlow, TTCPFlow} from '../../ducks/tutils'
|
||||||
import {render} from "../../test-utils";
|
import {render} from "../../test-utils";
|
||||||
|
|
||||||
test("should render columns", async () => {
|
test("should render columns", async () => {
|
||||||
@ -19,9 +19,14 @@ test("should render columns", async () => {
|
|||||||
|
|
||||||
describe('Flowcolumns Components', () => {
|
describe('Flowcolumns Components', () => {
|
||||||
it('should render IconColumn', () => {
|
it('should render IconColumn', () => {
|
||||||
let tflow = {...TFlow(), websocket: undefined},
|
let tcpflow = TTCPFlow(),
|
||||||
iconColumn = renderer.create(<FlowColumns.icon flow={tflow}/>),
|
iconColumn = renderer.create(<FlowColumns.icon flow={tcpflow}/>),
|
||||||
tree = iconColumn.toJSON()
|
tree = iconColumn.toJSON()
|
||||||
|
expect(tree).toMatchSnapshot()
|
||||||
|
|
||||||
|
let tflow = {...TFlow(), websocket: undefined};
|
||||||
|
iconColumn = renderer.create(<FlowColumns.icon flow={tflow}/>)
|
||||||
|
tree = iconColumn.toJSON()
|
||||||
// plain
|
// plain
|
||||||
expect(tree).toMatchSnapshot()
|
expect(tree).toMatchSnapshot()
|
||||||
// not modified
|
// not modified
|
||||||
|
@ -5,7 +5,7 @@ exports[`Flowcolumns Components should render IconColumn 1`] = `
|
|||||||
className="col-icon"
|
className="col-icon"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="resource-icon resource-icon-plain"
|
className="resource-icon resource-icon-tcp"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@ -15,7 +15,7 @@ exports[`Flowcolumns Components should render IconColumn 2`] = `
|
|||||||
className="col-icon"
|
className="col-icon"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="resource-icon resource-icon-not-modified"
|
className="resource-icon resource-icon-plain"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@ -25,7 +25,7 @@ exports[`Flowcolumns Components should render IconColumn 3`] = `
|
|||||||
className="col-icon"
|
className="col-icon"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="resource-icon resource-icon-redirect"
|
className="resource-icon resource-icon-not-modified"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@ -35,7 +35,7 @@ exports[`Flowcolumns Components should render IconColumn 4`] = `
|
|||||||
className="col-icon"
|
className="col-icon"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="resource-icon resource-icon-image"
|
className="resource-icon resource-icon-redirect"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@ -45,7 +45,7 @@ exports[`Flowcolumns Components should render IconColumn 5`] = `
|
|||||||
className="col-icon"
|
className="col-icon"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="resource-icon resource-icon-js"
|
className="resource-icon resource-icon-image"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@ -55,7 +55,7 @@ exports[`Flowcolumns Components should render IconColumn 6`] = `
|
|||||||
className="col-icon"
|
className="col-icon"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="resource-icon resource-icon-css"
|
className="resource-icon resource-icon-js"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@ -65,7 +65,7 @@ exports[`Flowcolumns Components should render IconColumn 7`] = `
|
|||||||
className="col-icon"
|
className="col-icon"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="resource-icon resource-icon-document"
|
className="resource-icon resource-icon-css"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@ -75,7 +75,7 @@ exports[`Flowcolumns Components should render IconColumn 8`] = `
|
|||||||
className="col-icon"
|
className="col-icon"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="resource-icon resource-icon-plain"
|
className="resource-icon resource-icon-document"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@ -90,6 +90,16 @@ exports[`Flowcolumns Components should render IconColumn 9`] = `
|
|||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Flowcolumns Components should render IconColumn 10`] = `
|
||||||
|
<td
|
||||||
|
className="col-icon"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="resource-icon resource-icon-websocket"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Flowcolumns Components should render TimeColumn 1`] = `
|
exports[`Flowcolumns Components should render TimeColumn 1`] = `
|
||||||
<td
|
<td
|
||||||
className="col-time"
|
className="col-time"
|
||||||
@ -205,24 +215,7 @@ exports[`should render columns: quickactions 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="col-quickactions"
|
class="col-quickactions"
|
||||||
>
|
>
|
||||||
<div>
|
<div />
|
||||||
<a
|
|
||||||
class="quickaction"
|
|
||||||
href="#"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="fa fa-fw fa-repeat text-primary"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="quickaction"
|
|
||||||
href="#"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="fa fa-fw fa-ellipsis-h text-muted"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -238,7 +231,7 @@ exports[`should render columns: size 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="col-size"
|
class="col-size"
|
||||||
>
|
>
|
||||||
14b
|
43b
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -45,7 +45,7 @@ exports[`FlowRow 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="col-size"
|
class="col-size"
|
||||||
>
|
>
|
||||||
14b
|
43b
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="col-time"
|
class="col-time"
|
||||||
@ -64,14 +64,6 @@ exports[`FlowRow 1`] = `
|
|||||||
class="fa fa-fw fa-play text-success"
|
class="fa fa-fw fa-play text-success"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
class="quickaction"
|
|
||||||
href="#"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="fa fa-fw fa-ellipsis-h text-muted"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {render, screen, waitFor} from "../test-utils";
|
import {render, screen} from "../test-utils";
|
||||||
import FlowView from "../../components/FlowView";
|
import FlowView from "../../components/FlowView";
|
||||||
|
import * as flowActions from "../../ducks/flows"
|
||||||
import fetchMock, {enableFetchMocks} from "jest-fetch-mock";
|
import fetchMock, {enableFetchMocks} from "jest-fetch-mock";
|
||||||
import {fireEvent} from "@testing-library/react";
|
import {fireEvent} from "@testing-library/react";
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ enableFetchMocks();
|
|||||||
test("FlowView", async () => {
|
test("FlowView", async () => {
|
||||||
fetchMock.mockReject(new Error("backend missing"));
|
fetchMock.mockReject(new Error("backend missing"));
|
||||||
|
|
||||||
const {asFragment} = render(<FlowView/>);
|
const {asFragment, store} = render(<FlowView/>);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Response"));
|
fireEvent.click(screen.getByText("Response"));
|
||||||
@ -26,4 +27,11 @@ test("FlowView", async () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByText("Error"));
|
fireEvent.click(screen.getByText("Error"));
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
store.dispatch(flowActions.select(store.getState().flows.list[2].id));
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("TCP Messages"));
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Error"));
|
||||||
});
|
});
|
||||||
|
@ -49,6 +49,20 @@ exports[`FlowMenu 1`] = `
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
<a
|
||||||
|
class=""
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-default"
|
||||||
|
title="mark flow"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-paint-brush text-success"
|
||||||
|
/>
|
||||||
|
Mark▾
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="menu-legend"
|
class="menu-legend"
|
||||||
|
@ -26,7 +26,7 @@ exports[`MainMenu 1`] = `
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
type="text"
|
type="text"
|
||||||
value="~u /second"
|
value="~u /second | ~tcp"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -898,6 +898,32 @@ exports[`FlowView 5`] = `
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Server conn. closed:
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
1999-12-31 23:00:05.000
|
||||||
|
<span
|
||||||
|
class="text-muted"
|
||||||
|
>
|
||||||
|
(5s)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Client conn. closed:
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
1999-12-31 23:00:06.000
|
||||||
|
<span
|
||||||
|
class="text-muted"
|
||||||
|
>
|
||||||
|
(6s)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
@ -967,3 +993,76 @@ exports[`FlowView 6`] = `
|
|||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`FlowView 7`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="flow-detail"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
class="nav-tabs nav-tabs-sm"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="active"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
TCP Messages
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class=""
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
Error
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class=""
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
Connection
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class=""
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
Timing
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<section
|
||||||
|
class="tcp"
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
TCP Data
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
class="contentview"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="controls"
|
||||||
|
>
|
||||||
|
<h5>
|
||||||
|
2 Messages
|
||||||
|
</h5>
|
||||||
|
<a
|
||||||
|
class="btn btn-default btn-xs"
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
class="fa fa-fw fa-files-o"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<b>
|
||||||
|
View:
|
||||||
|
</b>
|
||||||
|
auto
|
||||||
|
<span
|
||||||
|
class="caret"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
@ -84,6 +84,20 @@ exports[`Header 1`] = `
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
<a
|
||||||
|
class=""
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-default"
|
||||||
|
title="mark flow"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-paint-brush text-success"
|
||||||
|
/>
|
||||||
|
Mark▾
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="menu-legend"
|
class="menu-legend"
|
||||||
|
@ -1,164 +1,224 @@
|
|||||||
/** Auto-generated by test_app.py:test_generate_tflow_js */
|
/** Auto-generated by test_app.py:test_generate_tflow_js */
|
||||||
import {HTTPFlow} from '../../flow';
|
import {HTTPFlow, TCPFlow} from '../../flow';
|
||||||
export default function(): Required<HTTPFlow> {
|
export function THTTPFlow(): Required<HTTPFlow> {
|
||||||
return {
|
return {
|
||||||
"client_conn": {
|
"client_conn": {
|
||||||
"alpn": "http/1.1",
|
"alpn": "http/1.1",
|
||||||
"cert": undefined,
|
"cert": undefined,
|
||||||
"cipher": "cipher",
|
"cipher": "cipher",
|
||||||
"id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
|
"id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
|
||||||
"peername": [
|
"peername": [
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
22
|
22
|
||||||
],
|
|
||||||
"sni": "address",
|
|
||||||
"sockname": [
|
|
||||||
"",
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"timestamp_end": 946681206,
|
|
||||||
"timestamp_start": 946681200,
|
|
||||||
"timestamp_tls_setup": 946681201,
|
|
||||||
"tls_established": true,
|
|
||||||
"tls_version": "TLSv1.2"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"msg": "error",
|
|
||||||
"timestamp": 946681207.0
|
|
||||||
},
|
|
||||||
"id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
|
|
||||||
"intercepted": false,
|
|
||||||
"is_replay": undefined,
|
|
||||||
"marked": "",
|
|
||||||
"modified": false,
|
|
||||||
"request": {
|
|
||||||
"contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
|
|
||||||
"contentLength": 7,
|
|
||||||
"headers": [
|
|
||||||
[
|
|
||||||
"header",
|
|
||||||
"qvalue"
|
|
||||||
],
|
],
|
||||||
[
|
"sni": "address",
|
||||||
"content-length",
|
"sockname": [
|
||||||
"7"
|
"",
|
||||||
]
|
0
|
||||||
],
|
|
||||||
"host": "address",
|
|
||||||
"http_version": "HTTP/1.1",
|
|
||||||
//@ts-ignore
|
|
||||||
"is_replay": false,
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/path",
|
|
||||||
"port": 22,
|
|
||||||
"pretty_host": "address",
|
|
||||||
"scheme": "http",
|
|
||||||
"timestamp_end": 946681201,
|
|
||||||
"timestamp_start": 946681200
|
|
||||||
},
|
|
||||||
"response": {
|
|
||||||
"contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d",
|
|
||||||
"contentLength": 7,
|
|
||||||
"headers": [
|
|
||||||
[
|
|
||||||
"header-response",
|
|
||||||
"svalue"
|
|
||||||
],
|
],
|
||||||
[
|
"timestamp_end": 946681206,
|
||||||
"content-length",
|
"timestamp_start": 946681200,
|
||||||
"7"
|
"timestamp_tls_setup": 946681201,
|
||||||
]
|
"tls_established": true,
|
||||||
],
|
"tls_version": "TLSv1.2"
|
||||||
"http_version": "HTTP/1.1",
|
},
|
||||||
//@ts-ignore
|
"error": {
|
||||||
"is_replay": false,
|
"msg": "error",
|
||||||
"reason": "OK",
|
"timestamp": 946681207.0
|
||||||
"status_code": 200,
|
},
|
||||||
"timestamp_end": 946681203,
|
"id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
|
||||||
"timestamp_start": 946681202,
|
"intercepted": false,
|
||||||
"trailers": [
|
"is_replay": undefined,
|
||||||
[
|
"marked": "",
|
||||||
"trailer",
|
"modified": false,
|
||||||
"qvalue"
|
"request": {
|
||||||
]
|
"contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
|
||||||
]
|
"contentLength": 7,
|
||||||
},
|
"headers": [
|
||||||
"server_conn": {
|
|
||||||
//@ts-ignore
|
|
||||||
"address": [
|
|
||||||
"address",
|
|
||||||
22
|
|
||||||
],
|
|
||||||
"alpn": undefined,
|
|
||||||
"cert": {
|
|
||||||
"altnames": [
|
|
||||||
"example.mitmproxy.org"
|
|
||||||
],
|
|
||||||
"issuer": [
|
|
||||||
[
|
[
|
||||||
"C",
|
"header",
|
||||||
"AU"
|
"qvalue"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"ST",
|
"content-length",
|
||||||
"Some-State"
|
"7"
|
||||||
],
|
|
||||||
[
|
|
||||||
"O",
|
|
||||||
"Internet Widgits Pty Ltd"
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"keyinfo": [
|
"host": "address",
|
||||||
"RSA",
|
"http_version": "HTTP/1.1",
|
||||||
2048
|
"method": "GET",
|
||||||
|
"path": "/path",
|
||||||
|
"port": 22,
|
||||||
|
"pretty_host": "address",
|
||||||
|
"scheme": "http",
|
||||||
|
"timestamp_end": 946681201,
|
||||||
|
"timestamp_start": 946681200
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d",
|
||||||
|
"contentLength": 7,
|
||||||
|
"headers": [
|
||||||
|
[
|
||||||
|
"header-response",
|
||||||
|
"svalue"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"content-length",
|
||||||
|
"7"
|
||||||
|
]
|
||||||
],
|
],
|
||||||
"notafter": 2235103407,
|
"http_version": "HTTP/1.1",
|
||||||
"notbefore": 1604383407,
|
"reason": "OK",
|
||||||
"serial": "247170098335718583458667965517443538258472437317",
|
"status_code": 200,
|
||||||
"sha256": "e5f62a1175031b6feb959bc8e6dd0f8e2546dbbf7c32da39534309d8aa92967c",
|
"timestamp_end": 946681203,
|
||||||
"subject": [
|
"timestamp_start": 946681202,
|
||||||
|
"trailers": [
|
||||||
[
|
[
|
||||||
"C",
|
"trailer",
|
||||||
"AU"
|
"qvalue"
|
||||||
],
|
|
||||||
[
|
|
||||||
"ST",
|
|
||||||
"Some-State"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"O",
|
|
||||||
"Internet Widgits Pty Ltd"
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"cipher": undefined,
|
"server_conn": {
|
||||||
"id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
|
"address": [
|
||||||
"peername": [
|
"address",
|
||||||
"192.168.0.1",
|
22
|
||||||
22
|
],
|
||||||
],
|
"alpn": undefined,
|
||||||
"sni": "address",
|
"cert": {
|
||||||
"sockname": [
|
"altnames": [
|
||||||
"address",
|
"example.mitmproxy.org"
|
||||||
22
|
],
|
||||||
],
|
"issuer": [
|
||||||
"timestamp_end": 946681205,
|
[
|
||||||
"timestamp_start": 946681202,
|
"C",
|
||||||
"timestamp_tcp_setup": 946681203,
|
"AU"
|
||||||
"timestamp_tls_setup": 946681204,
|
],
|
||||||
"tls_established": true,
|
[
|
||||||
"tls_version": "TLSv1.2"
|
"ST",
|
||||||
},
|
"Some-State"
|
||||||
"type": "http",
|
],
|
||||||
"websocket": {
|
[
|
||||||
"close_code": 1000,
|
"O",
|
||||||
"close_reason": "Close Reason",
|
"Internet Widgits Pty Ltd"
|
||||||
"closed_by_client": false,
|
]
|
||||||
"messages_meta": {
|
],
|
||||||
"count": 3,
|
"keyinfo": [
|
||||||
"timestamp_last": 946681205
|
"RSA",
|
||||||
|
2048
|
||||||
|
],
|
||||||
|
"notafter": 2235103407,
|
||||||
|
"notbefore": 1604383407,
|
||||||
|
"serial": "247170098335718583458667965517443538258472437317",
|
||||||
|
"sha256": "e5f62a1175031b6feb959bc8e6dd0f8e2546dbbf7c32da39534309d8aa92967c",
|
||||||
|
"subject": [
|
||||||
|
[
|
||||||
|
"C",
|
||||||
|
"AU"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ST",
|
||||||
|
"Some-State"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"O",
|
||||||
|
"Internet Widgits Pty Ltd"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cipher": undefined,
|
||||||
|
"id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
|
||||||
|
"peername": [
|
||||||
|
"192.168.0.1",
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"sni": "address",
|
||||||
|
"sockname": [
|
||||||
|
"address",
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"timestamp_end": 946681205,
|
||||||
|
"timestamp_start": 946681202,
|
||||||
|
"timestamp_tcp_setup": 946681203,
|
||||||
|
"timestamp_tls_setup": 946681204,
|
||||||
|
"tls_established": true,
|
||||||
|
"tls_version": "TLSv1.2"
|
||||||
},
|
},
|
||||||
"timestamp_end": 946681205
|
"type": "http",
|
||||||
|
"websocket": {
|
||||||
|
"close_code": 1000,
|
||||||
|
"close_reason": "Close Reason",
|
||||||
|
"closed_by_client": false,
|
||||||
|
"messages_meta": {
|
||||||
|
"contentLength": 29,
|
||||||
|
"count": 3,
|
||||||
|
"timestamp_last": 946681205
|
||||||
|
},
|
||||||
|
"timestamp_end": 946681205
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function TTCPFlow(): Required<TCPFlow> {
|
||||||
|
return {
|
||||||
|
"client_conn": {
|
||||||
|
"alpn": "http/1.1",
|
||||||
|
"cert": undefined,
|
||||||
|
"cipher": "cipher",
|
||||||
|
"id": "8be32b99-a0b3-446e-93bc-b29982fe1322",
|
||||||
|
"peername": [
|
||||||
|
"127.0.0.1",
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"sni": "address",
|
||||||
|
"sockname": [
|
||||||
|
"",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"timestamp_end": 946681206,
|
||||||
|
"timestamp_start": 946681200,
|
||||||
|
"timestamp_tls_setup": 946681201,
|
||||||
|
"tls_established": true,
|
||||||
|
"tls_version": "TLSv1.2"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"msg": "error",
|
||||||
|
"timestamp": 946681207.0
|
||||||
|
},
|
||||||
|
"id": "2ea7012b-21b5-4f8f-98cd-d49819954001",
|
||||||
|
"intercepted": false,
|
||||||
|
"is_replay": undefined,
|
||||||
|
"marked": "",
|
||||||
|
"messages_meta": {
|
||||||
|
"contentLength": 12,
|
||||||
|
"count": 2,
|
||||||
|
"timestamp_last": 1629806221.58518
|
||||||
|
},
|
||||||
|
"modified": false,
|
||||||
|
"server_conn": {
|
||||||
|
"address": [
|
||||||
|
"address",
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"alpn": undefined,
|
||||||
|
"cert": undefined,
|
||||||
|
"cipher": undefined,
|
||||||
|
"id": "e33bb2cd-c07e-4214-9a8e-3a8f85f25200",
|
||||||
|
"peername": [
|
||||||
|
"192.168.0.1",
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"sni": "address",
|
||||||
|
"sockname": [
|
||||||
|
"address",
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"timestamp_end": 946681205,
|
||||||
|
"timestamp_start": 946681202,
|
||||||
|
"timestamp_tcp_setup": 946681203,
|
||||||
|
"timestamp_tls_setup": 946681204,
|
||||||
|
"tls_established": true,
|
||||||
|
"tls_version": "TLSv1.2"
|
||||||
|
},
|
||||||
|
"type": "tcp"
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,18 +1,19 @@
|
|||||||
import thunk from 'redux-thunk'
|
import thunk from 'redux-thunk'
|
||||||
import configureStore, {MockStoreCreator, MockStoreEnhanced} from 'redux-mock-store'
|
import configureStore, {MockStoreCreator, MockStoreEnhanced} from 'redux-mock-store'
|
||||||
import {ConnectionState} from '../../ducks/connection'
|
import {ConnectionState} from '../../ducks/connection'
|
||||||
import TFlow from './_tflow'
|
import {THTTPFlow, TTCPFlow} from './_tflow'
|
||||||
import {AppDispatch, RootState} from "../../ducks";
|
import {AppDispatch, RootState} from "../../ducks";
|
||||||
import {HTTPFlow} from "../../flow";
|
import {HTTPFlow, TCPFlow} from "../../flow";
|
||||||
import {defaultState as defaultConf} from "../../ducks/conf"
|
import {defaultState as defaultConf} from "../../ducks/conf"
|
||||||
import {defaultState as defaultOptions} from "../../ducks/options"
|
import {defaultState as defaultOptions} from "../../ducks/options"
|
||||||
|
|
||||||
const mockStoreCreator: MockStoreCreator<RootState, AppDispatch> = configureStore([thunk])
|
const mockStoreCreator: MockStoreCreator<RootState, AppDispatch> = configureStore([thunk])
|
||||||
|
|
||||||
export {TFlow}
|
export {THTTPFlow as TFlow, TTCPFlow}
|
||||||
|
|
||||||
const tflow1: HTTPFlow = TFlow();
|
const tflow1: HTTPFlow = THTTPFlow();
|
||||||
const tflow2: HTTPFlow = TFlow();
|
const tflow2: HTTPFlow = THTTPFlow();
|
||||||
|
const tflow3: TCPFlow = TTCPFlow();
|
||||||
tflow1.modified = true
|
tflow1.modified = true
|
||||||
tflow1.intercepted = true
|
tflow1.intercepted = true
|
||||||
tflow2.id = "flow2";
|
tflow2.id = "flow2";
|
||||||
@ -71,17 +72,17 @@ export const testState: RootState = {
|
|||||||
options: defaultOptions,
|
options: defaultOptions,
|
||||||
flows: {
|
flows: {
|
||||||
selected: [tflow2.id],
|
selected: [tflow2.id],
|
||||||
byId: {[tflow1.id]: tflow1, [tflow2.id]: tflow2},
|
byId: {[tflow1.id]: tflow1, [tflow2.id]: tflow2, [tflow3.id]: tflow3},
|
||||||
filter: '~u /second',
|
filter: '~u /second | ~tcp',
|
||||||
highlight: '~u /path',
|
highlight: '~u /path',
|
||||||
sort: {
|
sort: {
|
||||||
desc: true,
|
desc: true,
|
||||||
column: "path"
|
column: "path"
|
||||||
},
|
},
|
||||||
view: [tflow2],
|
view: [tflow2, tflow3],
|
||||||
list: [tflow1, tflow2],
|
list: [tflow1, tflow2, tflow3],
|
||||||
listIndex: {[tflow1.id]: 0, [tflow2.id]: 1},
|
listIndex: {[tflow1.id]: 0, [tflow2.id]: 1, [tflow3.id]: 2},
|
||||||
viewIndex: {[tflow2.id]: 0},
|
viewIndex: {[tflow2.id]: 0, [tflow3.id]: 1},
|
||||||
},
|
},
|
||||||
connection: {
|
connection: {
|
||||||
state: ConnectionState.ESTABLISHED
|
state: ConnectionState.ESTABLISHED
|
||||||
|
@ -12,7 +12,7 @@ describe('onKeyDown', () => {
|
|||||||
for (let i = 1; i <= 12; i++) {
|
for (let i = 1; i <= 12; i++) {
|
||||||
flows = reduceFlows(flows, {
|
flows = reduceFlows(flows, {
|
||||||
type: flowsActions.ADD,
|
type: flowsActions.ADD,
|
||||||
data: {id: i + "", request: true, response: true},
|
data: {id: i + "", request: true, response: true, type: "http"},
|
||||||
cmd: 'add'
|
cmd: 'add'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import React, {useState} from 'react'
|
import React, {ReactElement, useState} from 'react'
|
||||||
import {useDispatch} from 'react-redux'
|
import {useDispatch} from 'react-redux'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {endTime, getTotalSize, RequestUtils, ResponseUtils, startTime} from '../../flow/utils'
|
import {canReplay, endTime, getTotalSize, RequestUtils, ResponseUtils, startTime} from '../../flow/utils'
|
||||||
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils'
|
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils'
|
||||||
import * as flowActions from "../../ducks/flows";
|
import * as flowActions from "../../ducks/flows";
|
||||||
import {addInterceptFilter} from "../../ducks/options"
|
|
||||||
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
|
|
||||||
import {Flow} from "../../flow";
|
import {Flow} from "../../flow";
|
||||||
import {copy} from "../../flow/export";
|
|
||||||
|
|
||||||
|
|
||||||
type FlowColumnProps = {
|
type FlowColumnProps = {
|
||||||
@ -40,12 +37,15 @@ icon.headerName = ''
|
|||||||
icon.sortKey = flow => getIcon(flow)
|
icon.sortKey = flow => getIcon(flow)
|
||||||
|
|
||||||
const getIcon = (flow: Flow): string => {
|
const getIcon = (flow: Flow): string => {
|
||||||
if (flow.type !== "http" || !flow.response) {
|
if (flow.type === "tcp") {
|
||||||
return 'resource-icon-plain'
|
return "resource-icon-tcp"
|
||||||
}
|
}
|
||||||
if (flow.websocket) {
|
if (flow.websocket) {
|
||||||
return 'resource-icon-websocket'
|
return 'resource-icon-websocket'
|
||||||
}
|
}
|
||||||
|
if (!flow.response) {
|
||||||
|
return 'resource-icon-plain'
|
||||||
|
}
|
||||||
|
|
||||||
var contentType = ResponseUtils.getContentType(flow.response) || ''
|
var contentType = ResponseUtils.getContentType(flow.response) || ''
|
||||||
|
|
||||||
@ -71,6 +71,15 @@ const getIcon = (flow: Flow): string => {
|
|||||||
return 'resource-icon-plain'
|
return 'resource-icon-plain'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mainPath = (flow: Flow): string => {
|
||||||
|
switch (flow.type) {
|
||||||
|
case "http":
|
||||||
|
return RequestUtils.pretty_url(flow.request)
|
||||||
|
case "tcp":
|
||||||
|
return `${flow.client_conn.peername.join(':')} ↔ ${flow.server_conn?.address?.join(':')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const path: FlowColumn = ({flow}) => {
|
export const path: FlowColumn = ({flow}) => {
|
||||||
let err;
|
let err;
|
||||||
if (flow.error) {
|
if (flow.error) {
|
||||||
@ -90,20 +99,20 @@ export const path: FlowColumn = ({flow}) => {
|
|||||||
)}
|
)}
|
||||||
{err}
|
{err}
|
||||||
<span className="marker pull-right">{flow.marked}</span>
|
<span className="marker pull-right">{flow.marked}</span>
|
||||||
{flow.type === "http" ? RequestUtils.pretty_url(flow.request) : null}
|
{mainPath(flow)}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
path.headerName = 'Path'
|
path.headerName = 'Path'
|
||||||
path.sortKey = flow => flow.type === "http" && RequestUtils.pretty_url(flow.request)
|
path.sortKey = flow => mainPath(flow)
|
||||||
|
|
||||||
export const method: FlowColumn = ({flow}) => {
|
export const method: FlowColumn = ({flow}) => {
|
||||||
return (
|
return (
|
||||||
<td className="col-method">{flow.type === "http" ? flow.request.method : flow.type.toLowerCase()}</td>
|
<td className="col-method">{flow.type === "http" ? flow.request.method : flow.type.toUpperCase()}</td>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
method.headerName = 'Method'
|
method.headerName = 'Method'
|
||||||
method.sortKey = flow => flow.type === "http" && flow.request.method
|
method.sortKey = flow => flow.type === "http" ? flow.request.method : flow.type.toUpperCase()
|
||||||
|
|
||||||
export const status: FlowColumn = ({flow}) => {
|
export const status: FlowColumn = ({flow}) => {
|
||||||
let color = 'darkred';
|
let color = 'darkred';
|
||||||
@ -186,76 +195,22 @@ export const quickactions: FlowColumn = ({flow}) => {
|
|||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
let [open, setOpen] = useState(false)
|
let [open, setOpen] = useState(false)
|
||||||
|
|
||||||
let resume_or_replay: React.ReactNode | null = null;
|
let resume_or_replay: ReactElement | null = null;
|
||||||
if (flow.intercepted) {
|
if (flow.intercepted) {
|
||||||
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
|
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
|
||||||
<i className="fa fa-fw fa-play text-success"/>
|
<i className="fa fa-fw fa-play text-success"/>
|
||||||
</a>;
|
</a>;
|
||||||
} else {
|
} else if (canReplay(flow)) {
|
||||||
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.replay(flow))}>
|
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.replay(flow))}>
|
||||||
<i className="fa fa-fw fa-repeat text-primary"/>
|
<i className="fa fa-fw fa-repeat text-primary"/>
|
||||||
</a>;
|
</a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flow.type !== "http")
|
|
||||||
return <td className="col-quickactions"/>
|
|
||||||
|
|
||||||
const filt = (x) => dispatch(addInterceptFilter(x));
|
|
||||||
const ct = flow.response && ResponseUtils.getContentType(flow.response);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className={classnames("col-quickactions", {hover: open})} onClick={() => 0}>
|
<td className={classnames("col-quickactions", {hover: open})} onClick={() => 0}>
|
||||||
<div>
|
<div>
|
||||||
{resume_or_replay}
|
{resume_or_replay}
|
||||||
<Dropdown text={<i className="fa fa-fw fa-ellipsis-h text-muted"/>} className="quickaction"
|
|
||||||
onOpen={setOpen}
|
|
||||||
options={{placement: "bottom-end"}}>
|
|
||||||
<SubMenu title="Copy...">
|
|
||||||
<MenuItem onClick={() => copy(flow, "raw_request")}>Copy raw request</MenuItem>
|
|
||||||
<MenuItem onClick={() => copy(flow, "raw_response")}>Copy raw response</MenuItem>
|
|
||||||
<MenuItem onClick={() => copy(flow, "raw")}>Copy raw request and response</MenuItem>
|
|
||||||
<MenuItem onClick={() => copy(flow, "curl")}>Copy as cURL</MenuItem>
|
|
||||||
<MenuItem onClick={() => copy(flow, "httpie")}>Copy as HTTPie</MenuItem>
|
|
||||||
</SubMenu>
|
|
||||||
<SubMenu title="Mark..." className="markers-menu">
|
|
||||||
<MenuItem onClick={() => dispatch(flowActions.update(flow, {marked: ""}))}>⚪ (no marker)</MenuItem>
|
|
||||||
{Object.entries(markers).map(([name, sym]) =>
|
|
||||||
<MenuItem
|
|
||||||
key={name}
|
|
||||||
onClick={() => dispatch(flowActions.update(flow, {marked: name}))}>
|
|
||||||
{sym} {name.replace(/[:_]/g, " ")}
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</SubMenu>
|
|
||||||
<SubMenu title="Intercept requests like this">
|
|
||||||
<MenuItem onClick={() => filt(`~q ${flow.request.host}`)}>
|
|
||||||
Requests to {flow.request.host}
|
|
||||||
</MenuItem>
|
|
||||||
{flow.request.path !== "/" &&
|
|
||||||
<MenuItem onClick={() => filt(`~q ${flow.request.host}${flow.request.path}`)}>
|
|
||||||
Requests to {flow.request.host + flow.request.path}
|
|
||||||
</MenuItem>}
|
|
||||||
{flow.request.method !== "GET" &&
|
|
||||||
<MenuItem onClick={() => filt(`~q ~m ${flow.request.method} ${flow.request.host}`)}>
|
|
||||||
{flow.request.method} requests to {flow.request.host}
|
|
||||||
</MenuItem>}
|
|
||||||
</SubMenu>
|
|
||||||
<SubMenu title="Intercept responses like this">
|
|
||||||
<MenuItem onClick={() => filt(`~s ${flow.request.host}`)}>
|
|
||||||
Responses from {flow.request.host}
|
|
||||||
</MenuItem>
|
|
||||||
{flow.request.path !== "/" &&
|
|
||||||
<MenuItem onClick={() => filt(`~s ${flow.request.host}${flow.request.path}`)}>
|
|
||||||
Responses from {flow.request.host + flow.request.path}
|
|
||||||
</MenuItem>}
|
|
||||||
{!!ct &&
|
|
||||||
<MenuItem onClick={() => filt(`~ts ${ct}`)}>
|
|
||||||
Responses with a {ct} content type.
|
|
||||||
</MenuItem>}
|
|
||||||
</SubMenu>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,9 @@ export default React.memo(function FlowTableHead() {
|
|||||||
{displayColumns.map(Column => (
|
{displayColumns.map(Column => (
|
||||||
<th className={classnames(`col-${Column.name}`, sortColumn === Column.name && sortType)}
|
<th className={classnames(`col-${Column.name}`, sortColumn === Column.name && sortType)}
|
||||||
key={Column.name}
|
key={Column.name}
|
||||||
onClick={() => dispatch(setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc))}>
|
onClick={() => dispatch(setSort(
|
||||||
|
Column.name === sortColumn && sortDesc ? undefined : Column.name,
|
||||||
|
Column.name !== sortColumn ? false : !sortDesc))}>
|
||||||
{Column.headerName}
|
{Column.headerName}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
|
@ -10,22 +10,35 @@ import {selectTab} from '../ducks/ui/flow'
|
|||||||
import {useAppDispatch, useAppSelector} from "../ducks";
|
import {useAppDispatch, useAppSelector} from "../ducks";
|
||||||
import {Flow} from "../flow";
|
import {Flow} from "../flow";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
import TcpMessages from "./FlowView/TcpMessages";
|
||||||
|
|
||||||
type TabProps = {
|
type TabProps = {
|
||||||
flow: Flow
|
flow: Flow
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allTabs: { [name: string]: FunctionComponent<TabProps> & {displayName: string} } = {
|
export const allTabs: { [name: string]: FunctionComponent<TabProps> & { displayName: string } } = {
|
||||||
request: Request,
|
request: Request,
|
||||||
response: Response,
|
response: Response,
|
||||||
error: Error,
|
error: Error,
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
timing: Timing,
|
timing: Timing,
|
||||||
websocket: WebSocket
|
websocket: WebSocket,
|
||||||
|
messages: TcpMessages,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tabsForFlow(flow: Flow): string[] {
|
export function tabsForFlow(flow: Flow): string[] {
|
||||||
const tabs = ['request', 'response', 'websocket', 'error'].filter(k => flow[k])
|
let tabs;
|
||||||
|
switch (flow.type) {
|
||||||
|
case "http":
|
||||||
|
tabs = ['request', 'response', 'websocket'].filter(k => flow[k])
|
||||||
|
break
|
||||||
|
case "tcp":
|
||||||
|
tabs = ["messages"]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flow.error)
|
||||||
|
tabs.push("error")
|
||||||
tabs.push("connection")
|
tabs.push("connection")
|
||||||
tabs.push("timing")
|
tabs.push("timing")
|
||||||
return tabs;
|
return tabs;
|
||||||
|
50
web/src/js/components/FlowView/Messages.tsx
Normal file
50
web/src/js/components/FlowView/Messages.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {Flow, MessagesMeta} from "../../flow";
|
||||||
|
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useCallback, useMemo, useState} from "react";
|
||||||
|
import {ContentViewData, SHOW_MAX_LINES, useContent} from "../contentviews/useContent";
|
||||||
|
import {MessageUtils} from "../../flow/utils";
|
||||||
|
import ViewSelector from "../contentviews/ViewSelector";
|
||||||
|
import {setContentViewFor} from "../../ducks/ui/flow";
|
||||||
|
import {formatTimeStamp} from "../../utils";
|
||||||
|
import LineRenderer from "../contentviews/LineRenderer";
|
||||||
|
|
||||||
|
type MessagesPropTypes = {
|
||||||
|
flow: Flow
|
||||||
|
messages_meta: MessagesMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Messages({flow, messages_meta}: MessagesPropTypes) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const contentView = useAppSelector(state => state.ui.flow.contentViewFor[flow.id + "messages"] || "Auto");
|
||||||
|
let [maxLines, setMaxLines] = useState<number>(SHOW_MAX_LINES);
|
||||||
|
const showMore = useCallback(() => setMaxLines(Math.max(1024, maxLines * 2)), [maxLines]);
|
||||||
|
const content = useContent(
|
||||||
|
MessageUtils.getContentURL(flow, "messages", contentView, maxLines + 1),
|
||||||
|
flow.id + messages_meta.count
|
||||||
|
);
|
||||||
|
const messages = useMemo<ContentViewData[] | undefined>(() => content && JSON.parse(content), [content]) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="contentview">
|
||||||
|
<div className="controls">
|
||||||
|
<h5>{messages_meta.count} Messages</h5>
|
||||||
|
<ViewSelector value={contentView}
|
||||||
|
onChange={cv => dispatch(setContentViewFor(flow.id + "messages", cv))}/>
|
||||||
|
</div>
|
||||||
|
{messages.map((d: ContentViewData, i) => {
|
||||||
|
const className = `fa fa-fw fa-arrow-${d.from_client ? "right text-primary" : "left text-danger"}`;
|
||||||
|
const renderer = <div key={i}>
|
||||||
|
<small>
|
||||||
|
<i className={className}/>
|
||||||
|
<span className="pull-right">{d.timestamp && formatTimeStamp(d.timestamp)}</span>
|
||||||
|
</small>
|
||||||
|
<LineRenderer lines={d.lines} maxLines={maxLines} showMore={showMore}/>
|
||||||
|
</div>;
|
||||||
|
maxLines -= d.lines.length;
|
||||||
|
return renderer;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
14
web/src/js/components/FlowView/TcpMessages.tsx
Normal file
14
web/src/js/components/FlowView/TcpMessages.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import {TCPFlow} from "../../flow";
|
||||||
|
import * as React from "react";
|
||||||
|
import Messages from "./Messages";
|
||||||
|
|
||||||
|
|
||||||
|
export default function TcpMessages({flow}: { flow: TCPFlow }) {
|
||||||
|
return (
|
||||||
|
<section className="tcp">
|
||||||
|
<h4>TCP Data</h4>
|
||||||
|
<Messages flow={flow} messages_meta={flow.messages_meta}/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TcpMessages.displayName = "TCP Messages"
|
@ -47,6 +47,10 @@ export default function Timing({flow}: { flow: Flow }) {
|
|||||||
title: "Server conn. TLS handshake",
|
title: "Server conn. TLS handshake",
|
||||||
t: flow.server_conn?.timestamp_tls_setup,
|
t: flow.server_conn?.timestamp_tls_setup,
|
||||||
deltaTo: ref
|
deltaTo: ref
|
||||||
|
}, {
|
||||||
|
title: "Server conn. closed",
|
||||||
|
t: flow.server_conn?.timestamp_end,
|
||||||
|
deltaTo: ref
|
||||||
}, {
|
}, {
|
||||||
title: "Client conn. established",
|
title: "Client conn. established",
|
||||||
t: flow.client_conn.timestamp_start,
|
t: flow.client_conn.timestamp_start,
|
||||||
@ -55,7 +59,11 @@ export default function Timing({flow}: { flow: Flow }) {
|
|||||||
title: "Client conn. TLS handshake",
|
title: "Client conn. TLS handshake",
|
||||||
t: flow.client_conn.timestamp_tls_setup,
|
t: flow.client_conn.timestamp_tls_setup,
|
||||||
deltaTo: ref
|
deltaTo: ref
|
||||||
}
|
}, {
|
||||||
|
title: "Client conn. closed",
|
||||||
|
t: flow.client_conn.timestamp_end,
|
||||||
|
deltaTo: ref
|
||||||
|
},
|
||||||
]
|
]
|
||||||
if (flow.type === "http") {
|
if (flow.type === "http") {
|
||||||
timestamps.push(...[
|
timestamps.push(...[
|
||||||
|
@ -1,49 +1,14 @@
|
|||||||
import {HTTPFlow, WebSocketData} from "../../flow";
|
import {HTTPFlow, WebSocketData} from "../../flow";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useCallback, useMemo, useState} from "react";
|
|
||||||
import {ContentViewData, SHOW_MAX_LINES, useContent} from "../contentviews/useContent";
|
|
||||||
import {MessageUtils} from "../../flow/utils";
|
|
||||||
import ViewSelector from "../contentviews/ViewSelector";
|
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
|
||||||
import {setContentViewFor} from "../../ducks/ui/flow";
|
|
||||||
import LineRenderer from "../contentviews/LineRenderer";
|
|
||||||
import {formatTimeStamp} from "../../utils";
|
import {formatTimeStamp} from "../../utils";
|
||||||
|
import Messages from "./Messages";
|
||||||
|
|
||||||
|
|
||||||
export default function WebSocket({flow}: { flow: HTTPFlow & { websocket: WebSocketData } }) {
|
export default function WebSocket({flow}: { flow: HTTPFlow & { websocket: WebSocketData } }) {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const contentView = useAppSelector(state => state.ui.flow.contentViewFor[flow.id + "ws"] || "Auto");
|
|
||||||
let [maxLines, setMaxLines] = useState<number>(SHOW_MAX_LINES);
|
|
||||||
const showMore = useCallback(() => setMaxLines(Math.max(1024, maxLines * 2)), [maxLines]);
|
|
||||||
const content = useContent(
|
|
||||||
MessageUtils.getContentURL(flow, "messages", contentView, maxLines + 1),
|
|
||||||
flow.id + flow.websocket.messages_meta.count
|
|
||||||
);
|
|
||||||
const messages = useMemo<ContentViewData[] | undefined>(() => content && JSON.parse(content), [content]) || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="websocket">
|
<section className="websocket">
|
||||||
<h4>WebSocket</h4>
|
<h4>WebSocket</h4>
|
||||||
<div className="contentview">
|
<Messages flow={flow} messages_meta={flow.websocket.messages_meta}/>
|
||||||
<div className="controls">
|
|
||||||
<h5>{flow.websocket.messages_meta.count} Messages</h5>
|
|
||||||
<ViewSelector value={contentView}
|
|
||||||
onChange={cv => dispatch(setContentViewFor(flow.id + "ws", cv))}/>
|
|
||||||
</div>
|
|
||||||
{messages.map((d: ContentViewData, i) => {
|
|
||||||
const className = `fa fa-fw fa-arrow-${d.from_client ? "right text-primary" : "left text-danger"}`;
|
|
||||||
const renderer = <div key={i}>
|
|
||||||
<small>
|
|
||||||
<i className={className}/>
|
|
||||||
<span className="pull-right">{d.timestamp && formatTimeStamp(d.timestamp)}</span>
|
|
||||||
</small>
|
|
||||||
<LineRenderer lines={d.lines} maxLines={maxLines} showMore={showMore}/>
|
|
||||||
</div>;
|
|
||||||
maxLines -= d.lines.length;
|
|
||||||
return renderer;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<CloseSummary websocket={flow.websocket}/>
|
<CloseSummary websocket={flow.websocket}/>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
@ -51,17 +16,17 @@ export default function WebSocket({flow}: { flow: HTTPFlow & { websocket: WebSoc
|
|||||||
WebSocket.displayName = "WebSocket"
|
WebSocket.displayName = "WebSocket"
|
||||||
|
|
||||||
|
|
||||||
function CloseSummary({websocket}: {websocket: WebSocketData}){
|
function CloseSummary({websocket}: { websocket: WebSocketData }) {
|
||||||
if(!websocket.timestamp_end)
|
if (!websocket.timestamp_end)
|
||||||
return null;
|
return null;
|
||||||
const reason = websocket.close_reason ? `(${websocket.close_reason})` : ""
|
const reason = websocket.close_reason ? `(${websocket.close_reason})` : ""
|
||||||
return <div>
|
return <div>
|
||||||
<i className="fa fa-fw fa-window-close text-muted"/>
|
<i className="fa fa-fw fa-window-close text-muted"/>
|
||||||
|
|
||||||
Closed by {websocket.closed_by_client ? "client": "server"} with code {websocket.close_code} {reason}.
|
Closed by {websocket.closed_by_client ? "client" : "server"} with code {websocket.close_code} {reason}.
|
||||||
|
|
||||||
<small className="pull-right">
|
<small className="pull-right">
|
||||||
{formatTimeStamp(websocket.timestamp_end)}
|
{formatTimeStamp(websocket.timestamp_end)}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Button from "../common/Button"
|
import Button from "../common/Button"
|
||||||
import {MessageUtils} from "../../flow/utils"
|
import {canReplay, MessageUtils} from "../../flow/utils"
|
||||||
import HideInStatic from "../common/HideInStatic";
|
import HideInStatic from "../common/HideInStatic";
|
||||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||||
|
import * as flowActions from "../../ducks/flows"
|
||||||
import {
|
import {
|
||||||
duplicate as duplicateFlow,
|
duplicate as duplicateFlow,
|
||||||
kill as killFlow,
|
kill as killFlow,
|
||||||
@ -30,7 +31,7 @@ export default function FlowMenu(): JSX.Element {
|
|||||||
<div className="menu-content">
|
<div className="menu-content">
|
||||||
<Button title="[r]eplay flow" icon="fa-repeat text-primary"
|
<Button title="[r]eplay flow" icon="fa-repeat text-primary"
|
||||||
onClick={() => dispatch(replayFlow(flow))}
|
onClick={() => dispatch(replayFlow(flow))}
|
||||||
disabled={!(flow.type === "http" && !flow.websocket)}
|
disabled={!canReplay(flow)}
|
||||||
>
|
>
|
||||||
Replay
|
Replay
|
||||||
</Button>
|
</Button>
|
||||||
@ -46,6 +47,7 @@ export default function FlowMenu(): JSX.Element {
|
|||||||
onClick={() => dispatch(removeFlow(flow))}>
|
onClick={() => dispatch(removeFlow(flow))}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
<MarkButton flow={flow}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="menu-legend">Flow Modification</div>
|
<div className="menu-legend">Flow Modification</div>
|
||||||
</div>
|
</div>
|
||||||
@ -54,17 +56,7 @@ export default function FlowMenu(): JSX.Element {
|
|||||||
<div className="menu-group">
|
<div className="menu-group">
|
||||||
<div className="menu-content">
|
<div className="menu-content">
|
||||||
<DownloadButton flow={flow}/>
|
<DownloadButton flow={flow}/>
|
||||||
<Dropdown className="" text={
|
<ExportButton flow={flow}/>
|
||||||
<Button title="Export flow." icon="fa-clone" onClick={() => 1}>Export▾</Button>
|
|
||||||
} options={{"placement": "bottom-start"}}>
|
|
||||||
<MenuItem onClick={() => copy(flow, "raw_request")}>Copy raw request</MenuItem>
|
|
||||||
<MenuItem onClick={() => copy(flow, "raw_response")}>Copy raw response</MenuItem>
|
|
||||||
<MenuItem onClick={() => copy(flow, "raw")}>Copy raw request and response</MenuItem>
|
|
||||||
<MenuItem onClick={() => copy(flow, "curl")}>Copy as cURL</MenuItem>
|
|
||||||
<MenuItem onClick={() => copy(flow, "httpie")}>Copy as HTTPie</MenuItem>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="menu-legend">Export</div>
|
<div className="menu-legend">Export</div>
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +83,7 @@ export default function FlowMenu(): JSX.Element {
|
|||||||
|
|
||||||
function DownloadButton({flow}: { flow: Flow }) {
|
function DownloadButton({flow}: { flow: Flow }) {
|
||||||
if (flow.type !== "http")
|
if (flow.type !== "http")
|
||||||
return null;
|
return <Button icon="fa-download" onClick={() => 0} disabled>Download</Button>;
|
||||||
|
|
||||||
if (flow.request.contentLength && !flow.response?.contentLength) {
|
if (flow.request.contentLength && !flow.response?.contentLength) {
|
||||||
return <Button icon="fa-download"
|
return <Button icon="fa-download"
|
||||||
@ -119,3 +111,44 @@ function DownloadButton({flow}: { flow: Flow }) {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExportButton({flow}: { flow: Flow }) {
|
||||||
|
return <Dropdown className="" text={
|
||||||
|
<Button title="Export flow." icon="fa-clone" onClick={() => 1}
|
||||||
|
disabled={flow.type === "tcp"}>Export▾</Button>
|
||||||
|
} options={{"placement": "bottom-start"}}>
|
||||||
|
<MenuItem onClick={() => copy(flow, "raw_request")}>Copy raw request</MenuItem>
|
||||||
|
<MenuItem onClick={() => copy(flow, "raw_response")}>Copy raw response</MenuItem>
|
||||||
|
<MenuItem onClick={() => copy(flow, "raw")}>Copy raw request and response</MenuItem>
|
||||||
|
<MenuItem onClick={() => copy(flow, "curl")}>Copy as cURL</MenuItem>
|
||||||
|
<MenuItem onClick={() => copy(flow, "httpie")}>Copy as HTTPie</MenuItem>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const markers = {
|
||||||
|
":red_circle:": "🔴",
|
||||||
|
":orange_circle:": "🟠",
|
||||||
|
":yellow_circle:": "🟡",
|
||||||
|
":green_circle:": "🟢",
|
||||||
|
":large_blue_circle:": "🔵",
|
||||||
|
":purple_circle:": "🟣",
|
||||||
|
":brown_circle:": "🟤",
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarkButton({flow}: { flow: Flow }) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
return <Dropdown className="" text={
|
||||||
|
<Button title="mark flow" icon="fa-paint-brush text-success" onClick={() => 1}>Mark▾</Button>
|
||||||
|
} options={{"placement": "bottom-start"}}>
|
||||||
|
<MenuItem onClick={() => dispatch(flowActions.update(flow, {marked: ""}))}>⚪ (no
|
||||||
|
marker)</MenuItem>
|
||||||
|
{Object.entries(markers).map(([name, sym]) =>
|
||||||
|
<MenuItem
|
||||||
|
key={name}
|
||||||
|
onClick={() => dispatch(flowActions.update(flow, {marked: name}))}>
|
||||||
|
{sym} {name.replace(/[:_]/g, " ")}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import * as flowActions from "../flows";
|
||||||
|
import {tabsForFlow} from "../../components/FlowView";
|
||||||
|
|
||||||
export const
|
export const
|
||||||
SET_TAB = "UI_FLOWVIEW_SET_TAB",
|
SET_TAB = "UI_FLOWVIEW_SET_TAB",
|
||||||
SET_CONTENT_VIEW_FOR = "SET_CONTENT_VIEW_FOR"
|
SET_CONTENT_VIEW_FOR = "SET_CONTENT_VIEW_FOR"
|
||||||
|
@ -57,7 +57,7 @@ export function onKeyDown(e: KeyboardEvent) {
|
|||||||
if (!flow) break
|
if (!flow) break
|
||||||
let tabs = tabsForFlow(flow),
|
let tabs = tabsForFlow(flow),
|
||||||
currentTab = getState().ui.flow.tab,
|
currentTab = getState().ui.flow.tab,
|
||||||
nextTab = tabs[(tabs.indexOf(currentTab) - 1 + tabs.length) % tabs.length]
|
nextTab = tabs[(Math.max(0, tabs.indexOf(currentTab)) - 1 + tabs.length) % tabs.length]
|
||||||
dispatch(selectTab(nextTab))
|
dispatch(selectTab(nextTab))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ export function onKeyDown(e: KeyboardEvent) {
|
|||||||
if (!flow) break
|
if (!flow) break
|
||||||
let tabs = tabsForFlow(flow),
|
let tabs = tabsForFlow(flow),
|
||||||
currentTab = getState().ui.flow.tab,
|
currentTab = getState().ui.flow.tab,
|
||||||
nextTab = tabs[(tabs.indexOf(currentTab) + 1) % tabs.length]
|
nextTab = tabs[(Math.max(0, tabs.indexOf(currentTab)) + 1) % tabs.length]
|
||||||
dispatch(selectTab(nextTab))
|
dispatch(selectTab(nextTab))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ export interface HTTPFlow extends _Flow {
|
|||||||
|
|
||||||
export interface TCPFlow extends _Flow {
|
export interface TCPFlow extends _Flow {
|
||||||
type: "tcp"
|
type: "tcp"
|
||||||
|
messages_meta: MessagesMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
@ -100,11 +101,14 @@ export interface HTTPResponse extends HTTPMessage {
|
|||||||
reason: string
|
reason: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebSocketData {
|
export interface MessagesMeta {
|
||||||
messages_meta: {
|
contentLength: number,
|
||||||
count: number,
|
count: number,
|
||||||
timestamp_last?: number
|
timestamp_last?: number
|
||||||
},
|
}
|
||||||
|
|
||||||
|
export interface WebSocketData {
|
||||||
|
messages_meta: MessagesMeta,
|
||||||
closed_by_client?: boolean
|
closed_by_client?: boolean
|
||||||
close_code?: number
|
close_code?: number
|
||||||
close_reason?: string
|
close_reason?: string
|
||||||
|
@ -3,5 +3,11 @@ import {Flow} from "../flow";
|
|||||||
|
|
||||||
export const copy = async (flow: Flow, format: string): Promise<void> => {
|
export const copy = async (flow: Flow, format: string): Promise<void> => {
|
||||||
let ret = await runCommand("export", format, `@${flow.id}`);
|
let ret = await runCommand("export", format, `@${flow.id}`);
|
||||||
await navigator.clipboard.writeText(ret);
|
if(ret.value) {
|
||||||
|
await navigator.clipboard.writeText(ret.value);
|
||||||
|
} else if(ret.error) {
|
||||||
|
alert(ret.error)
|
||||||
|
} else {
|
||||||
|
console.error(ret);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Flow, HTTPFlow, HTTPMessage, HTTPRequest} from "../flow";
|
import {Flow, HTTPMessage, HTTPRequest} from "../flow";
|
||||||
|
|
||||||
const defaultPorts = {
|
const defaultPorts = {
|
||||||
"http": 80,
|
"http": 80,
|
||||||
@ -53,14 +53,14 @@ export class MessageUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getContentURL(
|
static getContentURL(
|
||||||
flow: HTTPFlow,
|
flow: Flow,
|
||||||
part: HTTPMessage | "request" | "response" | "messages",
|
part: HTTPMessage | "request" | "response" | "messages",
|
||||||
view?: string,
|
view?: string,
|
||||||
lines?: number
|
lines?: number
|
||||||
): string {
|
): string {
|
||||||
if (part === flow.request) {
|
if (flow.type === "http" && part === flow.request) {
|
||||||
part = "request";
|
part = "request";
|
||||||
} else if (part === flow.response) {
|
} else if (flow.type === "http" && part === flow.response) {
|
||||||
part = "response";
|
part = "response";
|
||||||
}
|
}
|
||||||
const lineStr = lines ? `?lines=${lines}` : "";
|
const lineStr = lines ? `?lines=${lines}` : "";
|
||||||
@ -129,33 +129,50 @@ export var isValidHttpVersion = function (httpVersion: string): boolean {
|
|||||||
|
|
||||||
|
|
||||||
export function startTime(flow: Flow): number | undefined {
|
export function startTime(flow: Flow): number | undefined {
|
||||||
if (flow.type === "http") {
|
switch (flow.type) {
|
||||||
return flow.request.timestamp_start
|
case "http":
|
||||||
|
return flow.request.timestamp_start
|
||||||
|
case "tcp":
|
||||||
|
return flow.client_conn.timestamp_start
|
||||||
}
|
}
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function endTime(flow: Flow): number | undefined {
|
export function endTime(flow: Flow): number | undefined {
|
||||||
if (flow.type === "http") {
|
switch (flow.type) {
|
||||||
if (flow.websocket) {
|
case "http":
|
||||||
if (flow.websocket.timestamp_end)
|
if (flow.websocket) {
|
||||||
return flow.websocket.timestamp_end
|
if (flow.websocket.timestamp_end)
|
||||||
if (flow.websocket.messages_meta.timestamp_last)
|
return flow.websocket.timestamp_end
|
||||||
return flow.websocket.messages_meta.timestamp_last
|
if (flow.websocket.messages_meta.timestamp_last)
|
||||||
}
|
return flow.websocket.messages_meta.timestamp_last
|
||||||
if (flow.response) {
|
}
|
||||||
return flow.response.timestamp_end
|
if (flow.response) {
|
||||||
}
|
return flow.response.timestamp_end
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
case "tcp":
|
||||||
|
return flow.server_conn?.timestamp_end
|
||||||
}
|
}
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTotalSize = (flow: Flow): number => {
|
export const getTotalSize = (flow: Flow): number => {
|
||||||
if (flow.type !== "http")
|
switch (flow.type) {
|
||||||
return 0
|
case "http":
|
||||||
let total = flow.request.contentLength || 0
|
let total = flow.request.contentLength || 0
|
||||||
if (flow.response) {
|
if (flow.response) {
|
||||||
total += flow.response.contentLength || 0
|
total += flow.response.contentLength || 0
|
||||||
|
}
|
||||||
|
if (flow.websocket) {
|
||||||
|
total += flow.websocket.messages_meta.contentLength || 0
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
case "tcp":
|
||||||
|
return flow.messages_meta.contentLength || 0
|
||||||
}
|
}
|
||||||
return total
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const canReplay = (flow: Flow): boolean => {
|
||||||
|
return (flow.type === "http" && !flow.websocket)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user