From 65aa0a6ef5d4b8b84b0293a5aa681b2fb8179ba7 Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Wed, 18 Aug 2021 17:38:22 +0200 Subject: [PATCH] major mitmweb upgrades --- mitmproxy/tools/web/app.py | 183 ++++++++++----- .../static/images/resourceWebSocketIcon.png | Bin 0 -> 1428 bytes test/mitmproxy/tools/web/test_app.py | 16 +- web/gulpfile.js | 2 +- web/src/css/flowdetail.less | 149 +++++++----- web/src/css/flowtable.less | 4 + web/src/css/flowview.less | 1 + web/src/css/footer.less | 4 +- web/src/css/header.less | 5 +- web/src/css/sprites.less | 6 +- web/src/css/tabs.less | 7 +- web/src/css/vendor-bootstrap-variables.less | 2 + web/src/images/resourceWebSocketIcon.png | Bin 0 -> 1428 bytes .../components/Command/CommandSpec.js | 4 +- .../ContentView/ContentLoaderSpec.js | 4 +- .../ContentView/ContentViewOptionsSpec.js | 2 +- .../components/ContentView/ContentViewSpec.js | 2 +- .../ContentView/DownloadContentButtonSpec.js | 2 +- .../components/ContentView/MetaViewsSpec.js | 2 +- .../ContentView/ShowFullContentButtonSpec.js | 2 +- .../ContentView/UploadContentButtonSpec.js | 2 +- .../ContentView/ViewSelectorSpec.tsx | 2 +- .../__tests__/components/ContentViewSpec.js | 2 +- .../components/EventLog/EventListSpec.js | 2 +- .../js/__tests__/components/EventLogSpec.js | 2 +- .../components/FlowTable/FlowColumnsSpec.js | 58 ++--- .../components/FlowTable/FlowRowSpec.tsx | 2 +- .../components/FlowTable/FlowTableHeadSpec.js | 4 +- .../js/__tests__/components/FlowTableSpec.js | 2 +- .../components/FlowView/DetailsSpec.js | 11 +- .../components/FlowView/HeadersSpec.js | 10 +- .../components/FlowView/MessagesSpec.js | 12 +- .../__tests__/components/FlowView/NavSpec.js | 2 +- .../components/FlowView/ToggleEditSpec.tsx | 2 +- .../__snapshots__/HeadersSpec.js.snap | 2 +- .../Header/ConnectionIndicatorSpec.tsx | 2 +- .../components/Header/FileMenuSpec.js | 2 +- .../components/Header/FilterDocsSpec.js | 2 +- .../components/Header/FilterInputSpec.js | 2 +- .../components/Header/FlowMenuSpec.js | 2 +- .../components/Header/MainMenuSpec.js | 6 +- .../components/Header/MenuToggleSpec.js | 2 +- .../components/Header/OptionMenuSpec.js | 2 +- .../Header/__snapshots__/MainMenuSpec.js.snap | 98 ++++++++ .../__tests__/components/Modal/ModalSpec.tsx | 2 +- .../components/Modal/OptionModalSpec.js | 2 +- .../__tests__/components/Modal/OptionSpec.js | 2 +- .../ValueEditor/ValidateEditorSpec.js | 8 +- .../components/ValueEditor/ValueEditorSpec.js | 8 +- .../__tests__/components/common/ButtonSpec.js | 2 +- .../components/common/DocsLinkSpec.js | 2 +- .../components/common/DropdownSpec.tsx | 2 +- .../components/common/FileChooserSpec.tsx | 2 +- .../components/common/SplitterSpec.js | 2 +- .../components/common/ToggleButtonSpec.js | 2 +- .../common/ToggleInputButtonSpec.js | 2 +- .../components/helpers/AutoScrollSpec.js | 2 +- web/src/js/__tests__/ducks/eventLogSpec.js | 8 +- web/src/js/__tests__/ducks/flowsSpec.js | 4 +- web/src/js/__tests__/ducks/utils/storeSpec.js | 50 ++-- web/src/js/__tests__/test-utils.tsx | 2 +- web/src/js/__tests__/utilsSpec.js | 17 -- web/src/js/{app.jsx => app.tsx} | 9 +- web/src/js/components/CommandBar.tsx | 8 +- web/src/js/components/ContentView.jsx | 59 ----- .../js/components/ContentView/CodeEditor.tsx | 20 -- .../components/ContentView/ContentLoader.tsx | 106 --------- .../ContentView/ContentViewOptions.tsx | 28 --- .../components/ContentView/ContentViews.tsx | 97 -------- .../ContentView/DownloadContentButton.tsx | 19 -- .../js/components/ContentView/MetaViews.tsx | 49 ---- .../ContentView/ShowFullContentButton.tsx | 23 -- .../ContentView/UploadContentButton.tsx | 18 -- .../components/ContentView/ViewSelector.tsx | 26 --- web/src/js/components/FlowTable.jsx | 2 +- .../js/components/FlowTable/FlowColumns.tsx | 154 ++++++------- web/src/js/components/FlowTable/FlowRow.tsx | 4 +- .../js/components/FlowTable/FlowTableHead.tsx | 8 +- web/src/js/components/FlowView.tsx | 78 ++++--- web/src/js/components/FlowView/Connection.tsx | 151 ++++++++++++ web/src/js/components/FlowView/Details.tsx | 173 -------------- web/src/js/components/FlowView/Error.tsx | 20 ++ web/src/js/components/FlowView/Headers.jsx | 162 ------------- .../js/components/FlowView/HttpMessages.tsx | 146 ++++++++++++ web/src/js/components/FlowView/Messages.tsx | 198 ---------------- web/src/js/components/FlowView/Nav.tsx | 47 ---- web/src/js/components/FlowView/Timing.tsx | 94 ++++++++ web/src/js/components/FlowView/ToggleEdit.jsx | 25 -- web/src/js/components/FlowView/WebSocket.tsx | 66 ++++++ web/src/js/components/Footer.tsx | 11 +- web/src/js/components/Header.jsx | 63 ----- web/src/js/components/Header.tsx | 64 ++++++ .../components/Header/ConnectionIndicator.tsx | 2 +- web/src/js/components/Header/FileMenu.tsx | 10 +- web/src/js/components/Header/FlowMenu.tsx | 135 +++++++---- web/src/js/components/Header/MenuToggle.tsx | 27 ++- web/src/js/components/Header/OptionMenu.tsx | 5 +- .../Header/{MainMenu.tsx => StartMenu.tsx} | 6 +- web/src/js/components/MainView.tsx | 2 +- web/src/js/components/Modal/Modal.jsx | 2 +- web/src/js/components/Modal/ModalLayout.jsx | 2 +- web/src/js/components/Modal/ModalList.tsx | 2 +- web/src/js/components/ProxyApp.tsx | 97 +++++++- .../components/ValueEditor/ValidateEditor.tsx | 60 ----- .../js/components/ValueEditor/ValueEditor.jsx | 169 -------------- web/src/js/components/common/Button.tsx | 18 +- web/src/js/components/common/DocsLink.tsx | 3 +- web/src/js/components/common/Dropdown.tsx | 36 ++- web/src/js/components/common/HideInStatic.tsx | 5 +- .../js/components/contentviews/CodeEditor.tsx | 29 +++ .../components/contentviews/HttpMessage.tsx | 106 +++++++++ .../components/contentviews/LineRenderer.tsx | 28 +++ .../components/contentviews/ViewSelector.tsx | 28 +++ .../js/components/contentviews/useContent.ts | 50 ++++ .../components/editors/KeyValueListEditor.tsx | 149 ++++++++++++ .../js/components/editors/ValidateEditor.tsx | 29 +++ web/src/js/components/editors/ValueEditor.tsx | 178 +++++++++++++++ web/src/js/contrib/CodeMirror.tsx | 4 +- web/src/js/ducks/_options_gen.ts | 216 +++++++++--------- web/src/js/ducks/commandBar.ts | 29 +++ web/src/js/ducks/connection.ts | 12 +- web/src/js/ducks/eventLog.ts | 45 ++-- web/src/js/ducks/flows.ts | 156 ++++++------- web/src/js/ducks/index.ts | 2 + web/src/js/ducks/options.ts | 22 +- web/src/js/ducks/ui/flow.ts | 153 +------------ web/src/js/ducks/ui/header.ts | 50 ---- web/src/js/ducks/ui/index.ts | 2 - web/src/js/ducks/ui/keyboard.js | 5 +- web/src/js/ducks/utils/{store.js => store.ts} | 85 ++++--- web/src/js/filt/filt.js | 4 +- web/src/js/filt/filt.peg | 4 +- web/src/js/flow.ts | 42 +++- web/src/js/flow/export.ts | 7 + web/src/js/flow/utils.js | 105 --------- web/src/js/flow/utils.ts | 160 +++++++++++++ web/src/js/{urlState.js => urlState.ts} | 13 +- web/src/js/utils.ts | 72 +++--- web/tsconfig.json | 2 + 139 files changed, 2588 insertions(+), 2457 deletions(-) create mode 100644 mitmproxy/tools/web/static/images/resourceWebSocketIcon.png create mode 100644 web/src/images/resourceWebSocketIcon.png rename web/src/js/{app.jsx => app.tsx} (77%) delete mode 100644 web/src/js/components/ContentView.jsx delete mode 100644 web/src/js/components/ContentView/CodeEditor.tsx delete mode 100644 web/src/js/components/ContentView/ContentLoader.tsx delete mode 100644 web/src/js/components/ContentView/ContentViewOptions.tsx delete mode 100644 web/src/js/components/ContentView/ContentViews.tsx delete mode 100644 web/src/js/components/ContentView/DownloadContentButton.tsx delete mode 100644 web/src/js/components/ContentView/MetaViews.tsx delete mode 100644 web/src/js/components/ContentView/ShowFullContentButton.tsx delete mode 100644 web/src/js/components/ContentView/UploadContentButton.tsx delete mode 100644 web/src/js/components/ContentView/ViewSelector.tsx create mode 100644 web/src/js/components/FlowView/Connection.tsx delete mode 100644 web/src/js/components/FlowView/Details.tsx create mode 100644 web/src/js/components/FlowView/Error.tsx delete mode 100644 web/src/js/components/FlowView/Headers.jsx create mode 100644 web/src/js/components/FlowView/HttpMessages.tsx delete mode 100644 web/src/js/components/FlowView/Messages.tsx delete mode 100644 web/src/js/components/FlowView/Nav.tsx create mode 100644 web/src/js/components/FlowView/Timing.tsx delete mode 100644 web/src/js/components/FlowView/ToggleEdit.jsx create mode 100644 web/src/js/components/FlowView/WebSocket.tsx delete mode 100644 web/src/js/components/Header.jsx create mode 100644 web/src/js/components/Header.tsx rename web/src/js/components/Header/{MainMenu.tsx => StartMenu.tsx} (95%) delete mode 100644 web/src/js/components/ValueEditor/ValidateEditor.tsx delete mode 100644 web/src/js/components/ValueEditor/ValueEditor.jsx create mode 100644 web/src/js/components/contentviews/CodeEditor.tsx create mode 100644 web/src/js/components/contentviews/HttpMessage.tsx create mode 100644 web/src/js/components/contentviews/LineRenderer.tsx create mode 100644 web/src/js/components/contentviews/ViewSelector.tsx create mode 100644 web/src/js/components/contentviews/useContent.ts create mode 100644 web/src/js/components/editors/KeyValueListEditor.tsx create mode 100644 web/src/js/components/editors/ValidateEditor.tsx create mode 100644 web/src/js/components/editors/ValueEditor.tsx create mode 100644 web/src/js/ducks/commandBar.ts delete mode 100644 web/src/js/ducks/ui/header.ts rename web/src/js/ducks/utils/{store.js => store.ts} (66%) create mode 100644 web/src/js/flow/export.ts delete mode 100644 web/src/js/flow/utils.js create mode 100644 web/src/js/flow/utils.ts rename web/src/js/{urlState.js => urlState.ts} (82%) diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index b0aa6f15e..1e241deb5 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -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[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[0-9a-f\-]+)/duplicate", DuplicateFlow), (r"/flows/(?P[0-9a-f\-]+)/replay", ReplayFlow), (r"/flows/(?P[0-9a-f\-]+)/revert", RevertFlow), - (r"/flows/(?P[0-9a-f\-]+)/export/(?P[a-z][a-z_]+).json", ExportFlow), - (r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response)/content.data", FlowContent), + (r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response|messages)/content.data", FlowContent), ( - r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response)/content/(?P[0-9a-zA-Z\-\_]+)(?:\.json)?", + r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response|messages)/content/(?P[0-9a-zA-Z\-\_%]+)(?:\.json)?", FlowContentView), (r"/clear", ClearAll), (r"/options(?:\.json)?", Options), diff --git a/mitmproxy/tools/web/static/images/resourceWebSocketIcon.png b/mitmproxy/tools/web/static/images/resourceWebSocketIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..3fece6c5aa6551ae07fcd4755a3b365a16a84e38 GIT binary patch literal 1428 zcmaJ>drZ?;6#f-tB8n?elz}+Rm=6X%zz3Tau>l1{QW-GGa6xg3LW=|2A?N^M42moT zNfAV4;h|Eg3|qiTH(CUtgIb`aZ%S$DgO*oY%1i8igltj%@Jr4;=ljmdxi`7z-h5=( z30oUy8vp=ogM$LXLGQL~)|TKJP>t&Z9V8|E1Og)H_RIjux~oCaDFCqEVc8%+d4&@I zSVSU2BLcxB2zz^b{}18k=LZVldh|W4l>9HQqAo5jU@JR2JG0sR?%lhkrKPuT-!3jL zE-Wm}&(F`!&YDanuwZU(&S*4(!OYBz!C(LbyYwx{wAV>F%&;0z(c&o%H(se*_l60D;MZo=@)Z=w` zN}*evoRuWK5TjdWwFb!z8XI2O<5fCR!(#0zA0zR4cbBP&oQ{nJ>Sq4L#KidcIM{YL zqs=6lU~Vlo2(`jQia5P>D38(YTGZoJCeEX4r7E3Hr`2k~%09n8l=t6=P|d=`CV6=e z8~#vM-OCQC`N#9FTC5lw8`Eesb2{U|smH3i0p*i^DWOa8oWuNzAS`NEtJNx%s^e2! z_tql*&n>LO4>Xcdl}P^DwMa^Oqf{zKM@RcZAF+-+;Kes`E;jUjbN|&w95;m`m&?Je z6$-`3$cRiP8y+4mEG*=5xqW?o1qB5|Lqk%jG&eUFjYhY&wzjmilLghFc@q_bcaGJK|ES!{2fBsWvdWyP*!l>Y`$VPCz;JZ{iiJZl=sQ%_}T{o<^%Q-g8fKt zbt>`fnxmzS4Z#-9{wG~BB8cFdu?h}27Li&J7hCyB?1l3e&;1yu8di)bWux-VP?-HD zJ4c67e9kU>G2s}bFJl!;od$edch*-Ce|zdL z1$LsWOAdNR`RqI3)6Axj8;_rkwS3itZA><8{5_1dn}nW--=4}(>q-@-cc+Uy=I81j zC(O@RmAN^@?r?xQRXgd3I(z#%E3%vC5sSdAOBYILh-;yV6}4$M5a+{_Q$wye<8HMX zZ->@A)m0!X4PGe*{d$`Myki&BQA8;5D19NMY6;)v{Q4*&Dw5|G4bk63da-9QzfcJ? zwuMhl^?KE#g0^Qe4mPBc}%UPpb*jkcTk|L z3EsRZGP`ESspH|576ek6x1&28wXgwi?D;InrmeP_RSsW!VL>b~C@pI?dxHO$Hw`tI z=zE5oN@vWU#KQpBQtu>FI9~2wyXPJ)URFF>Pz1}|aww@|e}g=;_E`)ity_N}p@;!d zn7Rv~QBQwpPVE)W1Z|79P8Qz|e10fVOulX)2#AmBqYq=2qTC(aw)td;u$3a!B>!t$ zcN*HccpV#Gwk~-w8L#ozoPCM?0!BWDxJXAd>_;9(rtwJdy;kY7<43PTEw}J9tZRp- jimF~$?`!+BY6AxXc*K}4mB+eJm&ZOhFf4$Ch`seMBupFN literal 0 HcmV?d00001 diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index db336fd59..11ac05eb9 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -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("}") diff --git a/web/gulpfile.js b/web/gulpfile.js index fc12e56cc..9022513fe 100644 --- a/web/gulpfile.js +++ b/web/gulpfile.js @@ -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, diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index 200ec46dd..37a2bb6a3 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -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); + } +} diff --git a/web/src/css/flowtable.less b/web/src/css/flowtable.less index 2649ecd89..5a5113cb7 100644 --- a/web/src/css/flowtable.less +++ b/web/src/css/flowtable.less @@ -111,8 +111,12 @@ } .col-path { + .marker { + } + .fa { margin-left: 0; + font-size: 16px; } .fa-repeat { diff --git a/web/src/css/flowview.less b/web/src/css/flowview.less index d33479a03..0f031c560 100644 --- a/web/src/css/flowview.less +++ b/web/src/css/flowview.less @@ -1,6 +1,7 @@ .flowview-image { text-align: center; + padding: 10px 0; img { max-width: 100%; diff --git a/web/src/css/footer.less b/web/src/css/footer.less index 346912070..10241ad9e 100644 --- a/web/src/css/footer.less +++ b/web/src/css/footer.less @@ -1,8 +1,8 @@ footer { box-shadow: 0 -1px 3px lightgray; - padding: 0px 10px 3px; + padding: 0 0 4px 3px; .label { margin-right: 3px; } -} \ No newline at end of file +} diff --git a/web/src/css/header.less b/web/src/css/header.less index d5e6495f7..87c98580e 100644 --- a/web/src/css/header.less +++ b/web/src/css/header.less @@ -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; diff --git a/web/src/css/sprites.less b/web/src/css/sprites.less index 74131c5e7..147f59080 100644 --- a/web/src/css/sprites.less +++ b/web/src/css/sprites.less @@ -43,4 +43,8 @@ .resource-icon-redirect { background-image: url(images/resourceRedirectIcon.png); -} \ No newline at end of file +} + +.resource-icon-websocket { + background-image: url(images/resourceWebSocketIcon.png); +} diff --git a/web/src/css/tabs.less b/web/src/css/tabs.less index 2c2573100..a66d30ed6 100644 --- a/web/src/css/tabs.less +++ b/web/src/css/tabs.less @@ -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; - } } diff --git a/web/src/css/vendor-bootstrap-variables.less b/web/src/css/vendor-bootstrap-variables.less index e2c37bf5a..668fec45d 100644 --- a/web/src/css/vendor-bootstrap-variables.less +++ b/web/src/css/vendor-bootstrap-variables.less @@ -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; diff --git a/web/src/images/resourceWebSocketIcon.png b/web/src/images/resourceWebSocketIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..3fece6c5aa6551ae07fcd4755a3b365a16a84e38 GIT binary patch literal 1428 zcmaJ>drZ?;6#f-tB8n?elz}+Rm=6X%zz3Tau>l1{QW-GGa6xg3LW=|2A?N^M42moT zNfAV4;h|Eg3|qiTH(CUtgIb`aZ%S$DgO*oY%1i8igltj%@Jr4;=ljmdxi`7z-h5=( z30oUy8vp=ogM$LXLGQL~)|TKJP>t&Z9V8|E1Og)H_RIjux~oCaDFCqEVc8%+d4&@I zSVSU2BLcxB2zz^b{}18k=LZVldh|W4l>9HQqAo5jU@JR2JG0sR?%lhkrKPuT-!3jL zE-Wm}&(F`!&YDanuwZU(&S*4(!OYBz!C(LbyYwx{wAV>F%&;0z(c&o%H(se*_l60D;MZo=@)Z=w` zN}*evoRuWK5TjdWwFb!z8XI2O<5fCR!(#0zA0zR4cbBP&oQ{nJ>Sq4L#KidcIM{YL zqs=6lU~Vlo2(`jQia5P>D38(YTGZoJCeEX4r7E3Hr`2k~%09n8l=t6=P|d=`CV6=e z8~#vM-OCQC`N#9FTC5lw8`Eesb2{U|smH3i0p*i^DWOa8oWuNzAS`NEtJNx%s^e2! z_tql*&n>LO4>Xcdl}P^DwMa^Oqf{zKM@RcZAF+-+;Kes`E;jUjbN|&w95;m`m&?Je z6$-`3$cRiP8y+4mEG*=5xqW?o1qB5|Lqk%jG&eUFjYhY&wzjmilLghFc@q_bcaGJK|ES!{2fBsWvdWyP*!l>Y`$VPCz;JZ{iiJZl=sQ%_}T{o<^%Q-g8fKt zbt>`fnxmzS4Z#-9{wG~BB8cFdu?h}27Li&J7hCyB?1l3e&;1yu8di)bWux-VP?-HD zJ4c67e9kU>G2s}bFJl!;od$edch*-Ce|zdL z1$LsWOAdNR`RqI3)6Axj8;_rkwS3itZA><8{5_1dn}nW--=4}(>q-@-cc+Uy=I81j zC(O@RmAN^@?r?xQRXgd3I(z#%E3%vC5sSdAOBYILh-;yV6}4$M5a+{_Q$wye<8HMX zZ->@A)m0!X4PGe*{d$`Myki&BQA8;5D19NMY6;)v{Q4*&Dw5|G4bk63da-9QzfcJ? zwuMhl^?KE#g0^Qe4mPBc}%UPpb*jkcTk|L z3EsRZGP`ESspH|576ek6x1&28wXgwi?D;InrmeP_RSsW!VL>b~C@pI?dxHO$Hw`tI z=zE5oN@vWU#KQpBQtu>FI9~2wyXPJ)URFF>Pz1}|aww@|e}g=;_E`)ity_N}p@;!d zn7Rv~QBQwpPVE)W1Z|79P8Qz|e10fVOulX)2#AmBqYq=2qTC(aw)td;u$3a!B>!t$ zcN*HccpV#Gwk~-w8L#ozoPCM?0!BWDxJXAd>_;9(rtwJdy;kY7<43PTEw}J9tZRp- jimF~$?`!+BY6AxXc*K}4mB+eJm&ZOhFf4$Ch`seMBupFN literal 0 HcmV?d00001 diff --git a/web/src/js/__tests__/components/Command/CommandSpec.js b/web/src/js/__tests__/components/Command/CommandSpec.js index 7cb101dbb..552a115d9 100644 --- a/web/src/js/__tests__/components/Command/CommandSpec.js +++ b/web/src/js/__tests__/components/Command/CommandSpec.js @@ -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(); }); -}) \ No newline at end of file +}) diff --git a/web/src/js/__tests__/components/ContentView/ContentLoaderSpec.js b/web/src/js/__tests__/components/ContentView/ContentLoaderSpec.js index d7083696a..65d99ba5f 100644 --- a/web/src/js/__tests__/components/ContentView/ContentLoaderSpec.js +++ b/web/src/js/__tests__/components/ContentView/ContentLoaderSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/ContentView/ContentViewOptionsSpec.js b/web/src/js/__tests__/components/ContentView/ContentViewOptionsSpec.js index 68afb4ec6..32ce576d2 100644 --- a/web/src/js/__tests__/components/ContentView/ContentViewOptionsSpec.js +++ b/web/src/js/__tests__/components/ContentView/ContentViewOptionsSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/ContentView/ContentViewSpec.js b/web/src/js/__tests__/components/ContentView/ContentViewSpec.js index 9b6f8a66b..f595b0dbb 100644 --- a/web/src/js/__tests__/components/ContentView/ContentViewSpec.js +++ b/web/src/js/__tests__/components/ContentView/ContentViewSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/ContentView/DownloadContentButtonSpec.js b/web/src/js/__tests__/components/ContentView/DownloadContentButtonSpec.js index fd00627d9..652d2ca77 100644 --- a/web/src/js/__tests__/components/ContentView/DownloadContentButtonSpec.js +++ b/web/src/js/__tests__/components/ContentView/DownloadContentButtonSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/ContentView/MetaViewsSpec.js b/web/src/js/__tests__/components/ContentView/MetaViewsSpec.js index 26b2a27cd..d5bdcc8f7 100644 --- a/web/src/js/__tests__/components/ContentView/MetaViewsSpec.js +++ b/web/src/js/__tests__/components/ContentView/MetaViewsSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/ContentView/ShowFullContentButtonSpec.js b/web/src/js/__tests__/components/ContentView/ShowFullContentButtonSpec.js index 56656c7a6..e2b5f1980 100644 --- a/web/src/js/__tests__/components/ContentView/ShowFullContentButtonSpec.js +++ b/web/src/js/__tests__/components/ContentView/ShowFullContentButtonSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/ContentView/UploadContentButtonSpec.js b/web/src/js/__tests__/components/ContentView/UploadContentButtonSpec.js index 3695be727..2a5b0ddec 100644 --- a/web/src/js/__tests__/components/ContentView/UploadContentButtonSpec.js +++ b/web/src/js/__tests__/components/ContentView/UploadContentButtonSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/ContentView/ViewSelectorSpec.tsx b/web/src/js/__tests__/components/ContentView/ViewSelectorSpec.tsx index 6a938be80..d252a2097 100644 --- a/web/src/js/__tests__/components/ContentView/ViewSelectorSpec.tsx +++ b/web/src/js/__tests__/components/ContentView/ViewSelectorSpec.tsx @@ -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' diff --git a/web/src/js/__tests__/components/ContentViewSpec.js b/web/src/js/__tests__/components/ContentViewSpec.js index a3927e7c5..33c78d9e2 100644 --- a/web/src/js/__tests__/components/ContentViewSpec.js +++ b/web/src/js/__tests__/components/ContentViewSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/EventLog/EventListSpec.js b/web/src/js/__tests__/components/EventLog/EventListSpec.js index 4f14dfba7..30e41d336 100644 --- a/web/src/js/__tests__/components/EventLog/EventListSpec.js +++ b/web/src/js/__tests__/components/EventLog/EventListSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/EventLogSpec.js b/web/src/js/__tests__/components/EventLogSpec.js index 1b22c757e..e6a74a546 100644 --- a/web/src/js/__tests__/components/EventLogSpec.js +++ b/web/src/js/__tests__/components/EventLogSpec.js @@ -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' diff --git a/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js index 363522f3d..c6f9e1165 100644 --- a/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js +++ b/web/src/js/__tests__/components/FlowTable/FlowColumnsSpec.js @@ -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(), + let tlsColumn = renderer.create(), tree = tlsColumn.toJSON() expect(tree).toMatchSnapshot() }) it('should render IconColumn', () => { let tflow = TFlow(), - iconColumn = renderer.create(), + iconColumn = renderer.create(), tree = iconColumn.toJSON() // plain expect(tree).toMatchSnapshot() // not modified tflow.response.status_code = 304 - iconColumn = renderer.create() + iconColumn = renderer.create() tree = iconColumn.toJSON() expect(tree).toMatchSnapshot() // redirect tflow.response.status_code = 302 - iconColumn = renderer.create() + iconColumn = renderer.create() tree = iconColumn.toJSON() expect(tree).toMatchSnapshot() // image let imageFlow = TFlow() imageFlow.response.headers = [['Content-Type', 'image/jpeg']] - iconColumn = renderer.create() + iconColumn = renderer.create() tree = iconColumn.toJSON() expect(tree).toMatchSnapshot() // javascript let jsFlow = TFlow() jsFlow.response.headers = [['Content-Type', 'application/x-javascript']] - iconColumn = renderer.create() + iconColumn = renderer.create() tree = iconColumn.toJSON() expect(tree).toMatchSnapshot() // css let cssFlow = TFlow() cssFlow.response.headers = [['Content-Type', 'text/css']] - iconColumn = renderer.create() + iconColumn = renderer.create() tree = iconColumn.toJSON() expect(tree).toMatchSnapshot() // html let htmlFlow = TFlow() htmlFlow.response.headers = [['Content-Type', 'text/html']] - iconColumn = renderer.create() + iconColumn = renderer.create() tree = iconColumn.toJSON() expect(tree).toMatchSnapshot() // default let fooFlow = TFlow() fooFlow.response.headers = [['Content-Type', 'foo']] - iconColumn = renderer.create() + iconColumn = renderer.create() tree = iconColumn.toJSON() expect(tree).toMatchSnapshot() // no response tflow.response = null - iconColumn = renderer.create() + iconColumn = renderer.create() tree = iconColumn.toJSON() expect(tree).toMatchSnapshot() }) it('should render pathColumn', () => { let tflow = TFlow(), - pathColumn = renderer.create(), + pathColumn = renderer.create(), tree = pathColumn.toJSON() expect(tree).toMatchSnapshot() tflow.error.msg = 'Connection killed.' tflow.intercepted = true - pathColumn = renderer.create() + pathColumn = renderer.create() tree = pathColumn.toJSON() expect(tree).toMatchSnapshot() }) it('should render MethodColumn', () => { - let methodColumn = renderer.create(), + let methodColumn = renderer.create(), tree = methodColumn.toJSON() expect(tree).toMatchSnapshot() }) it('should render StatusColumn', () => { - let statusColumn = renderer.create(), + let statusColumn = renderer.create(), tree = statusColumn.toJSON() expect(tree).toMatchSnapshot() }) it('should render SizeColumn', () => { - let sizeColumn = renderer.create(), + let sizeColumn = renderer.create(), tree = sizeColumn.toJSON() expect(tree).toMatchSnapshot() }) it('should render TimeColumn', () => { let tflow = TFlow(), - timeColumn = renderer.create(), + timeColumn = renderer.create(