mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-21 22:58:24 +00:00
major mitmweb upgrades
This commit is contained in:
parent
46cf75d01e
commit
65aa0a6ef5
@ -5,7 +5,8 @@ import logging
|
||||
import os.path
|
||||
import re
|
||||
from io import BytesIO
|
||||
from typing import ClassVar, Optional
|
||||
from itertools import islice
|
||||
from typing import ClassVar, Optional, Sequence, Union
|
||||
|
||||
import tornado.escape
|
||||
import tornado.web
|
||||
@ -13,7 +14,7 @@ import tornado.websocket
|
||||
|
||||
import mitmproxy.flow
|
||||
import mitmproxy.tools.web.master # noqa
|
||||
from mitmproxy import contentviews
|
||||
from mitmproxy import certs, command, contentviews
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy import http
|
||||
from mitmproxy import io
|
||||
@ -21,7 +22,27 @@ from mitmproxy import log
|
||||
from mitmproxy import optmanager
|
||||
from mitmproxy import version
|
||||
from mitmproxy.addons import export
|
||||
from mitmproxy.http import HTTPFlow
|
||||
from mitmproxy.tcp import TCPFlow, TCPMessage
|
||||
from mitmproxy.tools.console.common import SYMBOL_MARK, render_marker
|
||||
from mitmproxy.utils.strutils import always_str
|
||||
from mitmproxy.websocket import WebSocketMessage
|
||||
|
||||
|
||||
def cert_to_json(certs: Sequence[certs.Cert]) -> Optional[dict]:
|
||||
if not certs:
|
||||
return None
|
||||
cert = certs[0]
|
||||
return {
|
||||
"keyinfo": cert.keyinfo,
|
||||
"sha256": cert.fingerprint().hex(),
|
||||
"notbefore": int(cert.notbefore.timestamp()),
|
||||
"notafter": int(cert.notafter.timestamp()),
|
||||
"serial": str(cert.serial),
|
||||
"subject": cert.subject,
|
||||
"issuer": cert.issuer,
|
||||
"altnames": cert.altnames,
|
||||
}
|
||||
|
||||
|
||||
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
@ -37,7 +58,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"is_replay": flow.is_replay,
|
||||
"type": flow.type,
|
||||
"modified": flow.modified(),
|
||||
"marked": flow.marked,
|
||||
"marked": render_marker(flow.marked).replace(SYMBOL_MARK, "🔴") if flow.marked else "",
|
||||
}
|
||||
|
||||
if flow.client_conn:
|
||||
@ -46,6 +67,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"peername": flow.client_conn.peername,
|
||||
"sockname": flow.client_conn.sockname,
|
||||
"tls_established": flow.client_conn.tls_established,
|
||||
"cert": cert_to_json(flow.client_conn.certificate_list),
|
||||
"sni": flow.client_conn.sni,
|
||||
"cipher": flow.client_conn.cipher,
|
||||
"alpn": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
||||
@ -53,11 +75,6 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"timestamp_start": flow.client_conn.timestamp_start,
|
||||
"timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,
|
||||
"timestamp_end": flow.client_conn.timestamp_end,
|
||||
|
||||
# Legacy properties
|
||||
"address": flow.client_conn.peername,
|
||||
"cipher_name": flow.client_conn.cipher,
|
||||
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
|
||||
}
|
||||
|
||||
if flow.server_conn:
|
||||
@ -67,6 +84,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"sockname": flow.server_conn.sockname,
|
||||
"address": flow.server_conn.address,
|
||||
"tls_established": flow.server_conn.tls_established,
|
||||
"cert": cert_to_json(flow.server_conn.certificate_list),
|
||||
"sni": flow.server_conn.sni,
|
||||
"cipher": flow.server_conn.cipher,
|
||||
"alpn": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
|
||||
@ -75,10 +93,6 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,
|
||||
"timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,
|
||||
"timestamp_end": flow.server_conn.timestamp_end,
|
||||
# Legacy properties
|
||||
"ip_address": flow.server_conn.peername,
|
||||
"source_address": flow.server_conn.sockname,
|
||||
"alpn_proto_negotiated": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
|
||||
}
|
||||
if flow.error:
|
||||
f["error"] = flow.error.get_state()
|
||||
@ -87,7 +101,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
content_length: Optional[int]
|
||||
content_hash: Optional[str]
|
||||
if flow.request:
|
||||
if flow.request.raw_content:
|
||||
if flow.request.raw_content is not None:
|
||||
content_length = len(flow.request.raw_content)
|
||||
content_hash = hashlib.sha256(flow.request.raw_content).hexdigest()
|
||||
else:
|
||||
@ -109,7 +123,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
"pretty_host": flow.request.pretty_host,
|
||||
}
|
||||
if flow.response:
|
||||
if flow.response.raw_content:
|
||||
if flow.response.raw_content is not None:
|
||||
content_length = len(flow.response.raw_content)
|
||||
content_hash = hashlib.sha256(flow.response.raw_content).hexdigest()
|
||||
else:
|
||||
@ -129,6 +143,18 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
|
||||
if flow.response.data.trailers:
|
||||
f["response"]["trailers"] = tuple(flow.response.data.trailers.items(True))
|
||||
|
||||
if flow.websocket:
|
||||
f["websocket"] = {
|
||||
"messages_meta": {
|
||||
"count": len(flow.websocket.messages),
|
||||
"timestamp_last": flow.websocket.messages[-1].timestamp if flow.websocket.messages else None,
|
||||
},
|
||||
"closed_by_client": flow.websocket.closed_by_client,
|
||||
"close_code": flow.websocket.close_code,
|
||||
"close_reason": flow.websocket.close_reason,
|
||||
"timestamp_end": flow.websocket.timestamp_end,
|
||||
}
|
||||
|
||||
return f
|
||||
|
||||
|
||||
@ -147,7 +173,7 @@ class APIError(tornado.web.HTTPError):
|
||||
class RequestHandler(tornado.web.RequestHandler):
|
||||
application: "Application"
|
||||
|
||||
def write(self, chunk):
|
||||
def write(self, chunk: Union[str, bytes, dict, list]):
|
||||
# Writing arrays on the top level is ok nowadays.
|
||||
# http://flask.pocoo.org/docs/0.11/security/#json-security
|
||||
if isinstance(chunk, list):
|
||||
@ -217,7 +243,7 @@ class IndexHandler(RequestHandler):
|
||||
def get(self):
|
||||
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
|
||||
assert token
|
||||
self.render("index.html", static=False, version=version.VERSION)
|
||||
self.render("index.html")
|
||||
|
||||
|
||||
class FilterHelp(RequestHandler):
|
||||
@ -278,14 +304,6 @@ class DumpFlows(RequestHandler):
|
||||
bio.close()
|
||||
|
||||
|
||||
class ExportFlow(RequestHandler):
|
||||
def post(self, flow_id, format):
|
||||
out = export.formats[format](self.flow)
|
||||
self.write({
|
||||
"export": always_str(out, "utf8", "backslashreplace")
|
||||
})
|
||||
|
||||
|
||||
class ClearAll(RequestHandler):
|
||||
def post(self):
|
||||
self.view.clear()
|
||||
@ -329,12 +347,12 @@ class FlowHandler(RequestHandler):
|
||||
self.view.remove([self.flow])
|
||||
|
||||
def put(self, flow_id):
|
||||
flow = self.flow
|
||||
flow: mitmproxy.flow.Flow = self.flow
|
||||
flow.backup()
|
||||
try:
|
||||
for a, b in self.json.items():
|
||||
if a == "request" and hasattr(flow, "request"):
|
||||
request = flow.request
|
||||
request: mitmproxy.http.Request = flow.request
|
||||
for k, v in b.items():
|
||||
if k in ["method", "scheme", "host", "path", "http_version"]:
|
||||
setattr(request, k, str(v))
|
||||
@ -354,7 +372,7 @@ class FlowHandler(RequestHandler):
|
||||
raise APIError(400, f"Unknown update request.{k}: {v}")
|
||||
|
||||
elif a == "response" and hasattr(flow, "response"):
|
||||
response = flow.response
|
||||
response: mitmproxy.http.Response = flow.response
|
||||
for k, v in b.items():
|
||||
if k in ["msg", "http_version"]:
|
||||
setattr(response, k, str(v))
|
||||
@ -372,6 +390,8 @@ class FlowHandler(RequestHandler):
|
||||
response.text = v
|
||||
else:
|
||||
raise APIError(400, f"Unknown update response.{k}: {v}")
|
||||
elif a == "marked":
|
||||
flow.marked = b
|
||||
else:
|
||||
raise APIError(400, f"Unknown update {a}: {b}")
|
||||
except APIError:
|
||||
@ -409,9 +429,6 @@ class FlowContent(RequestHandler):
|
||||
def get(self, flow_id, message):
|
||||
message = getattr(self.flow, message)
|
||||
|
||||
if not message.raw_content:
|
||||
raise APIError(400, "No content.")
|
||||
|
||||
content_encoding = message.headers.get("Content-Encoding", None)
|
||||
if content_encoding:
|
||||
content_encoding = re.sub(r"[^\w]", "", content_encoding)
|
||||
@ -436,40 +453,88 @@ class FlowContent(RequestHandler):
|
||||
|
||||
|
||||
class FlowContentView(RequestHandler):
|
||||
def get(self, flow_id, message, content_view):
|
||||
message = getattr(self.flow, message)
|
||||
def message_to_json(
|
||||
self,
|
||||
viewname: str,
|
||||
message: Union[http.Message, TCPMessage, WebSocketMessage],
|
||||
flow: Union[HTTPFlow, TCPFlow],
|
||||
max_lines: Optional[int] = None
|
||||
):
|
||||
description, lines, error = contentviews.get_message_content_view(viewname, message, flow)
|
||||
if error:
|
||||
self.master.log.error(error)
|
||||
if max_lines:
|
||||
lines = islice(lines, max_lines)
|
||||
|
||||
description, lines, error = contentviews.get_message_content_view(
|
||||
content_view.replace('_', ' '), message, self.flow
|
||||
)
|
||||
# if error:
|
||||
# add event log
|
||||
|
||||
self.write(dict(
|
||||
return dict(
|
||||
lines=list(lines),
|
||||
description=description
|
||||
))
|
||||
description=description,
|
||||
)
|
||||
|
||||
def get(self, flow_id, message, content_view):
|
||||
flow = self.flow
|
||||
assert isinstance(flow, (HTTPFlow, TCPFlow))
|
||||
|
||||
if self.request.arguments.get("lines"):
|
||||
max_lines = int(self.request.arguments["lines"][0])
|
||||
else:
|
||||
max_lines = None
|
||||
|
||||
if message == "messages":
|
||||
if isinstance(flow, HTTPFlow) and flow.websocket:
|
||||
messages = flow.websocket.messages
|
||||
elif isinstance(flow, TCPFlow):
|
||||
messages = flow.messages
|
||||
else:
|
||||
raise APIError(400, f"This flow has no messages.")
|
||||
msgs = []
|
||||
for m in messages:
|
||||
d = self.message_to_json(content_view, m, flow, max_lines)
|
||||
d["from_client"] = m.from_client
|
||||
d["timestamp"] = m.timestamp
|
||||
msgs.append(d)
|
||||
if max_lines:
|
||||
max_lines -= len(d["lines"])
|
||||
if max_lines <= 0:
|
||||
break
|
||||
self.write(msgs)
|
||||
else:
|
||||
message = getattr(self.flow, message)
|
||||
self.write(self.message_to_json(content_view, message, flow, max_lines))
|
||||
|
||||
|
||||
class Commands(RequestHandler):
|
||||
def get(self):
|
||||
def get(self) -> None:
|
||||
commands = {}
|
||||
for (name, command) in self.master.commands.commands.items():
|
||||
for (name, cmd) in self.master.commands.commands.items():
|
||||
commands[name] = {
|
||||
"args": [],
|
||||
"signature_help": command.signature_help(),
|
||||
"description": command.help
|
||||
"help": cmd.help,
|
||||
"parameters": [
|
||||
{
|
||||
"name": param.name,
|
||||
"type": command.typename(param.type),
|
||||
"kind": str(param.kind),
|
||||
}
|
||||
for param in cmd.parameters
|
||||
],
|
||||
"return_type": command.typename(cmd.return_type) if cmd.return_type else None,
|
||||
"signature_help": cmd.signature_help(),
|
||||
}
|
||||
for parameter in command.parameters:
|
||||
commands[name]["args"].append(parameter.name)
|
||||
self.write({"commands": commands})
|
||||
self.write(commands)
|
||||
|
||||
def post(self):
|
||||
result = self.master.commands.execute(self.json["command"])
|
||||
if result is None:
|
||||
self.write({"result": ""})
|
||||
return
|
||||
self.write({"result": result, "type": type(result).__name__, "history": self.master.commands.execute("commands.history.get")})
|
||||
|
||||
class ExecuteCommand(RequestHandler):
|
||||
def post(self, cmd: str):
|
||||
# TODO: We should parse query strings here, this API is painful.
|
||||
try:
|
||||
args = self.json['arguments']
|
||||
except APIError:
|
||||
args = []
|
||||
result = self.master.commands.call_strings(cmd, args)
|
||||
self.write({
|
||||
"value": result,
|
||||
"type": command.typename(type(result)) if result is not None else "none"
|
||||
})
|
||||
|
||||
|
||||
class Events(RequestHandler):
|
||||
@ -512,7 +577,7 @@ class Conf(RequestHandler):
|
||||
conf = {
|
||||
"static": False,
|
||||
"version": version.VERSION,
|
||||
"contentViews": [v.name for v in contentviews.views]
|
||||
"contentViews": [v.name for v in contentviews.views if v.name != "Query"]
|
||||
}
|
||||
self.write(f"MITMWEB_CONF = {json.dumps(conf)};")
|
||||
self.set_header("content-type", "application/javascript")
|
||||
@ -542,6 +607,7 @@ class Application(tornado.web.Application):
|
||||
(r"/filter-help(?:\.json)?", FilterHelp),
|
||||
(r"/updates", ClientConnection),
|
||||
(r"/commands(?:\.json)?", Commands),
|
||||
(r"/commands/(?P<cmd>[a-z.]+)", ExecuteCommand),
|
||||
(r"/events(?:\.json)?", Events),
|
||||
(r"/flows(?:\.json)?", Flows),
|
||||
(r"/flows/dump", DumpFlows),
|
||||
@ -553,10 +619,9 @@ class Application(tornado.web.Application):
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/export/(?P<format>[a-z][a-z_]+).json", ExportFlow),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content.data", FlowContent),
|
||||
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response|messages)/content.data", FlowContent),
|
||||
(
|
||||
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)(?:\.json)?",
|
||||
r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response|messages)/content/(?P<content_view>[0-9a-zA-Z\-\_%]+)(?:\.json)?",
|
||||
FlowContentView),
|
||||
(r"/clear", ClearAll),
|
||||
(r"/options(?:\.json)?", Options),
|
||||
|
BIN
mitmproxy/tools/web/static/images/resourceWebSocketIcon.png
vendored
Normal file
BIN
mitmproxy/tools/web/static/images/resourceWebSocketIcon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -265,6 +265,18 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
||||
"description": "Raw"
|
||||
}
|
||||
|
||||
def test_commands(self):
|
||||
resp = self.fetch("/commands")
|
||||
assert resp.code == 200
|
||||
assert get_json(resp)["set"]["help"]
|
||||
|
||||
def test_command_execute(self):
|
||||
resp = self.fetch("/commands/unknown", method="POST")
|
||||
assert resp.code == 500
|
||||
resp = self.fetch("/commands/commands.history.get", method="POST")
|
||||
assert resp.code == 200
|
||||
assert get_json(resp) == ["unknown", "commands.history.get"]
|
||||
|
||||
def test_events(self):
|
||||
resp = self.fetch("/events")
|
||||
assert resp.code == 200
|
||||
@ -368,14 +380,14 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
|
||||
print("/** Auto-generated by test_app.py:TestApp.test_generate_options_js */")
|
||||
|
||||
print("export interface OptionsState {")
|
||||
for _, opt in m.options.items():
|
||||
for _, opt in sorted(m.options.items()):
|
||||
print(f" {opt.name}: {ts_type(opt.typespec)}")
|
||||
print("}")
|
||||
print("")
|
||||
print("export type Option = keyof OptionsState")
|
||||
print("")
|
||||
print("export const defaultState: OptionsState = {")
|
||||
for _, opt in m.options.items():
|
||||
for _, opt in sorted(m.options.items()):
|
||||
print(f" {opt.name}: {json.dumps(opt.default)},".replace(": null", ": undefined"))
|
||||
print("}")
|
||||
|
||||
|
@ -43,7 +43,7 @@ function styles_app_dev() {
|
||||
|
||||
|
||||
function esbuild(dev) {
|
||||
return gulp.src('src/js/app.jsx').pipe(
|
||||
return gulp.src('src/js/app.tsx').pipe(
|
||||
gulpEsbuild({
|
||||
outfile: 'app.js',
|
||||
sourcemap: true,
|
||||
|
@ -1,11 +1,6 @@
|
||||
//TODO: Move into some utils
|
||||
.monospace() {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.flow-detail {
|
||||
width: 100%;
|
||||
overflow:hidden;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -15,45 +10,52 @@
|
||||
|
||||
section {
|
||||
overflow-y: scroll;
|
||||
>article{
|
||||
overflow: auto;
|
||||
padding: 5px 12px 0;
|
||||
}
|
||||
>footer {
|
||||
flex: 1;
|
||||
padding: 5px 12px 10px;
|
||||
|
||||
> footer {
|
||||
box-shadow: 0 0 3px gray;
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
height:23px;
|
||||
height: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
section.detail, section.error{
|
||||
overflow: auto;
|
||||
padding: 5px 12px 0;
|
||||
}
|
||||
|
||||
|
||||
.first-line {
|
||||
.monospace();
|
||||
font-family: @font-family-monospace;
|
||||
background-color: #428bca;
|
||||
color: white;
|
||||
margin: 0 -8px;
|
||||
margin: 0 -8px 2px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
word-break: break-all;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
|
||||
.inline-input.editable {
|
||||
border-color: rgba(255,255,255,0.5);
|
||||
}
|
||||
}
|
||||
.request-line {
|
||||
margin-bottom: 2px;
|
||||
|
||||
.contentview {
|
||||
margin: 0 -12px;
|
||||
padding: 0 12px;
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
h5 {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
pre button:not(:only-child) {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0 0 5px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,30 +66,40 @@
|
||||
padding: 0 3px;
|
||||
|
||||
border: solid transparent 1px;
|
||||
&.editable {
|
||||
border-color: #ccc;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 1.25%), 0 2px 4px rgba(0, 0, 0, 5%), 0 2px 6px rgba(0, 0, 0, 2.5%);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&[placeholder]:empty:not(:focus-visible):before {
|
||||
content: attr(placeholder);
|
||||
color: lightgray;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&[contenteditable] {
|
||||
|
||||
outline-width: 0;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 5%), 0 2px 4px rgba(0, 0, 0, 20%), 0 2px 6px rgba(0, 0, 0, 10%);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
|
||||
|
||||
&.has-warning {
|
||||
color: rgb(255, 184, 184);
|
||||
}
|
||||
|
||||
&.has-success {
|
||||
//color: green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-all-content-btn{
|
||||
float: right;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.flow-detail table {
|
||||
.monospace();
|
||||
td:nth-child(2) {
|
||||
font-family: @font-family-monospace;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
word-break: break-all;
|
||||
@ -109,34 +121,45 @@
|
||||
|
||||
.connection-table {
|
||||
td:first-child {
|
||||
width: 50%;
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.header-table {
|
||||
td {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
.header-name {
|
||||
width: 33%;
|
||||
}
|
||||
.header-value {
|
||||
|
||||
.headers, .trailers {
|
||||
.kv-row {
|
||||
margin-bottom: .3em;
|
||||
max-height: 12.4ex;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// This exists so that you can copy
|
||||
// and paste headers out of mitmweb.
|
||||
.header-colon {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
.kv-key {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.kv-value {
|
||||
font-family: @font-family-monospace;
|
||||
}
|
||||
|
||||
.inline-input {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.kv-add-row {
|
||||
opacity: 0;
|
||||
color: #666;
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
transition: all 100ms ease-in-out;
|
||||
}
|
||||
|
||||
&:hover .kv-add-row {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
min-height: 2ex;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.connection-table, .timing-table {
|
||||
@ -146,3 +169,23 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
dl.cert-attributes {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
dt, dd {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
dt {
|
||||
flex: 0 0 2em;
|
||||
}
|
||||
|
||||
dd {
|
||||
flex: 0 0 calc(100% - 2em);
|
||||
}
|
||||
}
|
||||
|
@ -111,8 +111,12 @@
|
||||
}
|
||||
|
||||
.col-path {
|
||||
.marker {
|
||||
}
|
||||
|
||||
.fa {
|
||||
margin-left: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fa-repeat {
|
||||
|
@ -1,6 +1,7 @@
|
||||
.flowview-image {
|
||||
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
|
@ -1,8 +1,8 @@
|
||||
footer {
|
||||
box-shadow: 0 -1px 3px lightgray;
|
||||
padding: 0px 10px 3px;
|
||||
padding: 0 0 4px 3px;
|
||||
|
||||
.label {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,10 @@ header {
|
||||
height: @menu-height - @menu-legend-height;
|
||||
display: flow-root;
|
||||
|
||||
> .btn {
|
||||
> a {
|
||||
display: inline-block;
|
||||
}
|
||||
> .btn, > a > .btn {
|
||||
height: @menu-height - @menu-legend-height;
|
||||
text-align: center;
|
||||
margin: 0 1px;
|
||||
|
@ -43,4 +43,8 @@
|
||||
|
||||
.resource-icon-redirect {
|
||||
background-image: url(images/resourceRedirectIcon.png);
|
||||
}
|
||||
}
|
||||
|
||||
.resource-icon-websocket {
|
||||
background-image: url(images/resourceWebSocketIcon.png);
|
||||
}
|
||||
|
@ -38,12 +38,7 @@
|
||||
|
||||
.nav-tabs-sm {
|
||||
> a {
|
||||
padding: 0px 7px;
|
||||
padding: 0 7px;
|
||||
margin: 2px 2px -1px;
|
||||
}
|
||||
> a.nav-action {
|
||||
float: right;
|
||||
padding: 0;
|
||||
margin: 1px 0 0px;
|
||||
}
|
||||
}
|
||||
|
@ -3,3 +3,5 @@
|
||||
@navbar-default-color: #303030;
|
||||
@navbar-default-bg: #ffffff;
|
||||
@navbar-default-border: #e0e0e0;
|
||||
@font-family-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
@font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
|
BIN
web/src/images/resourceWebSocketIcon.png
Normal file
BIN
web/src/images/resourceWebSocketIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import CommandBar from '../../../components/CommandBar'
|
||||
import { render } from "../../test-utils"
|
||||
import fetchMock from 'fetch-mock';
|
||||
@ -13,4 +13,4 @@ test('CommandBar Component', async () => {
|
||||
await waitFor(() => {
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import withContentLoader from '../../../components/ContentView/ContentLoader'
|
||||
import withContentLoader from '../../../components/contentviews/useContent'
|
||||
import { TFlow } from '../../ducks/tutils'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import mockXMLHttpRequest from 'mock-xmlhttprequest'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import ContentViewOptions from '../../../components/ContentView/ContentViewOptions'
|
||||
import { Provider } from 'react-redux'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import { Provider } from 'react-redux'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import DownloadContentButton from '../../../components/ContentView/DownloadContentButton'
|
||||
import { TFlow } from '../../ducks/tutils'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import { ContentEmpty, ContentMissing, ContentTooLarge } from '../../../components/ContentView/MetaViews'
|
||||
import { TFlow } from '../../ducks/tutils'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import { Provider } from 'react-redux'
|
||||
import ShowFullContentButton from '../../../components/ContentView/ShowFullContentButton'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import UploadContentButton from '../../../components/ContentView/UploadContentButton'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import ViewSelector from '../../../components/ContentView/ViewSelector'
|
||||
import { Provider } from 'react-redux'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import ContentView from '../../components/ContentView'
|
||||
import { TStore, TFlow } from '../ducks/tutils'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import EventLogList from '../../../components/EventLog/EventList'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
jest.mock('../../components/EventLog/EventList')
|
||||
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import EventLog, {PureEventLog} from '../../components/EventLog'
|
||||
import {Provider} from 'react-redux'
|
||||
|
@ -1,15 +1,15 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import {
|
||||
IconColumn,
|
||||
MethodColumn,
|
||||
PathColumn,
|
||||
QuickActionsColumn,
|
||||
SizeColumn,
|
||||
StatusColumn,
|
||||
TimeColumn,
|
||||
TimeStampColumn,
|
||||
TLSColumn
|
||||
icon,
|
||||
method,
|
||||
path,
|
||||
quickactions,
|
||||
size,
|
||||
status,
|
||||
time,
|
||||
timestamp,
|
||||
tls
|
||||
} from '../../../components/FlowTable/FlowColumns'
|
||||
import {TFlow, TStore} from '../../ducks/tutils'
|
||||
import {Provider} from 'react-redux'
|
||||
@ -18,109 +18,109 @@ describe('Flowcolumns Components', () => {
|
||||
|
||||
let tflow = TFlow()
|
||||
it('should render TLSColumn', () => {
|
||||
let tlsColumn = renderer.create(<TLSColumn flow={tflow}/>),
|
||||
let tlsColumn = renderer.create(<tls flow={tflow}/>),
|
||||
tree = tlsColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render IconColumn', () => {
|
||||
let tflow = TFlow(),
|
||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>),
|
||||
iconColumn = renderer.create(<icon flow={tflow}/>),
|
||||
tree = iconColumn.toJSON()
|
||||
// plain
|
||||
expect(tree).toMatchSnapshot()
|
||||
// not modified
|
||||
tflow.response.status_code = 304
|
||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>)
|
||||
iconColumn = renderer.create(<icon flow={tflow}/>)
|
||||
tree = iconColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
// redirect
|
||||
tflow.response.status_code = 302
|
||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>)
|
||||
iconColumn = renderer.create(<icon flow={tflow}/>)
|
||||
tree = iconColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
// image
|
||||
let imageFlow = TFlow()
|
||||
imageFlow.response.headers = [['Content-Type', 'image/jpeg']]
|
||||
iconColumn = renderer.create(<IconColumn flow={imageFlow}/>)
|
||||
iconColumn = renderer.create(<icon flow={imageFlow}/>)
|
||||
tree = iconColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
// javascript
|
||||
let jsFlow = TFlow()
|
||||
jsFlow.response.headers = [['Content-Type', 'application/x-javascript']]
|
||||
iconColumn = renderer.create(<IconColumn flow={jsFlow}/>)
|
||||
iconColumn = renderer.create(<icon flow={jsFlow}/>)
|
||||
tree = iconColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
// css
|
||||
let cssFlow = TFlow()
|
||||
cssFlow.response.headers = [['Content-Type', 'text/css']]
|
||||
iconColumn = renderer.create(<IconColumn flow={cssFlow}/>)
|
||||
iconColumn = renderer.create(<icon flow={cssFlow}/>)
|
||||
tree = iconColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
// html
|
||||
let htmlFlow = TFlow()
|
||||
htmlFlow.response.headers = [['Content-Type', 'text/html']]
|
||||
iconColumn = renderer.create(<IconColumn flow={htmlFlow}/>)
|
||||
iconColumn = renderer.create(<icon flow={htmlFlow}/>)
|
||||
tree = iconColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
// default
|
||||
let fooFlow = TFlow()
|
||||
fooFlow.response.headers = [['Content-Type', 'foo']]
|
||||
iconColumn = renderer.create(<IconColumn flow={fooFlow}/>)
|
||||
iconColumn = renderer.create(<icon flow={fooFlow}/>)
|
||||
tree = iconColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
// no response
|
||||
tflow.response = null
|
||||
iconColumn = renderer.create(<IconColumn flow={tflow}/>)
|
||||
iconColumn = renderer.create(<icon flow={tflow}/>)
|
||||
tree = iconColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render pathColumn', () => {
|
||||
let tflow = TFlow(),
|
||||
pathColumn = renderer.create(<PathColumn flow={tflow}/>),
|
||||
pathColumn = renderer.create(<path flow={tflow}/>),
|
||||
tree = pathColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
||||
tflow.error.msg = 'Connection killed.'
|
||||
tflow.intercepted = true
|
||||
pathColumn = renderer.create(<PathColumn flow={tflow}/>)
|
||||
pathColumn = renderer.create(<path flow={tflow}/>)
|
||||
tree = pathColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render MethodColumn', () => {
|
||||
let methodColumn = renderer.create(<MethodColumn flow={tflow}/>),
|
||||
let methodColumn = renderer.create(<method flow={tflow}/>),
|
||||
tree = methodColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render StatusColumn', () => {
|
||||
let statusColumn = renderer.create(<StatusColumn flow={tflow}/>),
|
||||
let statusColumn = renderer.create(<status flow={tflow}/>),
|
||||
tree = statusColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render SizeColumn', () => {
|
||||
let sizeColumn = renderer.create(<SizeColumn flow={tflow}/>),
|
||||
let sizeColumn = renderer.create(<size flow={tflow}/>),
|
||||
tree = sizeColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render TimeColumn', () => {
|
||||
let tflow = TFlow(),
|
||||
timeColumn = renderer.create(<TimeColumn flow={tflow}/>),
|
||||
timeColumn = renderer.create(<time flow={tflow}/>),
|
||||
tree = timeColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
||||
tflow.response = null
|
||||
timeColumn = renderer.create(<TimeColumn flow={tflow}/>)
|
||||
timeColumn = renderer.create(<time flow={tflow}/>)
|
||||
tree = timeColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render TimeStampColumn', () => {
|
||||
let timeStampColumn = renderer.create(<TimeStampColumn flow={tflow}/>),
|
||||
let timeStampColumn = renderer.create(<timestamp flow={tflow}/>),
|
||||
tree = timeStampColumn.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
@ -129,7 +129,7 @@ describe('Flowcolumns Components', () => {
|
||||
let store = TStore(),
|
||||
provider = renderer.create(
|
||||
<Provider store={store}>
|
||||
<QuickActionsColumn flow={tflow}/>
|
||||
<quickactions flow={tflow}/>
|
||||
</Provider>),
|
||||
tree = provider.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import FlowRow from '../../../components/FlowTable/FlowRow'
|
||||
import {testState} from '../../ducks/tutils'
|
||||
import {fireEvent, render, screen} from "../../test-utils";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import FlowTableHead from '../../../components/FlowTable/FlowTableHead'
|
||||
import {Provider} from 'react-redux'
|
||||
import {TStore} from '../../ducks/tutils'
|
||||
@ -23,7 +23,7 @@ test("FlowTableHead Component", async () => {
|
||||
fireEvent.click(screen.getByText("Size"))
|
||||
|
||||
expect(store.getActions()).toStrictEqual([
|
||||
setSort("SizeColumn", false)
|
||||
setSort("size", false)
|
||||
]
|
||||
)
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import {PureFlowTable as FlowTable} from '../../components/FlowTable'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import Details, { TimeStamp, ConnectionInfo, CertificateInfo, Timing } from '../../../components/FlowView/Details'
|
||||
import Connection, { TimeStamp, ConnectionInfo, CertificateInfo, Timing } from '../../../components/FlowView/Connection'
|
||||
import { TFlow } from '../../ducks/tutils'
|
||||
|
||||
let tflow = TFlow()
|
||||
@ -43,7 +43,7 @@ describe('Timing Component', () => {
|
||||
|
||||
describe('Details Component', () => {
|
||||
it('should render correctly', () => {
|
||||
let details = renderer.create(<Details flow={tflow}/>),
|
||||
let details = renderer.create(<Connection flow={tflow}/>),
|
||||
tree = details.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
@ -53,7 +53,6 @@ describe('Details Component', () => {
|
||||
|
||||
tflowServerAddressNull.server_conn.address = null
|
||||
tflowServerAddressNull.server_conn.ip_address = null
|
||||
tflowServerAddressNull.server_conn.alpn_proto_negotiated = null
|
||||
tflowServerAddressNull.server_conn.sni = null
|
||||
tflowServerAddressNull.server_conn.ssl_established = false
|
||||
tflowServerAddressNull.server_conn.tls_version = null
|
||||
@ -61,8 +60,8 @@ describe('Details Component', () => {
|
||||
tflowServerAddressNull.server_conn.timestamp_ssl_setup = null
|
||||
tflowServerAddressNull.server_conn.timestamp_start = null
|
||||
tflowServerAddressNull.server_conn.timestamp_end = null
|
||||
|
||||
let details = renderer.create(<Details flow={tflowServerAddressNull}/>),
|
||||
|
||||
let details = renderer.create(<Connection flow={tflowServerAddressNull}/>),
|
||||
tree = details.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import ReactDOM from 'react-dom'
|
||||
import renderer from 'react-test-renderer'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import Headers, { HeaderEditor } from '../../../components/FlowView/Headers'
|
||||
import KeyValueListEditor, { HeaderEditor } from '../../../components/editors/KeyValueListEditor'
|
||||
import { Key } from '../../../utils'
|
||||
|
||||
describe('HeaderEditor Component', () => {
|
||||
@ -54,12 +54,12 @@ describe('Headers Component', () => {
|
||||
let changeFn = jest.fn(),
|
||||
mockMessage = { headers: [['k1', 'v1'], ['k2', '']] }
|
||||
it('should handle correctly', () => {
|
||||
let headers = renderer.create(<Headers onChange={changeFn} message={mockMessage}/>),
|
||||
let headers = renderer.create(<KeyValueListEditor onChange={changeFn} message={mockMessage}/>),
|
||||
tree = headers.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
let headers = TestUtils.renderIntoDocument(<Headers onChange={changeFn} message={mockMessage}/>),
|
||||
let headers = TestUtils.renderIntoDocument(<KeyValueListEditor onChange={changeFn} message={mockMessage}/>),
|
||||
headerEditors = TestUtils.scryRenderedComponentsWithType(headers, HeaderEditor),
|
||||
key1Editor = headerEditors[0],
|
||||
value1Editor = headerEditors[1],
|
||||
@ -123,7 +123,7 @@ describe('Headers Component', () => {
|
||||
|
||||
it('should not delete last row when handle remove', () => {
|
||||
mockMessage = { headers: [['', '']] }
|
||||
headers = TestUtils.renderIntoDocument(<Headers onChange={changeFn} message={mockMessage}/>)
|
||||
headers = TestUtils.renderIntoDocument(<KeyValueListEditor onChange={changeFn} message={mockMessage}/>)
|
||||
headers.onChange(0, 0, '')
|
||||
expect(changeFn).toBeCalledWith([['Name', 'Value']])
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
jest.mock('../../../components/ContentView', () => () => null)
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import {ErrorView, Request, Response} from '../../../components/FlowView/Messages'
|
||||
import {ErrorView, Request, Response} from '../../../components/FlowView/HttpMessages'
|
||||
import {Provider} from 'react-redux'
|
||||
import {TFlow, TStore} from '../../ducks/tutils'
|
||||
import {updateEdit} from '../../../ducks/ui/flow'
|
||||
import {parseUrl} from '../../../flow/utils'
|
||||
import ContentView from '../../../components/ContentView'
|
||||
import ContentViewOptions from '../../../components/ContentView/ContentViewOptions'
|
||||
import Headers from '../../../components/FlowView/Headers'
|
||||
import ValueEditor from '../../../components/ValueEditor/ValueEditor'
|
||||
import KeyValueListEditor from '../../../components/editors/KeyValueListEditor'
|
||||
import ValueEditor from '../../../components/editors/ValueEditor'
|
||||
|
||||
global.fetch = jest.fn()
|
||||
|
||||
@ -59,7 +59,7 @@ describe('Request Component', () => {
|
||||
})
|
||||
|
||||
it('should handle change on flow request header', () => {
|
||||
let headers = provider.root.findAllByType(Headers).filter(headers => headers.props.type === 'headers')[0]
|
||||
let headers = provider.root.findAllByType(KeyValueListEditor).filter(headers => headers.props.type === 'headers')[0]
|
||||
headers.props.onChange('foo')
|
||||
expect(store.getActions()).toEqual([updateEdit({request: {headers: 'foo'}})])
|
||||
})
|
||||
@ -118,7 +118,7 @@ describe('Response Component', () => {
|
||||
})
|
||||
|
||||
it('should handle change on flow response headers', () => {
|
||||
let headers = provider.root.findAllByType(Headers).filter(headers => headers.props.type === 'headers')[0]
|
||||
let headers = provider.root.findAllByType(KeyValueListEditor).filter(headers => headers.props.type === 'headers')[0]
|
||||
headers.props.onChange('foo')
|
||||
expect(store.getActions()).toEqual([updateEdit({response: {headers: 'foo'}})])
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import Nav, { NavAction } from '../../../components/FlowView/Nav'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import ToggleEdit from '../../../components/FlowView/ToggleEdit'
|
||||
import {TFlow} from '../../ducks/tutils'
|
||||
import {render} from "../../test-utils"
|
||||
|
@ -19,7 +19,7 @@ exports[`HeaderEditor Component should render correctly 1`] = `
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Headers Component should handle correctly 1`] = `
|
||||
exports[`KeyValueListEditor Component should handle correctly 1`] = `
|
||||
<table
|
||||
className="header-table"
|
||||
>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import ConnectionIndicator from '../../../components/Header/ConnectionIndicator'
|
||||
import * as connectionActions from '../../../ducks/connection'
|
||||
import {render} from "../../test-utils"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import FileMenu from '../../../components/Header/FileMenu'
|
||||
import {Provider} from "react-redux";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import FilterDocs from '../../../components/Header/FilterDocs'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import FilterInput from '../../../components/Header/FilterInput'
|
||||
import FilterDocs from '../../../components/Header/FilterDocs'
|
||||
|
@ -1,6 +1,6 @@
|
||||
jest.mock('../../../flow/utils')
|
||||
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import FlowMenu from '../../../components/Header/FlowMenu'
|
||||
import { TFlow, TStore }from '../../ducks/tutils'
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import MainMenu from '../../../components/Header/MainMenu'
|
||||
import * as React from "react"
|
||||
import StartMenu from '../../../components/Header/StartMenu'
|
||||
import {render} from "../../test-utils"
|
||||
|
||||
test("MainMenu", () => {
|
||||
const {asFragment} = render(<MainMenu/>);
|
||||
const {asFragment} = render(<StartMenu/>);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import {EventlogToggle, MenuToggle, OptionsToggle} from '../../../components/Header/MenuToggle'
|
||||
import {Provider} from 'react-redux'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import { Provider } from 'react-redux'
|
||||
import OptionMenu from '../../../components/Header/OptionMenu'
|
||||
|
@ -1,6 +1,104 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MainMenu 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="main-menu"
|
||||
>
|
||||
<div
|
||||
class="menu-group"
|
||||
>
|
||||
<div
|
||||
class="menu-content"
|
||||
>
|
||||
<div
|
||||
class="filter-input input-group"
|
||||
>
|
||||
<span
|
||||
class="input-group-addon"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-search"
|
||||
style="color: black;"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Search"
|
||||
type="text"
|
||||
value="~d address"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="filter-input input-group"
|
||||
>
|
||||
<span
|
||||
class="input-group-addon"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-tag"
|
||||
style="color: rgb(0, 0, 0);"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Highlight"
|
||||
type="text"
|
||||
value="~u /path"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-legend"
|
||||
>
|
||||
Find
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-group"
|
||||
>
|
||||
<div
|
||||
class="menu-content"
|
||||
>
|
||||
<div
|
||||
class="filter-input input-group"
|
||||
>
|
||||
<span
|
||||
class="input-group-addon"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-pause"
|
||||
style="color: rgb(68, 68, 68);"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="Intercept"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="btn-sm btn btn-default"
|
||||
title="[a]ccept all"
|
||||
>
|
||||
<i
|
||||
class="fa fa-forward text-success"
|
||||
/>
|
||||
Resume All
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="menu-legend"
|
||||
>
|
||||
Intercept
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`StartMenu 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="main-menu"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import Modal from '../../../components/Modal/Modal'
|
||||
import {render} from "../../test-utils"
|
||||
import {setActiveModal} from "../../../ducks/ui/modal";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import { PureOptionDefault } from '../../../components/Modal/OptionModal'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import { Options, ChoicesOption } from '../../../components/Modal/Option'
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import ValidateEditor from '../../../components/ValueEditor/ValidateEditor'
|
||||
import ValidateEditor from '../../../components/editors/ValidateEditor'
|
||||
|
||||
describe('ValidateEditor Component', () => {
|
||||
let validateFn = jest.fn( content => content.length == 3),
|
||||
@ -9,14 +9,14 @@ describe('ValidateEditor Component', () => {
|
||||
|
||||
it('should render correctly', () => {
|
||||
let validateEditor = renderer.create(
|
||||
<ValidateEditor content="foo" onDone={doneFn} isValid={validateFn}/>
|
||||
<ValidateEditor content="foo" onEditDone={doneFn} isValid={validateFn}/>
|
||||
),
|
||||
tree = validateEditor.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
let validateEditor = TestUtils.renderIntoDocument(
|
||||
<ValidateEditor content="foo" onDone={doneFn} isValid={validateFn}/>
|
||||
<ValidateEditor content="foo" onEditDone={doneFn} isValid={validateFn}/>
|
||||
)
|
||||
it('should handle componentWillReceiveProps', () => {
|
||||
let mockProps = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import TestUtils from 'react-dom/test-utils'
|
||||
import ValueEditor from '../../../components/ValueEditor/ValueEditor'
|
||||
import ValueEditor from '../../../components/editors/ValueEditor'
|
||||
import { Key } from '../../../utils'
|
||||
|
||||
describe('ValueEditor Component', () => {
|
||||
@ -9,14 +9,14 @@ describe('ValueEditor Component', () => {
|
||||
let mockFn = jest.fn()
|
||||
it ('should render correctly', () => {
|
||||
let valueEditor = renderer.create(
|
||||
<ValueEditor content="foo" onDone={mockFn}/>
|
||||
<ValueEditor content="foo" onEditDone={mockFn}/>
|
||||
),
|
||||
tree = valueEditor.toJSON()
|
||||
expect(tree).toMatchSnapshot()
|
||||
})
|
||||
|
||||
let valueEditor = TestUtils.renderIntoDocument(
|
||||
<ValueEditor content="<script>foo</script>" onDone={mockFn}/>
|
||||
<ValueEditor content="<script>foo</script>" onEditDone={mockFn}/>
|
||||
)
|
||||
it('should handle this.blur', () => {
|
||||
valueEditor.input.blur = jest.fn()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import Button from '../../../components/common/Button'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import DocsLink from '../../../components/common/DocsLink'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react"
|
||||
import * as React from "react";
|
||||
import Dropdown, {Divider, MenuItem, SubMenu} from '../../../components/common/Dropdown'
|
||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import FileChooser from '../../../components/common/FileChooser'
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import ReactDOM from 'react-dom'
|
||||
import renderer from 'react-test-renderer'
|
||||
import Splitter from '../../../components/common/Splitter'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import ToggleButton from '../../../components/common/ToggleButton'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import renderer from 'react-test-renderer'
|
||||
import ToggleInputButton from '../../../components/common/ToggleInputButton'
|
||||
import { Key } from '../../../utils'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react"
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom"
|
||||
import AutoScroll from '../../../components/helpers/AutoScroll'
|
||||
import { calcVScroll } from '../../../components/helpers/VirtualScroll'
|
||||
|
@ -1,12 +1,12 @@
|
||||
import reduceEventLog, * as eventLogActions from '../../ducks/eventLog'
|
||||
import reduceStore from '../../ducks/utils/store'
|
||||
import reduce from '../../ducks/utils/store'
|
||||
|
||||
describe('event log reducer', () => {
|
||||
it('should return initial state', () => {
|
||||
expect(reduceEventLog(undefined, {})).toEqual({
|
||||
visible: false,
|
||||
filters: { debug: false, info: true, web: true, warn: true, error: true },
|
||||
...reduceStore(undefined, {}),
|
||||
...reduce(undefined, {}),
|
||||
})
|
||||
})
|
||||
|
||||
@ -15,7 +15,7 @@ describe('event log reducer', () => {
|
||||
expect(reduceEventLog(state, eventLogActions.toggleFilter('info'))).toEqual({
|
||||
visible: false,
|
||||
filters: { ...state.filters, info: false},
|
||||
...reduceStore(state, {})
|
||||
...reduce(state, {})
|
||||
})
|
||||
})
|
||||
|
||||
@ -24,7 +24,7 @@ describe('event log reducer', () => {
|
||||
expect(reduceEventLog(state, eventLogActions.toggleVisibility())).toEqual({
|
||||
visible: true,
|
||||
filters: {...state.filters},
|
||||
...reduceStore(undefined, {})
|
||||
...reduce(undefined, {})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -2,7 +2,7 @@ jest.mock('../../utils')
|
||||
|
||||
import reduceFlows from "../../ducks/flows"
|
||||
import * as flowActions from "../../ducks/flows"
|
||||
import reduceStore from "../../ducks/utils/store"
|
||||
import reduce from "../../ducks/utils/store"
|
||||
import { fetchApi } from "../../utils"
|
||||
import { createStore } from "./tutils"
|
||||
|
||||
@ -18,7 +18,7 @@ describe('flow reducer', () => {
|
||||
filter: null,
|
||||
sort: { column: null, desc: false },
|
||||
selected: [],
|
||||
...reduceStore(undefined, {})
|
||||
...reduce(undefined, {})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import reduceStore, * as storeActions from '../../../ducks/utils/store'
|
||||
import reduce, * as storeActions from '../../../ducks/utils/store'
|
||||
|
||||
describe('store reducer', () => {
|
||||
it('should return initial state', () => {
|
||||
expect(reduceStore(undefined, {})).toEqual({
|
||||
expect(reduce(undefined, {})).toEqual({
|
||||
byId: {},
|
||||
list: [],
|
||||
listIndex: {},
|
||||
@ -14,8 +14,8 @@ describe('store reducer', () => {
|
||||
it('should handle add action', () => {
|
||||
let a = {id: 1},
|
||||
b = {id: 9},
|
||||
state = reduceStore(undefined, {})
|
||||
expect(state = reduceStore(state, storeActions.add(a))).toEqual({
|
||||
state = reduce(undefined, {})
|
||||
expect(state = reduce(state, storeActions.add(a))).toEqual({
|
||||
byId: { 1: a },
|
||||
listIndex: { 1: 0 },
|
||||
list: [ a ],
|
||||
@ -23,7 +23,7 @@ describe('store reducer', () => {
|
||||
viewIndex: { 1: 0 },
|
||||
})
|
||||
|
||||
expect(state = reduceStore(state, storeActions.add(b))).toEqual({
|
||||
expect(state = reduce(state, storeActions.add(b))).toEqual({
|
||||
byId: { 1: a, 9: b },
|
||||
listIndex: { 1: 0, 9: 1 },
|
||||
list: [ a, b ],
|
||||
@ -33,7 +33,7 @@ describe('store reducer', () => {
|
||||
|
||||
// add item and sort them
|
||||
let c = {id: 0}
|
||||
expect(reduceStore(state, storeActions.add(c, undefined,
|
||||
expect(reduce(state, storeActions.add(c, undefined,
|
||||
(a, b) => {return a.id - b.id}))).toEqual({
|
||||
byId: {...state.byId, 0: c },
|
||||
list: [...state.list, c ],
|
||||
@ -46,15 +46,15 @@ describe('store reducer', () => {
|
||||
|
||||
it('should not add the item with duplicated id', () => {
|
||||
let a = {id: 1},
|
||||
state = reduceStore(undefined, storeActions.add(a))
|
||||
expect(reduceStore(state, storeActions.add(a))).toEqual(state)
|
||||
state = reduce(undefined, storeActions.add(a))
|
||||
expect(reduce(state, storeActions.add(a))).toEqual(state)
|
||||
})
|
||||
|
||||
it('should handle update action', () => {
|
||||
let a = {id: 1, foo: "foo"},
|
||||
updated = {...a, foo: "bar"},
|
||||
state = reduceStore(undefined, storeActions.add(a))
|
||||
expect(reduceStore(state, storeActions.update(updated))).toEqual({
|
||||
state = reduce(undefined, storeActions.add(a))
|
||||
expect(reduce(state, storeActions.update(updated))).toEqual({
|
||||
byId: { 1: updated },
|
||||
list: [ updated ],
|
||||
listIndex: { 1: 0 },
|
||||
@ -65,8 +65,8 @@ describe('store reducer', () => {
|
||||
|
||||
it('should handle update action with filter', () => {
|
||||
let a = {id: 0}, b = {id: 1},
|
||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
||||
state = reduceStore(state, storeActions.update(b,
|
||||
state = reduce(undefined, storeActions.receive([a, b]))
|
||||
state = reduce(state, storeActions.update(b,
|
||||
item => {return item.id != 1}))
|
||||
expect(state).toEqual({
|
||||
byId: { 0: a, 1: b },
|
||||
@ -75,7 +75,7 @@ describe('store reducer', () => {
|
||||
view: [ a ],
|
||||
viewIndex: { 0: 0 }
|
||||
})
|
||||
expect(reduceStore(state, storeActions.update(b,
|
||||
expect(reduce(state, storeActions.update(b,
|
||||
item => {return item.id != 0}))).toEqual({
|
||||
byId: { 0: a, 1: b },
|
||||
list: [ a, b ],
|
||||
@ -88,8 +88,8 @@ describe('store reducer', () => {
|
||||
it('should handle update action with sort', () => {
|
||||
let a = {id: 2},
|
||||
b = {id: 3},
|
||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
||||
expect(reduceStore(state, storeActions.update(b, undefined,
|
||||
state = reduce(undefined, storeActions.receive([a, b]))
|
||||
expect(reduce(state, storeActions.update(b, undefined,
|
||||
(a, b) => {return b.id - a.id}))).toEqual({
|
||||
// sort by id in descending order
|
||||
byId: { 2: a, 3: b },
|
||||
@ -99,8 +99,8 @@ describe('store reducer', () => {
|
||||
viewIndex: { 2: 1, 3: 0 },
|
||||
})
|
||||
|
||||
let state1 = reduceStore(undefined, storeActions.receive([b, a]))
|
||||
expect(reduceStore(state1, storeActions.update(b, undefined,
|
||||
let state1 = reduce(undefined, storeActions.receive([b, a]))
|
||||
expect(reduce(state1, storeActions.update(b, undefined,
|
||||
(a, b) => {return a.id - b.id}))).toEqual({
|
||||
// sort by id in ascending order
|
||||
byId: { 2: a, 3: b },
|
||||
@ -114,8 +114,8 @@ describe('store reducer', () => {
|
||||
it('should set filter', () => {
|
||||
let a = { id: 1 },
|
||||
b = { id: 2 },
|
||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
||||
expect(reduceStore(state, storeActions.setFilter(
|
||||
state = reduce(undefined, storeActions.receive([a, b]))
|
||||
expect(reduce(state, storeActions.setFilter(
|
||||
item => {return item.id != 1}
|
||||
))).toEqual({
|
||||
byId: { 1 :a, 2: b },
|
||||
@ -129,8 +129,8 @@ describe('store reducer', () => {
|
||||
it('should set sort', () => {
|
||||
let a = { id: 1 },
|
||||
b = { id: 2 },
|
||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
||||
expect(reduceStore(state, storeActions.setSort(
|
||||
state = reduce(undefined, storeActions.receive([a, b]))
|
||||
expect(reduce(state, storeActions.setSort(
|
||||
(a, b) => { return b.id - a.id }
|
||||
))).toEqual({
|
||||
byId: { 1: a, 2: b },
|
||||
@ -143,8 +143,8 @@ describe('store reducer', () => {
|
||||
|
||||
it('should handle remove action', () => {
|
||||
let a = { id: 1 }, b = { id: 2},
|
||||
state = reduceStore(undefined, storeActions.receive([a, b]))
|
||||
expect(reduceStore(state, storeActions.remove(1))).toEqual({
|
||||
state = reduce(undefined, storeActions.receive([a, b]))
|
||||
expect(reduce(state, storeActions.remove(1))).toEqual({
|
||||
byId: { 2: b },
|
||||
list: [ b ],
|
||||
listIndex: { 2: 0 },
|
||||
@ -152,13 +152,13 @@ describe('store reducer', () => {
|
||||
viewIndex: { 2: 0 },
|
||||
})
|
||||
|
||||
expect(reduceStore(state, storeActions.remove(3))).toEqual(state)
|
||||
expect(reduce(state, storeActions.remove(3))).toEqual(state)
|
||||
})
|
||||
|
||||
it('should handle receive list', () => {
|
||||
let a = { id: 1 }, b = { id: 2 },
|
||||
list = [ a, b ]
|
||||
expect(reduceStore(undefined, storeActions.receive(list))).toEqual({
|
||||
expect(reduce(undefined, storeActions.receive(list))).toEqual({
|
||||
byId: { 1: a, 2: b },
|
||||
list: [ a, b ],
|
||||
listIndex: {1: 0, 2: 1},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import {render as rtlRender} from '@testing-library/react'
|
||||
import {Provider} from 'react-redux'
|
||||
// Import your own reducer
|
||||
|
@ -71,20 +71,3 @@ describe('getDiff', () => {
|
||||
expect(utils.getDiff(obj1, obj2)).toEqual({ b: {foo: 2}, c:[4]})
|
||||
})
|
||||
})
|
||||
|
||||
describe('pure', () => {
|
||||
let tFunc = function({ className }) {
|
||||
return (<p className={ className }>foo</p>)
|
||||
},
|
||||
puredFunc = utils.pure(tFunc),
|
||||
f = new puredFunc('bar')
|
||||
|
||||
it('should display function name', () => {
|
||||
expect(utils.pure(tFunc).displayName).toEqual('tFunc')
|
||||
})
|
||||
|
||||
it('should render properties', () => {
|
||||
expect(f.render()).toEqual(tFunc('bar'))
|
||||
})
|
||||
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import {render} from 'react-dom'
|
||||
import {Provider} from 'react-redux'
|
||||
|
||||
@ -11,14 +11,17 @@ import {store} from "./ducks";
|
||||
|
||||
|
||||
useUrlState(store)
|
||||
// @ts-ignore
|
||||
if (window.MITMWEB_STATIC) {
|
||||
// @ts-ignore
|
||||
window.backend = new StaticBackend(store)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
window.backend = new WebSocketBackend(store)
|
||||
}
|
||||
|
||||
window.addEventListener('error', msg => {
|
||||
store.dispatch(addLog(msg))
|
||||
window.addEventListener('error', (e: ErrorEvent) => {
|
||||
store.dispatch(addLog(`${e.message}\n${e.error.stack}`))
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
@ -37,8 +37,8 @@ export function Results({results}: ResultProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (resultElement) {
|
||||
resultElement.current.addEventListener('DOMNodeInserted', event => {
|
||||
const { currentTarget: target } = event;
|
||||
resultElement.current.addEventListener('DOMNodeInserted', (event) => {
|
||||
const target = event.currentTarget as Element;
|
||||
target.scroll({ top: target.scrollHeight, behavior: 'auto' });
|
||||
});
|
||||
}
|
||||
@ -99,7 +99,7 @@ export default function CommandBar() {
|
||||
.then(data => {
|
||||
setAllCommands(data["commands"])
|
||||
setCompletionCandidate(getAvailableCommands(data["commands"]))
|
||||
setAvailableCommands(Object.keys(data["commands"]))
|
||||
setAvailableCommands(Object.keys(data))
|
||||
})
|
||||
}, [])
|
||||
|
||||
@ -213,4 +213,4 @@ export default function CommandBar() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,59 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Edit, ViewServer, ViewImage } from './ContentView/ContentViews'
|
||||
import * as MetaViews from './ContentView/MetaViews'
|
||||
import ShowFullContentButton from './ContentView/ShowFullContentButton'
|
||||
|
||||
|
||||
import { displayLarge, updateEdit } from '../ducks/ui/flow'
|
||||
|
||||
ContentView.propTypes = {
|
||||
// It may seem a bit weird at the first glance:
|
||||
// Every view takes the flow and the message as props, e.g.
|
||||
// <Auto flow={flow} message={flow.request}/>
|
||||
flow: PropTypes.object.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ViewImage.matches(msg) ? 10 : 0.2)
|
||||
|
||||
function ContentView(props) {
|
||||
const { flow, message, contentView, isDisplayLarge, displayLarge, onContentChange, readonly } = props
|
||||
|
||||
if (message.contentLength === 0 && readonly) {
|
||||
return <MetaViews.ContentEmpty {...props}/>
|
||||
}
|
||||
|
||||
if (message.contentLength === null && readonly) {
|
||||
return <MetaViews.ContentMissing {...props}/>
|
||||
}
|
||||
|
||||
if (!isDisplayLarge && ContentView.isContentTooLarge(message)) {
|
||||
return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/>
|
||||
}
|
||||
|
||||
let view;
|
||||
if(contentView === "Edit") {
|
||||
view = <Edit flow={flow} message={message} onChange={onContentChange}/>
|
||||
} else {
|
||||
view = <ViewServer flow={flow} message={message} contentView={contentView}/>
|
||||
}
|
||||
return (
|
||||
<div className="contentview">
|
||||
{view}
|
||||
<ShowFullContentButton/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
contentView: state.ui.flow.contentView,
|
||||
isDisplayLarge: state.ui.flow.displayLarge,
|
||||
}),
|
||||
{
|
||||
displayLarge,
|
||||
updateEdit
|
||||
}
|
||||
)(ContentView)
|
@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import CodeMirror from "../../contrib/CodeMirror"
|
||||
|
||||
|
||||
type CodeEditorProps = {
|
||||
content: string,
|
||||
onChange: Function,
|
||||
}
|
||||
|
||||
export default function CodeEditor ( { content, onChange}: CodeEditorProps ){
|
||||
|
||||
let options = {
|
||||
lineNumbers: true
|
||||
};
|
||||
return (
|
||||
<div className="codeeditor" onKeyDown={e => e.stopPropagation()}>
|
||||
<CodeMirror value={content} onChange={onChange} options={options}/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
import React from 'react'
|
||||
import { MessageUtils } from '../../flow/utils'
|
||||
import { Flow, HTTPMessage } from '../../flow'
|
||||
|
||||
type ContentLoaderProps = {
|
||||
content: string,
|
||||
contentView: object,
|
||||
flow: Flow,
|
||||
message: HTTPMessage,
|
||||
}
|
||||
|
||||
type ContentLoaderStates = {
|
||||
content: string | undefined,
|
||||
request: { abort: () => void }| undefined,
|
||||
}
|
||||
|
||||
export default function withContentLoader(View) {
|
||||
|
||||
return class extends React.Component<ContentLoaderProps, ContentLoaderStates> {
|
||||
static displayName: string = View.displayName || View.name
|
||||
static matches: (message: any) => boolean = View.matches
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
content: undefined,
|
||||
request: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateContent(this.props)
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
nextProps.message.content !== this.props.message.content ||
|
||||
nextProps.message.contentHash !== this.props.message.contentHash ||
|
||||
nextProps.contentView !== this.props.contentView
|
||||
) {
|
||||
this.updateContent(nextProps)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.request) {
|
||||
this.state.request.abort()
|
||||
}
|
||||
}
|
||||
|
||||
updateContent(props) {
|
||||
if (this.state.request) {
|
||||
this.state.request.abort()
|
||||
}
|
||||
// We have a few special cases where we do not need to make an HTTP request.
|
||||
if (props.message.content !== undefined) {
|
||||
return this.setState({request: undefined, content: props.message.content})
|
||||
}
|
||||
if (props.message.contentLength === 0) {
|
||||
return this.setState({request: undefined, content: ""})
|
||||
}
|
||||
|
||||
let requestUrl = MessageUtils.getContentURL(props.flow, props.message, props.contentView)
|
||||
|
||||
// We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable.
|
||||
let request = new XMLHttpRequest();
|
||||
request.addEventListener("load", this.requestComplete.bind(this, request));
|
||||
request.addEventListener("error", this.requestFailed.bind(this, request));
|
||||
request.open("GET", requestUrl);
|
||||
request.send();
|
||||
this.setState({request, content: undefined})
|
||||
}
|
||||
|
||||
requestComplete(request, e) {
|
||||
if (request !== this.state.request) {
|
||||
return // Stale request
|
||||
}
|
||||
this.setState({
|
||||
content: request.responseText,
|
||||
request: undefined
|
||||
})
|
||||
}
|
||||
|
||||
requestFailed(request, e) {
|
||||
if (request !== this.state.request) {
|
||||
return // Stale request
|
||||
}
|
||||
console.error(e)
|
||||
// FIXME: Better error handling
|
||||
this.setState({
|
||||
content: "Error getting content.",
|
||||
request: undefined
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.content !== undefined ? (
|
||||
<View content={this.state.content} {...this.props}/>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<i className="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
@ -1,28 +0,0 @@
|
||||
import React from 'react'
|
||||
import ViewSelector from './ViewSelector'
|
||||
import UploadContentButton from './UploadContentButton'
|
||||
import DownloadContentButton from './DownloadContentButton'
|
||||
import { useAppSelector } from "../../ducks";
|
||||
import { Flow, HTTPMessage } from '../../flow'
|
||||
|
||||
type ContentViewOptionsProps = {
|
||||
flow: Flow,
|
||||
message: HTTPMessage,
|
||||
uploadContent: (content: string) => Promise<Response>,
|
||||
}
|
||||
|
||||
export default function ContentViewOptions({ flow, message, uploadContent }: ContentViewOptionsProps) {
|
||||
const contentViewDescription = useAppSelector(state => state.ui.flow.viewDescription)
|
||||
const readonly = useAppSelector(state => state.ui.flow.modifiedFlow);
|
||||
return (
|
||||
<div className="view-options">
|
||||
{readonly ? <ViewSelector /> : <span><b>View:</b> edit</span>}
|
||||
|
||||
<DownloadContentButton flow={flow} message={message}/>
|
||||
|
||||
{!readonly && <UploadContentButton uploadContent={uploadContent}/> }
|
||||
|
||||
{readonly && <span>{contentViewDescription}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { setContentViewDescription, setContent } from '../../ducks/ui/flow'
|
||||
import withContentLoader from './ContentLoader'
|
||||
import { MessageUtils } from '../../flow/utils'
|
||||
import CodeEditor from './CodeEditor'
|
||||
import { useAppDispatch, useAppSelector } from "../../ducks";
|
||||
|
||||
|
||||
const isImage = /^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i
|
||||
ViewImage.matches = msg => isImage.test(MessageUtils.getContentType(msg))
|
||||
|
||||
type ViewImageProps = {
|
||||
flow: object,
|
||||
message: object,
|
||||
}
|
||||
|
||||
function ViewImage({ flow, message }: ViewImageProps) {
|
||||
return (
|
||||
<div className="flowview-image">
|
||||
<img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type EditProps = {
|
||||
content: string,
|
||||
onChange: (content: string) => any,
|
||||
}
|
||||
|
||||
function PureEdit({ content, onChange }: EditProps) {
|
||||
return <CodeEditor content={content} onChange={onChange}/>
|
||||
}
|
||||
const Edit = withContentLoader(PureEdit)
|
||||
|
||||
type PureViewServerProps = {
|
||||
flow: object,
|
||||
message: object,
|
||||
content: string,
|
||||
}
|
||||
|
||||
type PureViewServerStates = {
|
||||
lines: [style: string, text: string][][],
|
||||
description: string,
|
||||
}
|
||||
|
||||
export function PureViewServer({flow, message, content}: PureViewServerProps) {
|
||||
const [data, setData] = useState<PureViewServerStates>({
|
||||
lines: [],
|
||||
description: "",
|
||||
})
|
||||
|
||||
const dispatch = useAppDispatch(),
|
||||
showFullContent: boolean = useAppSelector(state => state.ui.flow.showFullContent),
|
||||
maxLines: number = useAppSelector(state => state.ui.flow.maxContentLines)
|
||||
|
||||
let lines = showFullContent ? data.lines : data.lines?.slice(0, maxLines)
|
||||
|
||||
useEffect(() => {
|
||||
setContentView({flow, message, content})
|
||||
}, [flow, message, content])
|
||||
|
||||
const setContentView = (props) => {
|
||||
try {
|
||||
setData(JSON.parse(props.content))
|
||||
}catch(err) {
|
||||
setData({lines: [], description: err.message})
|
||||
}
|
||||
|
||||
dispatch(setContentViewDescription(props.contentView !== data.description ? data.description : ''))
|
||||
dispatch(setContent(data.lines))
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ViewImage.matches(message) && <ViewImage flow={flow} message={message}/>}
|
||||
<pre>
|
||||
{lines.map((line, i) =>
|
||||
<div key={`line${i}`}>
|
||||
{line.map((element, j) => {
|
||||
let [style, text] = element
|
||||
return (
|
||||
<span key={`tuple${j}`} className={style}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
const ViewServer = withContentLoader(PureViewServer)
|
||||
|
||||
export { Edit, ViewServer, ViewImage }
|
@ -1,19 +0,0 @@
|
||||
import React from 'react'
|
||||
import { MessageUtils } from "../../flow/utils"
|
||||
import { Flow, HTTPMessage } from '../../flow'
|
||||
|
||||
type DownloadContentButtonProps = {
|
||||
flow: Flow,
|
||||
message: HTTPMessage,
|
||||
}
|
||||
|
||||
export default function DownloadContentButton({ flow, message }: DownloadContentButtonProps) {
|
||||
|
||||
return (
|
||||
<a className="btn btn-default btn-xs"
|
||||
href={MessageUtils.getContentURL(flow, message)}
|
||||
title="Download the content of the flow.">
|
||||
<i className="fa fa-download"/>
|
||||
</a>
|
||||
)
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import React from 'react'
|
||||
import { formatSize } from '../../utils'
|
||||
import UploadContentButton from './UploadContentButton'
|
||||
import DownloadContentButton from './DownloadContentButton'
|
||||
import { HTTPFlow, HTTPMessage } from '../../flow'
|
||||
|
||||
interface ContentProps {
|
||||
flow: HTTPFlow,
|
||||
message: HTTPMessage,
|
||||
}
|
||||
|
||||
interface ContentTooLargeProps extends ContentProps {
|
||||
onClick: () => void,
|
||||
uploadContent: () => any,
|
||||
}
|
||||
|
||||
|
||||
export function ContentEmpty({ flow, message }: ContentProps) {
|
||||
return (
|
||||
<div className="alert alert-info">
|
||||
No {flow.request === message ? 'request' : 'response'} content.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContentMissing({ flow, message }: ContentProps) {
|
||||
return (
|
||||
<div className="alert alert-info">
|
||||
{flow.request === message ? 'Request' : 'Response'} content missing.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ContentTooLarge({ message, onClick, uploadContent, flow }: ContentTooLargeProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className="alert alert-warning">
|
||||
|
||||
<button onClick={onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
|
||||
{formatSize(message.contentLength)} content size.
|
||||
</div>
|
||||
<div className="view-options text-center">
|
||||
<UploadContentButton uploadContent={uploadContent}/>
|
||||
|
||||
<DownloadContentButton flow={flow} message={message}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
import Button from '../common/Button';
|
||||
import { setShowFullContent } from '../../ducks/ui/flow'
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
|
||||
export default function ShowFullContentButton() {
|
||||
const dispatch = useAppDispatch(),
|
||||
showFullContent = useAppSelector(state => state.ui.flow.showFullContent),
|
||||
visibleLines = useAppSelector(state => state.ui.flow.maxContentLines),
|
||||
contentLines = useAppSelector(state => state.ui.flow.content.length)
|
||||
|
||||
return (
|
||||
!showFullContent ? (
|
||||
<div>
|
||||
<Button className="view-all-content-btn btn-xs" onClick={() => dispatch(setShowFullContent())}>
|
||||
Show full content
|
||||
</Button>
|
||||
<span className="pull-right"> {visibleLines}/{contentLines} are visible </span>
|
||||
</div>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
import React from 'react'
|
||||
import FileChooser from '../common/FileChooser'
|
||||
|
||||
type UploadContentButtonProps = {
|
||||
uploadContent: (content: string) => Promise<Response>,
|
||||
}
|
||||
|
||||
export default function UploadContentButton({ uploadContent }: UploadContentButtonProps) {
|
||||
|
||||
return (
|
||||
<FileChooser
|
||||
icon="fa-upload"
|
||||
title="Upload a file to replace the content."
|
||||
onOpenFile={uploadContent}
|
||||
className="btn btn-default btn-xs"/>
|
||||
)
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
import {setContentView} from '../../ducks/ui/flow';
|
||||
import Dropdown, {MenuItem} from '../common/Dropdown'
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
|
||||
|
||||
export default React.memo(function ViewSelector() {
|
||||
const dispatch = useAppDispatch(),
|
||||
contentViews = useAppSelector(state => state.conf.contentViews || []),
|
||||
activeView = useAppSelector(state => state.ui.flow.contentView);
|
||||
|
||||
let inner = <span><b>View:</b> {activeView.toLowerCase()} <span className="caret"/></span>
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
text={inner}
|
||||
className="btn btn-default btn-xs pull-left"
|
||||
options={{placement: "top-start"}}>
|
||||
{contentViews.map(name =>
|
||||
<MenuItem key={name} onClick={() => dispatch(setContentView(name))}>
|
||||
{name.toLowerCase().replace('_', ' ')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
)
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { connect } from 'react-redux'
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React, {useState} from 'react'
|
||||
import {useDispatch} from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import {RequestUtils, ResponseUtils} from '../../flow/utils'
|
||||
import {fetchApi, formatSize, formatTimeDelta, formatTimeStamp} from '../../utils'
|
||||
import {endTime, getTotalSize, RequestUtils, ResponseUtils, startTime} from '../../flow/utils'
|
||||
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils'
|
||||
import * as flowActions from "../../ducks/flows";
|
||||
import {addInterceptFilter} from "../../ducks/options"
|
||||
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
|
||||
import {Flow} from "../../flow";
|
||||
import {copy} from "../../flow/export";
|
||||
|
||||
|
||||
type FlowColumnProps = {
|
||||
@ -16,38 +17,38 @@ type FlowColumnProps = {
|
||||
interface FlowColumn {
|
||||
(props: FlowColumnProps): JSX.Element;
|
||||
|
||||
headerClass: string;
|
||||
headerName: string;
|
||||
headerName: string; // Shown in the UI
|
||||
sortKey: (flow: Flow) => any;
|
||||
}
|
||||
|
||||
export const TLSColumn: FlowColumn = ({flow}) => {
|
||||
export const tls: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className={classnames('col-tls', flow.client_conn.tls_established ? 'col-tls-https' : 'col-tls-http')}/>
|
||||
)
|
||||
}
|
||||
tls.headerName = ''
|
||||
tls.sortKey = flow => flow.type === "http" && flow.request.scheme
|
||||
|
||||
TLSColumn.headerClass = 'col-tls'
|
||||
TLSColumn.headerName = ''
|
||||
|
||||
export const IconColumn: FlowColumn = ({flow}) => {
|
||||
export const icon: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className="col-icon">
|
||||
<div className={classnames('resource-icon', getIcon(flow))}/>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
IconColumn.headerClass = 'col-icon'
|
||||
IconColumn.headerName = ''
|
||||
icon.headerName = ''
|
||||
icon.sortKey = flow => 0
|
||||
|
||||
const getIcon = (flow: Flow): string => {
|
||||
if (flow.type !== "http" || !flow.response) {
|
||||
return 'resource-icon-plain'
|
||||
}
|
||||
if (flow.websocket) {
|
||||
return 'resource-icon-websocket'
|
||||
}
|
||||
|
||||
var contentType = ResponseUtils.getContentType(flow.response) || ''
|
||||
|
||||
// @todo We should assign a type to the flow somewhere else.
|
||||
if (flow.response.status_code === 304) {
|
||||
return 'resource-icon-not-modified'
|
||||
}
|
||||
@ -70,7 +71,7 @@ const getIcon = (flow: Flow): string => {
|
||||
return 'resource-icon-plain'
|
||||
}
|
||||
|
||||
export const PathColumn: FlowColumn = ({flow}) => {
|
||||
export const path: FlowColumn = ({flow}) => {
|
||||
let err;
|
||||
if (flow.error) {
|
||||
if (flow.error.msg === "Connection killed.") {
|
||||
@ -88,24 +89,23 @@ export const PathColumn: FlowColumn = ({flow}) => {
|
||||
<i className="fa fa-fw fa-pause pull-right"/>
|
||||
)}
|
||||
{err}
|
||||
<span className="marker pull-right">{flow.marked}</span>
|
||||
{flow.type === "http" ? RequestUtils.pretty_url(flow.request) : null}
|
||||
</td>
|
||||
)
|
||||
};
|
||||
path.headerName = 'Path'
|
||||
path.sortKey = flow => flow.type === "http" && RequestUtils.pretty_url(flow.request)
|
||||
|
||||
PathColumn.headerClass = 'col-path'
|
||||
PathColumn.headerName = 'Path'
|
||||
|
||||
export const MethodColumn: FlowColumn = ({flow}) => {
|
||||
export const method: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className="col-method">{flow.type === "http" ? flow.request.method : flow.type.toLowerCase()}</td>
|
||||
)
|
||||
};
|
||||
method.headerName = 'Method'
|
||||
method.sortKey = flow => flow.type === "http" && flow.request.method
|
||||
|
||||
MethodColumn.headerClass = 'col-method'
|
||||
MethodColumn.headerName = 'Method'
|
||||
|
||||
export const StatusColumn: FlowColumn = ({flow}) => {
|
||||
export const status: FlowColumn = ({flow}) => {
|
||||
let color = 'darkred';
|
||||
|
||||
if (flow.type !== "http" || !flow.response)
|
||||
@ -127,75 +127,65 @@ export const StatusColumn: FlowColumn = ({flow}) => {
|
||||
<td className="col-status" style={{color: color}}>{flow.response.status_code}</td>
|
||||
)
|
||||
}
|
||||
status.headerName = 'Status'
|
||||
status.sortKey = flow => flow.type === "http" && flow.response && flow.response.status_code
|
||||
|
||||
StatusColumn.headerClass = 'col-status'
|
||||
StatusColumn.headerName = 'Status'
|
||||
|
||||
export const SizeColumn: FlowColumn = ({flow}) => {
|
||||
export const size: FlowColumn = ({flow}) => {
|
||||
return (
|
||||
<td className="col-size">{formatSize(getTotalSize(flow))}</td>
|
||||
)
|
||||
};
|
||||
size.headerName = 'Size'
|
||||
size.sortKey = flow => getTotalSize(flow)
|
||||
|
||||
const getTotalSize = (flow: Flow): number => {
|
||||
if (flow.type !== "http")
|
||||
return 0
|
||||
let total = flow.request.contentLength
|
||||
if (flow.response) {
|
||||
total += flow.response.contentLength || 0
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
SizeColumn.headerClass = 'col-size'
|
||||
SizeColumn.headerName = 'Size'
|
||||
|
||||
export const TimeColumn: FlowColumn = ({flow}) => {
|
||||
export const time: FlowColumn = ({flow}) => {
|
||||
const start = startTime(flow), end = endTime(flow);
|
||||
return (
|
||||
<td className="col-time">
|
||||
{flow.type === "http" && flow.response?.timestamp_end ? (
|
||||
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
|
||||
{start && end ? (
|
||||
formatTimeDelta(1000 * (end - start))
|
||||
) : (
|
||||
'...'
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
time.headerName = 'Time'
|
||||
time.sortKey = flow => {
|
||||
const start = startTime(flow), end = endTime(flow);
|
||||
return start && end && end - start;
|
||||
}
|
||||
|
||||
TimeColumn.headerClass = 'col-time'
|
||||
TimeColumn.headerName = 'Time'
|
||||
|
||||
export const TimeStampColumn: FlowColumn = ({flow}) => {
|
||||
export const timestamp: FlowColumn = ({flow}) => {
|
||||
const start = startTime(flow);
|
||||
return (
|
||||
<td className="col-start">
|
||||
{flow.type === "http" && flow.request.timestamp_start ? (
|
||||
formatTimeStamp(flow.request.timestamp_start)
|
||||
{start ? (
|
||||
formatTimeStamp(start)
|
||||
) : (
|
||||
'...'
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
timestamp.headerName = 'Start time'
|
||||
timestamp.sortKey = flow => startTime(flow)
|
||||
|
||||
TimeStampColumn.headerClass = 'col-timestamp'
|
||||
TimeStampColumn.headerName = 'TimeStamp'
|
||||
const markers = {
|
||||
":red_circle:": "🔴",
|
||||
":orange_circle:": "🟠",
|
||||
":yellow_circle:": "🟡",
|
||||
":green_circle:": "🟢",
|
||||
":large_blue_circle:": "🔵",
|
||||
":purple_circle:": "🟣",
|
||||
":brown_circle:": "🟤",
|
||||
}
|
||||
|
||||
export const QuickActionsColumn: FlowColumn = ({flow}) => {
|
||||
export const quickactions: FlowColumn = ({flow}) => {
|
||||
const dispatch = useDispatch()
|
||||
let [open, setOpen] = useState(false)
|
||||
|
||||
const copy = (format: string) => {
|
||||
if (!flow) {
|
||||
return
|
||||
}
|
||||
|
||||
fetchApi(`/flows/${flow.id}/export/${format}.json`, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
navigator.clipboard.writeText(data.export)
|
||||
})
|
||||
}
|
||||
|
||||
let resume_or_replay: React.ReactNode | null = null;
|
||||
if (flow.intercepted) {
|
||||
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
|
||||
@ -221,11 +211,21 @@ export const QuickActionsColumn: FlowColumn = ({flow}) => {
|
||||
onOpen={setOpen}
|
||||
options={{placement: "bottom-end"}}>
|
||||
<SubMenu title="Copy...">
|
||||
<MenuItem onClick={() => copy("raw_request")}>Copy raw request</MenuItem>
|
||||
<MenuItem onClick={() => copy("raw_response")}>Copy raw response</MenuItem>
|
||||
<MenuItem onClick={() => copy("raw")}>Copy raw request and response</MenuItem>
|
||||
<MenuItem onClick={() => copy("curl")}>Copy as cURL</MenuItem>
|
||||
<MenuItem onClick={() => copy("httpie")}>Copy as HTTPie</MenuItem>
|
||||
<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}`)}>
|
||||
@ -260,21 +260,5 @@ export const QuickActionsColumn: FlowColumn = ({flow}) => {
|
||||
)
|
||||
}
|
||||
|
||||
QuickActionsColumn.headerClass = 'col-quickactions'
|
||||
QuickActionsColumn.headerName = ''
|
||||
|
||||
|
||||
export const columns: { [key: string]: FlowColumn } = {};
|
||||
for (let col of [
|
||||
TLSColumn,
|
||||
IconColumn,
|
||||
PathColumn,
|
||||
MethodColumn,
|
||||
StatusColumn,
|
||||
TimeStampColumn,
|
||||
SizeColumn,
|
||||
TimeColumn,
|
||||
QuickActionsColumn,
|
||||
]) {
|
||||
columns[col.name.replace(/Column$/, "").toLowerCase()] = col;
|
||||
}
|
||||
quickactions.headerName = ''
|
||||
quickactions.sortKey = flow => 0;
|
||||
|
@ -3,7 +3,7 @@ import classnames from 'classnames'
|
||||
import {Flow} from "../../flow";
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
import {select} from '../../ducks/flows'
|
||||
import {columns, QuickActionsColumn} from "./FlowColumns";
|
||||
import * as columns from "./FlowColumns";
|
||||
|
||||
type FlowRowProps = {
|
||||
flow: Flow
|
||||
@ -33,7 +33,7 @@ export default React.memo(function FlowRow({flow, selected, highlighted}: FlowRo
|
||||
dispatch(select(flow.id));
|
||||
}, [flow]);
|
||||
|
||||
const displayColumns = displayColumnNames.map(x => columns[x]).concat(QuickActionsColumn);
|
||||
const displayColumns = displayColumnNames.map(x => columns[x]).filter(x => x).concat(columns.quickactions);
|
||||
|
||||
return (
|
||||
<tr className={className} onClick={onClick}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import classnames from 'classnames'
|
||||
import {columns, QuickActionsColumn} from './FlowColumns'
|
||||
import * as columns from './FlowColumns'
|
||||
|
||||
import {setSort} from '../../ducks/flows'
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
@ -12,12 +12,12 @@ export default React.memo(function FlowTableHead() {
|
||||
displayColumnNames = useAppSelector(state => state.options.web_columns);
|
||||
|
||||
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
|
||||
const displayColumns = displayColumnNames.map(x => columns[x]).concat(QuickActionsColumn);
|
||||
const displayColumns = displayColumnNames.map(x => columns[x]).filter(x => x).concat(columns.quickactions);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
{displayColumns.map(Column => (
|
||||
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
|
||||
<th className={classnames(`col-${Column.name}`, sortColumn === Column.name && sortType)}
|
||||
key={Column.name}
|
||||
onClick={() => dispatch(setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc))}>
|
||||
{Column.headerName}
|
||||
|
@ -1,44 +1,66 @@
|
||||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import * as React from "react"
|
||||
import {FunctionComponent} from "react"
|
||||
import {Request, Response} from './FlowView/HttpMessages'
|
||||
import Connection from './FlowView/Connection'
|
||||
import Error from "./FlowView/Error"
|
||||
import Timing from "./FlowView/Timing"
|
||||
import WebSocket from "./FlowView/WebSocket"
|
||||
|
||||
import Nav from './FlowView/Nav'
|
||||
import { ErrorView as Error, Request, Response } from './FlowView/Messages'
|
||||
import Details from './FlowView/Details'
|
||||
|
||||
import { selectTab } from '../ducks/ui/flow'
|
||||
import {selectTab} from '../ducks/ui/flow'
|
||||
import {useAppDispatch, useAppSelector} from "../ducks";
|
||||
import {Flow} from "../flow";
|
||||
import classnames from "classnames";
|
||||
|
||||
export const allTabs = { Request, Response, Error, Details }
|
||||
type TabProps = {
|
||||
flow: Flow
|
||||
}
|
||||
|
||||
export const allTabs: { [name: string]: FunctionComponent<TabProps> } = {
|
||||
request: Request,
|
||||
response: Response,
|
||||
error: Error,
|
||||
connection: Connection,
|
||||
timing: Timing,
|
||||
websocket: WebSocket
|
||||
}
|
||||
|
||||
export function tabsForFlow(flow: Flow): string[] {
|
||||
const tabs = ['request', 'response', 'websocket', 'error'].filter(k => flow[k])
|
||||
tabs.push("connection")
|
||||
tabs.push("timing")
|
||||
return tabs;
|
||||
}
|
||||
|
||||
export default function FlowView() {
|
||||
const dispatch = useAppDispatch(),
|
||||
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
|
||||
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]),
|
||||
tabs = tabsForFlow(flow);
|
||||
|
||||
let tabName = useAppSelector(state => state.ui.flow.tab)
|
||||
|
||||
// only display available tab names
|
||||
const tabs = ['request', 'response', 'error'].filter(k => flow[k])
|
||||
tabs.push("details")
|
||||
|
||||
if (tabs.indexOf(tabName) < 0) {
|
||||
if (tabName === 'response' && flow.error) {
|
||||
tabName = 'error'
|
||||
} else if (tabName === 'error' && flow.response) {
|
||||
tabName = 'response'
|
||||
let active = useAppSelector(state => state.ui.flow.tab)
|
||||
if (tabs.indexOf(active) < 0) {
|
||||
if (active === 'response' && flow.error) {
|
||||
active = 'error'
|
||||
} else if (active === 'error' && "response" in flow) {
|
||||
active = 'response'
|
||||
} else {
|
||||
tabName = tabs[0]
|
||||
active = tabs[0]
|
||||
}
|
||||
}
|
||||
|
||||
const Tab = allTabs[_.capitalize(tabName)]
|
||||
const Tab = allTabs[active];
|
||||
|
||||
return (
|
||||
<div className="flow-detail">
|
||||
<Nav
|
||||
tabs={tabs}
|
||||
active={tabName}
|
||||
onSelectTab={(tab: string) => dispatch(selectTab(tab))}
|
||||
/>
|
||||
<nav className="nav-tabs nav-tabs-sm">
|
||||
{tabs.map(tabId => (
|
||||
<a key={tabId} href="#" className={classnames({active: active === tabId})}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
dispatch(selectTab(tabId))
|
||||
}}>
|
||||
{allTabs[tabId].name}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<Tab flow={flow}/>
|
||||
</div>
|
||||
)
|
||||
|
151
web/src/js/components/FlowView/Connection.tsx
Normal file
151
web/src/js/components/FlowView/Connection.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import * as React from "react"
|
||||
import {formatTimeStamp} from '../../utils'
|
||||
import {Client, Flow, Server} from '../../flow'
|
||||
|
||||
|
||||
type ConnectionInfoProps = {
|
||||
conn: Client | Server
|
||||
}
|
||||
|
||||
export function ConnectionInfo({conn}: ConnectionInfoProps) {
|
||||
let address_info: JSX.Element | null = null;
|
||||
if ("address" in conn) {
|
||||
// Server
|
||||
address_info = <>
|
||||
<tr>
|
||||
<td>Address:</td>
|
||||
<td>{conn.address?.join(':')}</td>
|
||||
</tr>
|
||||
{conn.peername && (
|
||||
<tr>
|
||||
<td>Resolved address:</td>
|
||||
<td>{conn.peername.join(':')}</td>
|
||||
</tr>
|
||||
)}
|
||||
{conn.sockname && (
|
||||
<tr>
|
||||
<td>Source address:</td>
|
||||
<td>{conn.sockname.join(':')}</td>
|
||||
</tr>
|
||||
)}
|
||||
</>;
|
||||
} else {
|
||||
// Client
|
||||
if (conn.peername?.[0]) {
|
||||
address_info = <>
|
||||
<tr>
|
||||
<td>Address:</td>
|
||||
<td>{conn.peername?.join(':')}</td>
|
||||
</tr>
|
||||
</>
|
||||
}
|
||||
|
||||
}
|
||||
return (
|
||||
<table className="connection-table">
|
||||
<tbody>
|
||||
{address_info}
|
||||
{conn.sni ? (
|
||||
<tr>
|
||||
<td><abbr title="TLS Server Name Indication">SNI</abbr>:</td>
|
||||
<td>{conn.sni}</td>
|
||||
</tr>
|
||||
): null}
|
||||
{conn.alpn ? (
|
||||
<tr>
|
||||
<td><abbr title="ALPN protocol negotiated">ALPN</abbr>:</td>
|
||||
<td>{conn.alpn}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{conn.tls_version ? (
|
||||
<tr>
|
||||
<td>TLS Version:</td>
|
||||
<td>{conn.tls_version}</td>
|
||||
</tr>
|
||||
): null}
|
||||
{conn.cipher ? (
|
||||
<tr>
|
||||
<td>TLS Cipher:</td>
|
||||
<td>{conn.cipher}</td>
|
||||
</tr>
|
||||
): null}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
function attrList(data: [string, string][]): JSX.Element {
|
||||
return <dl className="cert-attributes">
|
||||
{data.map(([k, v]) =>
|
||||
<React.Fragment key={k}>
|
||||
<dt>{k}</dt>
|
||||
<dd>{v}</dd>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</dl>
|
||||
}
|
||||
|
||||
export function CertificateInfo({flow}: { flow: Flow }): JSX.Element {
|
||||
const cert = flow.server_conn?.cert;
|
||||
if (!cert)
|
||||
return <></>;
|
||||
|
||||
return <>
|
||||
<h4 key="name">Server Certificate</h4>
|
||||
<table className="certificate-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{cert.keyinfo[0]}, {cert.keyinfo[1]} bits</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SHA256 digest</td>
|
||||
<td>{cert.sha256}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Valid from</td>
|
||||
<td>{formatTimeStamp(cert.notbefore, {milliseconds: false})}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Valid to</td>
|
||||
<td>{formatTimeStamp(cert.notafter, {milliseconds: false})}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subject Alternative Names</td>
|
||||
<td>{cert.altnames.join(", ")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subject</td>
|
||||
<td>{attrList(cert.subject)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Issuer</td>
|
||||
<td>{attrList(cert.issuer)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial</td>
|
||||
<td>{cert.serial}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
}
|
||||
|
||||
export default function Connection({flow}: { flow: Flow }) {
|
||||
return (
|
||||
<section className="detail">
|
||||
<h4>Client Connection</h4>
|
||||
<ConnectionInfo conn={flow.client_conn}/>
|
||||
|
||||
{
|
||||
flow.server_conn?.address &&
|
||||
<>
|
||||
<h4>Server Connection</h4>
|
||||
<ConnectionInfo conn={flow.server_conn}/>
|
||||
</>
|
||||
}
|
||||
|
||||
<CertificateInfo flow={flow}/>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
import React from 'react'
|
||||
import {formatTimeDelta, formatTimeStamp} from '../../utils'
|
||||
import { Flow, HTTPMessage, Connection } from '../../flow'
|
||||
|
||||
type TimeStampProps = {
|
||||
t: number,
|
||||
deltaTo: number,
|
||||
title: string,
|
||||
}
|
||||
|
||||
export function TimeStamp({t, deltaTo, title}: TimeStampProps) {
|
||||
return t ? (
|
||||
<tr>
|
||||
<td>{title}:</td>
|
||||
<td>
|
||||
{formatTimeStamp(t)}
|
||||
{deltaTo && (
|
||||
<span className="text-muted">
|
||||
({formatTimeDelta(1000 * (t - deltaTo))})
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr></tr>
|
||||
)
|
||||
}
|
||||
|
||||
type ConnectionInfoProps = {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
export function ConnectionInfo({conn}: ConnectionInfoProps) {
|
||||
return (
|
||||
<table className="connection-table">
|
||||
<tbody>
|
||||
<tr key="address">
|
||||
<td>Address:</td>
|
||||
<td>{conn.address?.join(':')}</td>
|
||||
</tr>
|
||||
{conn.sni && (
|
||||
<tr key="sni">
|
||||
<td><abbr title="TLS Server Name Indication">TLS SNI:</abbr></td>
|
||||
<td>{conn.sni}</td>
|
||||
</tr>
|
||||
)}
|
||||
{conn.tls_version && (
|
||||
<tr key="tls_version">
|
||||
<td>TLS version:</td>
|
||||
<td>{conn.tls_version}</td>
|
||||
</tr>
|
||||
)}
|
||||
{conn.cipher_name && (
|
||||
<tr key="cipher_name">
|
||||
<td>cipher name:</td>
|
||||
<td>{conn.cipher_name}</td>
|
||||
</tr>
|
||||
)}
|
||||
{conn.alpn_proto_negotiated && (
|
||||
<tr key="ALPN">
|
||||
<td><abbr title="ALPN protocol negotiated">ALPN:</abbr></td>
|
||||
<td>{conn.alpn_proto_negotiated}</td>
|
||||
</tr>
|
||||
)}
|
||||
{conn.ip_address && (
|
||||
<tr key="ip_address">
|
||||
<td>Resolved address:</td>
|
||||
<td>{conn.ip_address.join(':')}</td>
|
||||
</tr>
|
||||
)}
|
||||
{conn.source_address && (
|
||||
<tr key="source_address">
|
||||
<td>Source address:</td>
|
||||
<td>{conn.source_address.join(':')}</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 className="detail">
|
||||
<h4>Client Connection</h4>
|
||||
<ConnectionInfo conn={flow.client_conn}/>
|
||||
|
||||
{flow.server_conn.address &&
|
||||
[
|
||||
<h4 key="sc">Server Connection</h4>,
|
||||
<ConnectionInfo key="sc-ci" conn={flow.server_conn}/>
|
||||
]
|
||||
}
|
||||
|
||||
<CertificateInfo flow={flow}/>
|
||||
|
||||
<Timing flow={flow}/>
|
||||
</section>
|
||||
)
|
||||
}
|
20
web/src/js/components/FlowView/Error.tsx
Normal file
20
web/src/js/components/FlowView/Error.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import {HTTPFlow} from "../../flow";
|
||||
import {formatTimeStamp} from "../../utils";
|
||||
import * as React from "react";
|
||||
|
||||
type ErrorProps = {
|
||||
flow: HTTPFlow & { error: Error }
|
||||
}
|
||||
|
||||
export default function Error({flow}: ErrorProps) {
|
||||
return (
|
||||
<section className="error">
|
||||
<div className="alert alert-warning">
|
||||
{flow.error.msg}
|
||||
<div>
|
||||
<small>{formatTimeStamp(flow.error.timestamp)}</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ValueEditor from '../ValueEditor/ValueEditor'
|
||||
import { Key } from '../../utils'
|
||||
|
||||
export class HeaderEditor extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.onKeyDown = this.onKeyDown.bind(this)
|
||||
}
|
||||
|
||||
render() {
|
||||
let { onTab, ...props } = this.props
|
||||
return <ValueEditor
|
||||
{...props}
|
||||
onKeyDown={this.onKeyDown}
|
||||
/>
|
||||
}
|
||||
|
||||
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.ENTER:
|
||||
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,
|
||||
type: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
type: 'headers',
|
||||
}
|
||||
|
||||
onChange(row, col, val) {
|
||||
const nextHeaders = _.cloneDeep(this.props.message[this.props.type])
|
||||
|
||||
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[this.props.type]
|
||||
|
||||
if (col === 0) {
|
||||
this._nextSel = `${row}-value`
|
||||
return
|
||||
}
|
||||
if (row !== headers.length - 1) {
|
||||
this._nextSel = `${row + 1}-key`
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const nextHeaders = _.cloneDeep(this.props.message[this.props.type])
|
||||
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, readonly } = this.props
|
||||
if (message[this.props.type]) {
|
||||
return (
|
||||
<table className="header-table">
|
||||
<tbody>
|
||||
{message[this.props.type].map((header, i) => (
|
||||
<tr key={i}>
|
||||
<td className="header-name">
|
||||
<HeaderEditor
|
||||
ref={`${i}-key`}
|
||||
content={header[0]}
|
||||
readonly={readonly}
|
||||
onDone={val => this.onChange(i, 0, val)}
|
||||
onRemove={event => this.onRemove(i, 0, event)}
|
||||
onTab={event => this.onTab(i, 0, event)}
|
||||
/>
|
||||
<span className="header-colon">:</span>
|
||||
</td>
|
||||
<td className="header-value">
|
||||
<HeaderEditor
|
||||
ref={`${i}-value`}
|
||||
content={header[1]}
|
||||
readonly={readonly}
|
||||
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>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<table className="header-table">
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
146
web/src/js/components/FlowView/HttpMessages.tsx
Normal file
146
web/src/js/components/FlowView/HttpMessages.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import * as React from "react"
|
||||
|
||||
import {isValidHttpVersion, MessageUtils, parseUrl, RequestUtils} from '../../flow/utils'
|
||||
import ValidateEditor from '../editors/ValidateEditor'
|
||||
import ValueEditor from '../editors/ValueEditor'
|
||||
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
import {HTTPFlow, HTTPMessage, HTTPResponse} from '../../flow'
|
||||
import * as flowActions from '../../ducks/flows'
|
||||
import KeyValueListEditor from "../editors/KeyValueListEditor";
|
||||
import HttpMessage from "../contentviews/HttpMessage";
|
||||
|
||||
|
||||
type RequestLineProps = {
|
||||
flow: HTTPFlow,
|
||||
}
|
||||
|
||||
function RequestLine({flow}: RequestLineProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<div className="first-line request-line">
|
||||
<div>
|
||||
<ValidateEditor
|
||||
content={flow.request.method}
|
||||
onEditDone={method => dispatch(flowActions.update(flow, {request: {method}}))}
|
||||
isValid={method => method.length > 0}
|
||||
/>
|
||||
|
||||
<ValidateEditor
|
||||
content={RequestUtils.pretty_url(flow.request)}
|
||||
onEditDone={url => dispatch(flowActions.update(flow, {request: {path: '', ...parseUrl(url)}}))}
|
||||
isValid={url => !!parseUrl(url)?.host}
|
||||
/>
|
||||
|
||||
<ValidateEditor
|
||||
content={flow.request.http_version}
|
||||
onEditDone={http_version => dispatch(flowActions.update(flow, {request: {http_version}}))}
|
||||
isValid={isValidHttpVersion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
type ResponseLineProps = {
|
||||
flow: HTTPFlow & { response: HTTPResponse },
|
||||
}
|
||||
|
||||
function ResponseLine({flow}: ResponseLineProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<div className="first-line response-line">
|
||||
<ValidateEditor
|
||||
content={flow.response.http_version}
|
||||
onEditDone={nextVer => dispatch(flowActions.update(flow, {response: {http_version: nextVer}}))}
|
||||
isValid={isValidHttpVersion}
|
||||
/>
|
||||
|
||||
<ValidateEditor
|
||||
content={flow.response.status_code + ''}
|
||||
onEditDone={code => dispatch(flowActions.update(flow, {response: {code: parseInt(code)}}))}
|
||||
isValid={code => /^\d+$/.test(code)}
|
||||
/>
|
||||
{flow.response.http_version !== "HTTP/2.0" &&
|
||||
<>
|
||||
<ValueEditor
|
||||
content={flow.response.reason}
|
||||
onEditDone={msg => dispatch(flowActions.update(flow, {response: {msg}}))}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
type HeadersProps = {
|
||||
flow: HTTPFlow,
|
||||
message: HTTPMessage
|
||||
}
|
||||
|
||||
function Headers({flow, message}: HeadersProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const part = flow.request === message ? "request" : "response";
|
||||
|
||||
return (
|
||||
<KeyValueListEditor
|
||||
className="headers"
|
||||
data={message.headers}
|
||||
onChange={headers => dispatch(flowActions.update(flow, {[part]: {headers}}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type TrailersProps = {
|
||||
flow: HTTPFlow,
|
||||
message: HTTPMessage
|
||||
}
|
||||
|
||||
function Trailers({flow, message}: TrailersProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const part = flow.request === message ? "request" : "response";
|
||||
const hasTrailers = !!MessageUtils.get_first_header(message, /^trailer$/i)
|
||||
|
||||
if (!hasTrailers)
|
||||
return null;
|
||||
|
||||
return <>
|
||||
<hr/>
|
||||
<h5>HTTP Trailers</h5>
|
||||
<KeyValueListEditor
|
||||
className="trailers"
|
||||
data={message.trailers}
|
||||
onChange={trailers => dispatch(flowActions.update(flow, {[part]: {trailers}}))}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
|
||||
const Message = React.memo(function Message({flow, message}: { flow: HTTPFlow, message: HTTPMessage }) {
|
||||
const part = flow.request === message ? "request" : "response";
|
||||
const FirstLine = flow.request === message ? RequestLine : ResponseLine;
|
||||
|
||||
return (
|
||||
<section className={part}>
|
||||
<FirstLine flow={flow}/>
|
||||
<Headers flow={flow} message={message}/>
|
||||
<hr/>
|
||||
<HttpMessage key={flow.id + part} flow={flow} message={message}/>
|
||||
<Trailers flow={flow} message={message}/>
|
||||
</section>
|
||||
)
|
||||
});
|
||||
|
||||
export function Request() {
|
||||
const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as HTTPFlow;
|
||||
return <Message flow={flow} message={flow.request}/>;
|
||||
}
|
||||
|
||||
export function Response() {
|
||||
const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as HTTPFlow & { response: HTTPResponse }
|
||||
return <Message flow={flow} message={flow.response}/>;
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils'
|
||||
import { formatTimeStamp } from '../../utils'
|
||||
import ContentView from '../ContentView'
|
||||
import ContentViewOptions from '../ContentView/ContentViewOptions'
|
||||
import ValidateEditor from '../ValueEditor/ValidateEditor'
|
||||
import ValueEditor from '../ValueEditor/ValueEditor'
|
||||
import HideInStatic from '../common/HideInStatic'
|
||||
|
||||
import Headers from './Headers'
|
||||
import { updateEdit as updateFlow } from '../../ducks/ui/flow'
|
||||
import { uploadContent } from '../../ducks/flows'
|
||||
import ToggleEdit from './ToggleEdit'
|
||||
import { useAppDispatch, useAppSelector } from "../../ducks";
|
||||
import { HTTPFlow, HTTPMessage } from '../../flow'
|
||||
|
||||
|
||||
type RequestLineProps = {
|
||||
flow: HTTPFlow,
|
||||
readonly: boolean,
|
||||
}
|
||||
|
||||
function RequestLine({ flow, readonly }: RequestLineProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return (
|
||||
<div className="first-line request-line">
|
||||
<div>
|
||||
<ValueEditor
|
||||
content={flow.request.method}
|
||||
readonly={readonly}
|
||||
onDone={method => dispatch(updateFlow({ request: { method } }))}
|
||||
/>
|
||||
|
||||
<ValidateEditor
|
||||
content={RequestUtils.pretty_url(flow.request)}
|
||||
readonly={readonly}
|
||||
onDone={url => dispatch(updateFlow({ request: {path: '', ...parseUrl(url)}}))}
|
||||
isValid={url => !!parseUrl(url).host}
|
||||
/>
|
||||
|
||||
<ValidateEditor
|
||||
content={flow.request.http_version}
|
||||
readonly={readonly}
|
||||
onDone={http_version => dispatch(updateFlow({ request: { http_version } }))}
|
||||
isValid={isValidHttpVersion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ResponseLineProps = {
|
||||
flow: HTTPFlow,
|
||||
readonly: boolean,
|
||||
}
|
||||
|
||||
function ResponseLine({ flow, readonly }: ResponseLineProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return (
|
||||
<div className="first-line response-line">
|
||||
<ValidateEditor
|
||||
content={flow.response?.http_version}
|
||||
readonly={readonly}
|
||||
onDone={nextVer => dispatch(updateFlow({ response: { http_version: nextVer } }))}
|
||||
isValid={isValidHttpVersion}
|
||||
/>
|
||||
|
||||
<ValidateEditor
|
||||
content={flow.response?.status_code + ''}
|
||||
readonly={readonly}
|
||||
onDone={code => dispatch(updateFlow({ response: { code: parseInt(code) } }))}
|
||||
isValid={code => /^\d+$/.test(code)}
|
||||
/>
|
||||
|
||||
<ValueEditor
|
||||
content={flow.response?.reason}
|
||||
readonly={readonly}
|
||||
onDone={msg => dispatch(updateFlow({ response: { msg } }))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Request() {
|
||||
const dispatch = useAppDispatch(),
|
||||
flow = useAppSelector(state => state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]]),
|
||||
isEdit = useAppSelector(state => !!state.ui.flow.modifiedFlow)
|
||||
|
||||
let noContent = !isEdit && (flow.request.contentLength == 0 || flow.request.contentLength == null)
|
||||
return (
|
||||
<section className="request">
|
||||
<article>
|
||||
<ToggleEdit/>
|
||||
<RequestLine
|
||||
flow={flow}
|
||||
readonly={!isEdit} />
|
||||
<Headers
|
||||
message={flow.request}
|
||||
readonly={!isEdit}
|
||||
onChange={headers => dispatch(updateFlow({ request: { headers } }))}
|
||||
/>
|
||||
|
||||
<hr/>
|
||||
<ContentView
|
||||
readonly={!isEdit}
|
||||
flow={flow}
|
||||
onContentChange={content => dispatch(updateFlow({ request: {content}}))}
|
||||
message={flow.request}/>
|
||||
|
||||
<hr/>
|
||||
<Headers
|
||||
message={flow.request}
|
||||
readonly={!isEdit}
|
||||
onChange={trailers => dispatch(updateFlow({ request: { trailers } }))}
|
||||
type='trailers'
|
||||
/>
|
||||
</article>
|
||||
<HideInStatic>
|
||||
{!noContent &&
|
||||
<footer>
|
||||
<ContentViewOptions
|
||||
flow={flow}
|
||||
message={flow.request}
|
||||
uploadContent={content => dispatch(uploadContent(flow, content, "request"))}/>
|
||||
</footer>
|
||||
}
|
||||
</HideInStatic>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function Response() {
|
||||
const dispatch = useAppDispatch(),
|
||||
flow = useAppSelector(state => state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]]),
|
||||
isEdit = useAppSelector(state => !!state.ui.flow.modifiedFlow)
|
||||
|
||||
let noContent = !isEdit && (flow.response.contentLength == 0 || flow.response.contentLength == null)
|
||||
|
||||
return (
|
||||
<section className="response">
|
||||
<article>
|
||||
<ToggleEdit/>
|
||||
<ResponseLine
|
||||
flow={flow}
|
||||
readonly={!isEdit} />
|
||||
<Headers
|
||||
message={flow.response}
|
||||
readonly={!isEdit}
|
||||
onChange={headers => dispatch(updateFlow({ response: { headers } }))}
|
||||
/>
|
||||
<hr/>
|
||||
<ContentView
|
||||
readonly={!isEdit}
|
||||
flow={flow}
|
||||
onContentChange={content => dispatch(updateFlow({ response: {content}}))}
|
||||
message={flow.response}
|
||||
/>
|
||||
<hr/>
|
||||
<Headers
|
||||
message={flow.response}
|
||||
readonly={!isEdit}
|
||||
onChange={trailers => dispatch(updateFlow({ response: { trailers } }))}
|
||||
type='trailers'
|
||||
/>
|
||||
</article>
|
||||
<HideInStatic>
|
||||
{!noContent &&
|
||||
<footer >
|
||||
<ContentViewOptions
|
||||
flow={flow}
|
||||
message={flow.response}
|
||||
uploadContent={content => dispatch(uploadContent(flow, content, "response"))} />
|
||||
</footer>
|
||||
}
|
||||
</HideInStatic>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
type ErrorViewProps = {
|
||||
flow: HTTPFlow
|
||||
}
|
||||
|
||||
export function ErrorView({ flow }: ErrorViewProps) {
|
||||
return (
|
||||
<section className="error">
|
||||
<div className="alert alert-warning">
|
||||
{flow.error?.msg}
|
||||
<div>
|
||||
<small>{formatTimeStamp(flow.error?.timestamp)}</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
type NavActionProps = {
|
||||
icon: string,
|
||||
title: string,
|
||||
onClick: (e: any) => void,
|
||||
}
|
||||
|
||||
export function NavAction({ icon, title, onClick }: NavActionProps) {
|
||||
return (
|
||||
<a title={title}
|
||||
href="#"
|
||||
className="nav-action"
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
onClick(event)
|
||||
}}>
|
||||
<i className={`fa fa-fw ${icon}`}></i>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
type NavProps = {
|
||||
active: string,
|
||||
tabs: string[],
|
||||
onSelectTab: (e: string) => void,
|
||||
}
|
||||
|
||||
export default function Nav({ active, tabs, onSelectTab }: NavProps) {
|
||||
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>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
94
web/src/js/components/FlowView/Timing.tsx
Normal file
94
web/src/js/components/FlowView/Timing.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import {Flow} from "../../flow";
|
||||
import * as React from "react";
|
||||
import {formatTimeDelta, formatTimeStamp} from "../../utils";
|
||||
|
||||
export type TimeStampProps = {
|
||||
t: number,
|
||||
deltaTo?: number,
|
||||
title: string,
|
||||
}
|
||||
|
||||
export function TimeStamp({t, deltaTo, title}: TimeStampProps) {
|
||||
return t ? (
|
||||
<tr>
|
||||
<td>{title}:</td>
|
||||
<td>
|
||||
{formatTimeStamp(t)}
|
||||
{deltaTo && (
|
||||
<span className="text-muted">
|
||||
({formatTimeDelta(1000 * (t - deltaTo))})
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Timing({flow}: { flow: Flow }) {
|
||||
let ref: number;
|
||||
if (flow.type === "http") {
|
||||
ref = flow.request.timestamp_start
|
||||
} else {
|
||||
ref = flow.client_conn.timestamp_start
|
||||
}
|
||||
|
||||
const timestamps: Partial<TimeStampProps>[] = [
|
||||
{
|
||||
title: "Server conn. initiated",
|
||||
t: flow.server_conn?.timestamp_start,
|
||||
deltaTo: ref
|
||||
}, {
|
||||
title: "Server conn. TCP handshake",
|
||||
t: flow.server_conn?.timestamp_tcp_setup,
|
||||
deltaTo: ref
|
||||
}, {
|
||||
title: "Server conn. TLS handshake",
|
||||
t: flow.server_conn?.timestamp_tls_setup,
|
||||
deltaTo: ref
|
||||
}, {
|
||||
title: "Client conn. established",
|
||||
t: flow.client_conn.timestamp_start,
|
||||
deltaTo: flow.type === "http" ? ref : undefined
|
||||
}, {
|
||||
title: "Client conn. TLS handshake",
|
||||
t: flow.client_conn.timestamp_tls_setup,
|
||||
deltaTo: ref
|
||||
}
|
||||
]
|
||||
if (flow.type === "http") {
|
||||
timestamps.push(...[
|
||||
{
|
||||
title: "First request byte",
|
||||
t: flow.request.timestamp_start
|
||||
}, {
|
||||
title: "Request complete",
|
||||
t: flow.request.timestamp_end,
|
||||
deltaTo: ref
|
||||
}, {
|
||||
title: "First response byte",
|
||||
t: flow.response?.timestamp_start,
|
||||
deltaTo: ref
|
||||
}, {
|
||||
title: "Response complete",
|
||||
t: flow.response?.timestamp_end,
|
||||
deltaTo: ref
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="timing">
|
||||
<h4>Timing</h4>
|
||||
<table className="timing-table">
|
||||
<tbody>
|
||||
{timestamps
|
||||
.filter((v): v is TimeStampProps => !!v.t)
|
||||
.sort((a, b) => a.t - b.t)
|
||||
.map(props => <TimeStamp key={props.title} {...props}/>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
import { startEdit, stopEdit } from '../../ducks/ui/flow'
|
||||
|
||||
|
||||
export default function ToggleEdit() {
|
||||
const dispatch = useAppDispatch(),
|
||||
isEdit = useAppSelector(state => !!state.ui.flow.modifiedFlow),
|
||||
modifiedFlow = useAppSelector(state => state.ui.flow.modifiedFlow|| state.flows.byId[state.flows.selected[0]]),
|
||||
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
|
||||
|
||||
return (
|
||||
<div className="edit-flow-container">
|
||||
{isEdit ?
|
||||
<a className="edit-flow" title="Finish Edit" onClick={() => dispatch(stopEdit(flow, modifiedFlow))}>
|
||||
<i className="fa fa-check"/>
|
||||
</a>
|
||||
:
|
||||
<a className="edit-flow" title="Edit Flow" onClick={() => dispatch(startEdit(flow))}>
|
||||
<i className="fa fa-pencil"/>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
66
web/src/js/components/FlowView/WebSocket.tsx
Normal file
66
web/src/js/components/FlowView/WebSocket.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import {HTTPFlow, WebSocketData} from "../../flow";
|
||||
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";
|
||||
|
||||
|
||||
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 (
|
||||
<section className="websocket">
|
||||
<h4>WebSocket</h4>
|
||||
<div className="contentview">
|
||||
<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}/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function CloseSummary({websocket}: {websocket: WebSocketData}){
|
||||
if(!websocket.timestamp_end)
|
||||
return null;
|
||||
const reason = websocket.close_reason ? `(${websocket.close_reason})` : ""
|
||||
return <div>
|
||||
<i className="fa fa-fw fa-window-close text-muted"/>
|
||||
|
||||
Closed by {websocket.closed_by_client ? "client": "server"} with code {websocket.close_code} {reason}.
|
||||
|
||||
<small className="pull-right">
|
||||
{formatTimeStamp(websocket.timestamp_end)}
|
||||
</small>
|
||||
</div>
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import {formatSize} from '../utils'
|
||||
import HideInStatic from '../components/common/HideInStatic'
|
||||
import {useAppSelector} from "../ducks";
|
||||
@ -7,7 +7,7 @@ export default function Footer() {
|
||||
const version = useAppSelector(state => state.conf.version);
|
||||
let {
|
||||
mode, intercept, showhost, upstream_cert, rawtcp, http2, websocket, anticache, anticomp,
|
||||
stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, server
|
||||
stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, server, ssl_insecure
|
||||
} = useAppSelector(state => state.options);
|
||||
|
||||
return (
|
||||
@ -18,6 +18,9 @@ export default function Footer() {
|
||||
{intercept && (
|
||||
<span className="label label-success">Intercept: {intercept}</span>
|
||||
)}
|
||||
{ssl_insecure && (
|
||||
<span className="label label-danger">ssl_insecure</span>
|
||||
)}
|
||||
{showhost && (
|
||||
<span className="label label-success">showhost</span>
|
||||
)}
|
||||
@ -57,8 +60,8 @@ export default function Footer() {
|
||||
</span>)
|
||||
}
|
||||
</HideInStatic>
|
||||
<span className="label label-info" title="Mitmproxy Version">
|
||||
{version}
|
||||
<span className="label label-default" title="Mitmproxy Version">
|
||||
mitmproxy {version}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -1,63 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import MainMenu from './Header/MainMenu'
|
||||
import OptionMenu from './Header/OptionMenu'
|
||||
import FileMenu from './Header/FileMenu'
|
||||
import FlowMenu from './Header/FlowMenu'
|
||||
import {setActiveMenu} from '../ducks/ui/header'
|
||||
import ConnectionIndicator from "./Header/ConnectionIndicator"
|
||||
import HideInStatic from './common/HideInStatic'
|
||||
|
||||
class Header extends Component {
|
||||
static entries = [MainMenu, OptionMenu]
|
||||
|
||||
handleClick(active, e) {
|
||||
e.preventDefault()
|
||||
this.props.setActiveMenu(active.title)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedFlowId, activeMenu} = this.props
|
||||
|
||||
let entries = [...Header.entries]
|
||||
if(selectedFlowId)
|
||||
entries.push(FlowMenu)
|
||||
|
||||
// Make sure to have a fallback in case FlowMenu is selected but we don't have any flows
|
||||
// (e.g. because they are all deleted or not yet received)
|
||||
const Active = _.find(entries, (e) => e.title == activeMenu) || MainMenu
|
||||
|
||||
return (
|
||||
<header>
|
||||
<nav className="nav-tabs nav-tabs-lg">
|
||||
<FileMenu/>
|
||||
{entries.map(Entry => (
|
||||
<a key={Entry.title}
|
||||
href="#"
|
||||
className={classnames({ active: Entry === Active})}
|
||||
onClick={e => this.handleClick(Entry, e)}>
|
||||
{Entry.title}
|
||||
</a>
|
||||
))}
|
||||
<HideInStatic>
|
||||
<ConnectionIndicator/>
|
||||
</HideInStatic>
|
||||
</nav>
|
||||
<div>
|
||||
<Active/>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
selectedFlowId: state.flows.selected[0],
|
||||
activeMenu: state.ui.header.activeMenu,
|
||||
}),
|
||||
{
|
||||
setActiveMenu,
|
||||
}
|
||||
)(Header)
|
64
web/src/js/components/Header.tsx
Normal file
64
web/src/js/components/Header.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, {useState} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import StartMenu from './Header/StartMenu'
|
||||
import OptionMenu from './Header/OptionMenu'
|
||||
import FileMenu from './Header/FileMenu'
|
||||
import FlowMenu from './Header/FlowMenu'
|
||||
import ConnectionIndicator from "./Header/ConnectionIndicator"
|
||||
import HideInStatic from './common/HideInStatic'
|
||||
import {useAppSelector} from "../ducks";
|
||||
|
||||
interface Menu {
|
||||
(): JSX.Element;
|
||||
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const selectedFlows = useAppSelector(state => state.flows.selected.filter(id => id in state.flows.byId)),
|
||||
[ActiveMenu, setActiveMenu] = useState<Menu>(() => StartMenu),
|
||||
[wasFlowSelected, setWasFlowSelected] = useState(false);
|
||||
|
||||
let entries: Menu[] = [StartMenu, OptionMenu];
|
||||
if (selectedFlows.length > 0) {
|
||||
if (!wasFlowSelected) {
|
||||
setActiveMenu(() => FlowMenu);
|
||||
setWasFlowSelected(true);
|
||||
}
|
||||
entries.push(FlowMenu)
|
||||
} else {
|
||||
if (wasFlowSelected) {
|
||||
setWasFlowSelected(false);
|
||||
}
|
||||
if (ActiveMenu === FlowMenu) {
|
||||
setActiveMenu(() => StartMenu)
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(active: Menu, e) {
|
||||
e.preventDefault()
|
||||
setActiveMenu(() => active)
|
||||
}
|
||||
|
||||
return (
|
||||
<header>
|
||||
<nav className="nav-tabs nav-tabs-lg">
|
||||
<FileMenu/>
|
||||
{entries.map(Entry => (
|
||||
<a key={Entry.title}
|
||||
href="#"
|
||||
className={classnames({active: Entry === ActiveMenu})}
|
||||
onClick={e => handleClick(Entry, e)}>
|
||||
{Entry.title}
|
||||
</a>
|
||||
))}
|
||||
<HideInStatic>
|
||||
<ConnectionIndicator/>
|
||||
</HideInStatic>
|
||||
</nav>
|
||||
<div>
|
||||
<ActiveMenu/>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React from "react"
|
||||
import * as React from "react";
|
||||
import {ConnectionState} from "../../ducks/connection"
|
||||
import {useAppSelector} from "../../ducks";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import {useDispatch} from 'react-redux'
|
||||
import FileChooser from '../common/FileChooser'
|
||||
import Dropdown, {Divider, MenuItem} from '../common/Dropdown'
|
||||
@ -9,10 +9,7 @@ import HideInStatic from "../common/HideInStatic";
|
||||
export default React.memo(function FileMenu() {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<Dropdown className="pull-left special" text="mitmproxy" options={{"placement": "bottom-start"}}>
|
||||
<MenuItem onClick={() => confirm('Delete all flows?') && dispatch(flowsActions.clear())}>
|
||||
<i className="fa fa-fw fa-trash"/> Clear All
|
||||
</MenuItem>
|
||||
<Dropdown className="pull-left special" text="File" options={{"placement": "bottom-start"}}>
|
||||
<li>
|
||||
<FileChooser
|
||||
icon="fa-folder-open"
|
||||
@ -30,6 +27,9 @@ export default React.memo(function FileMenu() {
|
||||
<MenuItem onClick={() => dispatch(flowsActions.download())}>
|
||||
<i className="fa fa-fw fa-floppy-o"/> Save...
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => confirm('Delete all flows?') && dispatch(flowsActions.clear())}>
|
||||
<i className="fa fa-fw fa-trash"/> Clear All
|
||||
</MenuItem>
|
||||
<HideInStatic>
|
||||
<Divider/>
|
||||
<li>
|
||||
|
@ -1,76 +1,121 @@
|
||||
import React from "react"
|
||||
import * as React from "react";
|
||||
import Button from "../common/Button"
|
||||
import { MessageUtils } from "../../flow/utils.js"
|
||||
import {MessageUtils} from "../../flow/utils.js"
|
||||
import HideInStatic from "../common/HideInStatic";
|
||||
import { useAppDispatch, useAppSelector } from "../../ducks";
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
import {
|
||||
resume as resumeFlow,
|
||||
replay as replayFlow,
|
||||
duplicate as duplicateFlow,
|
||||
revert as revertFlow,
|
||||
kill as killFlow,
|
||||
remove as removeFlow,
|
||||
kill as killFlow
|
||||
replay as replayFlow,
|
||||
resume as resumeFlow,
|
||||
revert as revertFlow
|
||||
} from "../../ducks/flows"
|
||||
import Dropdown, {MenuItem} from "../common/Dropdown";
|
||||
import {copy} from "../../flow/export";
|
||||
import {Flow} from "../../flow";
|
||||
|
||||
FlowMenu.title = 'Flow'
|
||||
|
||||
export default function FlowMenu() {
|
||||
export default function FlowMenu(): JSX.Element {
|
||||
const dispatch = useAppDispatch(),
|
||||
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
|
||||
flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]])
|
||||
|
||||
if (!flow)
|
||||
return <div/>
|
||||
return (
|
||||
<div className="flow-menu">
|
||||
<HideInStatic>
|
||||
<div className="menu-group">
|
||||
<div className="menu-content">
|
||||
<Button title="[r]eplay flow" icon="fa-repeat text-primary"
|
||||
onClick={() => dispatch(replayFlow(flow))}>
|
||||
Replay
|
||||
</Button>
|
||||
<Button title="[D]uplicate flow" icon="fa-copy text-info"
|
||||
onClick={() => dispatch(duplicateFlow(flow))}>
|
||||
Duplicate
|
||||
</Button>
|
||||
<Button disabled={!flow || !flow.modified} title="revert changes to flow [V]"
|
||||
icon="fa-history text-warning" onClick={() => dispatch(revertFlow(flow))}>
|
||||
Revert
|
||||
</Button>
|
||||
<Button title="[d]elete flow" icon="fa-trash text-danger"
|
||||
onClick={() => dispatch(removeFlow(flow))}>
|
||||
Delete
|
||||
</Button>
|
||||
<div className="menu-group">
|
||||
<div className="menu-content">
|
||||
<Button title="[r]eplay flow" icon="fa-repeat text-primary"
|
||||
onClick={() => dispatch(replayFlow(flow))}
|
||||
disabled={!(flow.type === "http" && !flow.websocket)}
|
||||
>
|
||||
Replay
|
||||
</Button>
|
||||
<Button title="[D]uplicate flow" icon="fa-copy text-info"
|
||||
onClick={() => dispatch(duplicateFlow(flow))}>
|
||||
Duplicate
|
||||
</Button>
|
||||
<Button disabled={!flow || !flow.modified} title="revert changes to flow [V]"
|
||||
icon="fa-history text-warning" onClick={() => dispatch(revertFlow(flow))}>
|
||||
Revert
|
||||
</Button>
|
||||
<Button title="[d]elete flow" icon="fa-trash text-danger"
|
||||
onClick={() => dispatch(removeFlow(flow))}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<div className="menu-legend">Flow Modification</div>
|
||||
</div>
|
||||
<div className="menu-legend">Flow Modification</div>
|
||||
</div>
|
||||
</HideInStatic>
|
||||
|
||||
<div className="menu-group">
|
||||
<div className="menu-content">
|
||||
<Button title="download" icon="fa-download"
|
||||
onClick={() => window.location = MessageUtils.getContentURL(flow, flow.response)}>
|
||||
Download
|
||||
</Button>
|
||||
<DownloadButton flow={flow}/>
|
||||
<Dropdown className="" text={
|
||||
<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 className="menu-legend">Export</div>
|
||||
</div>
|
||||
|
||||
<HideInStatic>
|
||||
<div className="menu-group">
|
||||
<div className="menu-content">
|
||||
<Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow"
|
||||
icon="fa-play text-success" onClick={() => dispatch(resumeFlow(flow))}>
|
||||
Resume
|
||||
</Button>
|
||||
<Button disabled={!flow || !flow.intercepted} title="kill intercepted flow [x]"
|
||||
icon="fa-times text-danger" onClick={() => dispatch(killFlow(flow))}>
|
||||
Abort
|
||||
</Button>
|
||||
<div className="menu-group">
|
||||
<div className="menu-content">
|
||||
<Button disabled={!flow || !flow.intercepted} title="[a]ccept intercepted flow"
|
||||
icon="fa-play text-success" onClick={() => dispatch(resumeFlow(flow))}>
|
||||
Resume
|
||||
</Button>
|
||||
<Button disabled={!flow || !flow.intercepted} title="kill intercepted flow [x]"
|
||||
icon="fa-times text-danger" onClick={() => dispatch(killFlow(flow))}>
|
||||
Abort
|
||||
</Button>
|
||||
</div>
|
||||
<div className="menu-legend">Interception</div>
|
||||
</div>
|
||||
<div className="menu-legend">Interception</div>
|
||||
</div>
|
||||
</HideInStatic>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function DownloadButton({flow}: { flow: Flow }) {
|
||||
if (flow.type !== "http")
|
||||
return null;
|
||||
|
||||
if (flow.request.contentLength && !flow.response?.contentLength) {
|
||||
return <Button icon="fa-download"
|
||||
onClick={() => window.location.href = MessageUtils.getContentURL(flow, flow.request)}
|
||||
>Download</Button>
|
||||
}
|
||||
if (flow.response) {
|
||||
const response = flow.response;
|
||||
if (!flow.request.contentLength && flow.response.contentLength) {
|
||||
return <Button icon="fa-download"
|
||||
onClick={() => window.location.href = MessageUtils.getContentURL(flow, response)}
|
||||
>Download</Button>
|
||||
}
|
||||
if (flow.request.contentLength && flow.response.contentLength) {
|
||||
return <Dropdown text={
|
||||
<Button icon="fa-download" onClick={() => 1}>Download▾</Button>
|
||||
} options={{"placement": "bottom-start"}}>
|
||||
<MenuItem onClick={() => window.location.href = MessageUtils.getContentURL(flow, flow.request)}>Download
|
||||
request</MenuItem>
|
||||
<MenuItem onClick={() => window.location.href = MessageUtils.getContentURL(flow, response)}>Download
|
||||
response</MenuItem>
|
||||
</Dropdown>
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
import React, {ChangeEvent} from "react"
|
||||
import * as React from "react";
|
||||
import {useDispatch} from "react-redux"
|
||||
import {toggleVisibility} from "../../ducks/eventLog"
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
import * as optionsActions from "../../ducks/options";
|
||||
import * as eventLogActions from "../../ducks/eventLog"
|
||||
import * as commandBarActions from "../../ducks/commandBar"
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks"
|
||||
import * as optionsActions from "../../ducks/options"
|
||||
|
||||
|
||||
type MenuToggleProps = {
|
||||
value: boolean
|
||||
onChange: (e: ChangeEvent) => void
|
||||
onChange: (e: React.ChangeEvent) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
@ -51,9 +52,23 @@ export function EventlogToggle() {
|
||||
return (
|
||||
<MenuToggle
|
||||
value={visible}
|
||||
onChange={() => dispatch(toggleVisibility())}
|
||||
onChange={() => dispatch(eventLogActions.toggleVisibility())}
|
||||
>
|
||||
Display Event Log
|
||||
</MenuToggle>
|
||||
)
|
||||
}
|
||||
|
||||
export function CommandBarToggle() {
|
||||
const dispatch = useDispatch(),
|
||||
visible = useAppSelector(state => state.commandBar.visible);
|
||||
|
||||
return (
|
||||
<MenuToggle
|
||||
value={visible}
|
||||
onChange={() => dispatch(commandBarActions.toggleVisibility())}
|
||||
>
|
||||
Display Command Bar
|
||||
</MenuToggle>
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { EventlogToggle, OptionsToggle } from "./MenuToggle"
|
||||
import * as React from "react";
|
||||
import {CommandBarToggle, EventlogToggle, OptionsToggle} from "./MenuToggle"
|
||||
import Button from "../common/Button"
|
||||
import DocsLink from "../common/DocsLink"
|
||||
import HideInStatic from "../common/HideInStatic";
|
||||
@ -44,6 +44,7 @@ export default function OptionMenu() {
|
||||
<div className="menu-group">
|
||||
<div className="menu-content">
|
||||
<EventlogToggle/>
|
||||
<CommandBarToggle/>
|
||||
</div>
|
||||
<div className="menu-legend">View Options</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react"
|
||||
import * as React from "react";
|
||||
import FilterInput from "./FilterInput"
|
||||
import * as flowsActions from "../../ducks/flows"
|
||||
import {setFilter, setHighlight} from "../../ducks/flows"
|
||||
@ -6,9 +6,9 @@ import Button from "../common/Button"
|
||||
import {update as updateOptions} from "../../ducks/options";
|
||||
import {useAppDispatch, useAppSelector} from "../../ducks";
|
||||
|
||||
MainMenu.title = "Start"
|
||||
StartMenu.title = "Start"
|
||||
|
||||
export default function MainMenu() {
|
||||
export default function StartMenu() {
|
||||
return (
|
||||
<div className="main-menu">
|
||||
<div className="menu-group">
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import Splitter from './common/Splitter'
|
||||
import FlowTable from './FlowTable'
|
||||
import FlowView from './FlowView'
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import * as React from "react"
|
||||
import ModalList from './ModalList'
|
||||
import { useAppSelector } from "../../ducks";
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user