web: major upgrades

This commit brings a bunch of under-the-hood mitmweb improvements:

 - migrate large parts of the codebase to typescript
 - introduce modern react testing conventions
 - vendor react-codemirror to silence warnings
 - use esbuild for both bundles and tests
 - move from yarn to npm
 - various fixes across the board
This commit is contained in:
Maximilian Hils 2021-06-20 02:12:59 +02:00
parent d6fc9a7b27
commit 9b119c3dac
131 changed files with 12650 additions and 10695 deletions

View File

@ -137,17 +137,20 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- run: git rev-parse --abbrev-ref HEAD - run: git rev-parse --abbrev-ref HEAD
- uses: actions/setup-node@v1 - uses: actions/setup-node@v2
- id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
with: with:
path: ${{ steps.yarn-cache.outputs.dir }} node-version: '14'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - name: Cache Node.js modules
uses: actions/cache@v2
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: | restore-keys: |
${{ runner.os }}-yarn- ${{ runner.OS }}-node-
${{ runner.OS }}-
- working-directory: ./web - working-directory: ./web
run: yarn run: npm ci
- working-directory: ./web - working-directory: ./web
run: npm test run: npm test
- uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27 - uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27

View File

@ -134,7 +134,7 @@ formats = dict(
) )
class Export(): class Export:
def load(self, loader): def load(self, loader):
loader.add_option( loader.add_option(
"export_preserve_original_ip", bool, False, "export_preserve_original_ip", bool, False,

View File

@ -20,8 +20,8 @@ from mitmproxy import io
from mitmproxy import log from mitmproxy import log
from mitmproxy import optmanager from mitmproxy import optmanager
from mitmproxy import version from mitmproxy import version
from mitmproxy.addons import export
from mitmproxy.utils.strutils import always_str from mitmproxy.utils.strutils import always_str
from mitmproxy.addons.export import curl_command
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
@ -30,6 +30,8 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
Args: Args:
flow: The original flow. flow: The original flow.
Sync with web/src/flow.ts.
""" """
f = { f = {
"id": flow.id, "id": flow.id,
@ -43,31 +45,42 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
if flow.client_conn: if flow.client_conn:
f["client_conn"] = { f["client_conn"] = {
"id": flow.client_conn.id, "id": flow.client_conn.id,
"address": flow.client_conn.peername, "peername": flow.client_conn.peername,
"sockname": flow.client_conn.sockname,
"tls_established": flow.client_conn.tls_established, "tls_established": flow.client_conn.tls_established,
"sni": flow.client_conn.sni,
"cipher": flow.client_conn.cipher,
"alpn": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
"tls_version": flow.client_conn.tls_version,
"timestamp_start": flow.client_conn.timestamp_start, "timestamp_start": flow.client_conn.timestamp_start,
"timestamp_tls_setup": flow.client_conn.timestamp_tls_setup, "timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,
"timestamp_end": flow.client_conn.timestamp_end, "timestamp_end": flow.client_conn.timestamp_end,
"sni": flow.client_conn.sni,
# Legacy properties
"address": flow.client_conn.peername,
"cipher_name": flow.client_conn.cipher, "cipher_name": flow.client_conn.cipher,
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"), "alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),
"tls_version": flow.client_conn.tls_version,
} }
if flow.server_conn: if flow.server_conn:
f["server_conn"] = { f["server_conn"] = {
"id": flow.server_conn.id, "id": flow.server_conn.id,
"peername": flow.server_conn.peername,
"sockname": flow.server_conn.sockname,
"address": flow.server_conn.address, "address": flow.server_conn.address,
"ip_address": flow.server_conn.peername,
"source_address": flow.server_conn.sockname,
"tls_established": flow.server_conn.tls_established, "tls_established": flow.server_conn.tls_established,
"sni": flow.server_conn.sni, "sni": flow.server_conn.sni,
"alpn_proto_negotiated": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"), "cipher": flow.server_conn.cipher,
"alpn": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),
"tls_version": flow.server_conn.tls_version, "tls_version": flow.server_conn.tls_version,
"timestamp_start": flow.server_conn.timestamp_start, "timestamp_start": flow.server_conn.timestamp_start,
"timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup, "timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,
"timestamp_tls_setup": flow.server_conn.timestamp_tls_setup, "timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,
"timestamp_end": flow.server_conn.timestamp_end, "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: if flow.error:
f["error"] = flow.error.get_state() f["error"] = flow.error.get_state()
@ -129,12 +142,6 @@ def logentry_to_json(e: log.LogEntry) -> dict:
} }
def cURL_format_to_json(cURL: str):
return {
"export": cURL
}
class APIError(tornado.web.HTTPError): class APIError(tornado.web.HTTPError):
pass pass
@ -212,7 +219,7 @@ class IndexHandler(RequestHandler):
def get(self): def get(self):
token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645 token = self.xsrf_token # https://github.com/tornadoweb/tornado/issues/645
assert token assert token
self.render("index.html") self.render("index.html", static=False, version=version.VERSION)
class FilterHelp(RequestHandler): class FilterHelp(RequestHandler):
@ -274,8 +281,11 @@ class DumpFlows(RequestHandler):
class ExportFlow(RequestHandler): class ExportFlow(RequestHandler):
def post(self, flow_id): def post(self, flow_id, format):
self.write(cURL_format_to_json(curl_command(self.flow))) out = export.formats[format](self.flow)
self.write({
"export": always_str(out, "utf8", "backslashreplace")
})
class ClearAll(RequestHandler): class ClearAll(RequestHandler):
@ -448,42 +458,6 @@ class Events(RequestHandler):
self.write([logentry_to_json(e) for e in self.master.events.data]) self.write([logentry_to_json(e) for e in self.master.events.data])
class Settings(RequestHandler):
def get(self):
self.write(dict(
version=version.VERSION,
mode=str(self.master.options.mode),
intercept_active=self.master.options.intercept_active,
intercept=self.master.options.intercept,
showhost=self.master.options.showhost,
upstream_cert=self.master.options.upstream_cert,
rawtcp=self.master.options.rawtcp,
http2=self.master.options.http2,
websocket=self.master.options.websocket,
anticache=self.master.options.anticache,
anticomp=self.master.options.anticomp,
stickyauth=self.master.options.stickyauth,
stickycookie=self.master.options.stickycookie,
stream=self.master.options.stream_large_bodies,
contentViews=[v.name.replace(' ', '_') for v in contentviews.views],
listen_host=self.master.options.listen_host,
listen_port=self.master.options.listen_port,
server=self.master.options.server,
))
def put(self):
update = self.json
allowed_options = {
"intercept", "showhost", "upstream_cert", "ssl_insecure",
"rawtcp", "http2", "websocket", "anticache", "anticomp",
"stickycookie", "stickyauth", "stream_large_bodies"
}
for k in update:
if k not in allowed_options:
raise APIError(400, f"Unknown setting {k}")
self.master.options.update(**update)
class Options(RequestHandler): class Options(RequestHandler):
def get(self): def get(self):
self.write(optmanager.dump_dicts(self.master.options)) self.write(optmanager.dump_dicts(self.master.options))
@ -514,6 +488,17 @@ class DnsRebind(RequestHandler):
) )
class Conf(RequestHandler):
def get(self):
conf = {
"static": False,
"version": version.VERSION,
"contentViews": [v.name for v in contentviews.views]
}
self.write(f"MITMWEB_CONF = {json.dumps(conf)};")
self.set_header("content-type", "application/javascript")
class Application(tornado.web.Application): class Application(tornado.web.Application):
master: "mitmproxy.tools.web.master.WebMaster" master: "mitmproxy.tools.web.master.WebMaster"
@ -548,14 +533,14 @@ class Application(tornado.web.Application):
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow), (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\-]+)/replay", ReplayFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow), (r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),
(r"/flows/(?P<flow_id>[0-9a-f\-]+)/export", ExportFlow), (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)/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)/content/(?P<content_view>[0-9a-zA-Z\-\_]+)(?:\.json)?",
FlowContentView), FlowContentView),
(r"/settings(?:\.json)?", Settings),
(r"/clear", ClearAll), (r"/clear", ClearAll),
(r"/options(?:\.json)?", Options), (r"/options(?:\.json)?", Options),
(r"/options/save", SaveOptions) (r"/options/save", SaveOptions),
(r"/conf\.js", Conf),
] ]
) )

View File

@ -28,7 +28,6 @@ class WebMaster(master.Master):
self.events.sig_refresh.connect(self._sig_events_refresh) self.events.sig_refresh.connect(self._sig_events_refresh)
self.options.changed.connect(self._sig_options_update) self.options.changed.connect(self._sig_options_update)
self.options.changed.connect(self._sig_settings_update)
self.addons.add(*addons.default_addons()) self.addons.add(*addons.default_addons())
self.addons.add( self.addons.add(
@ -93,13 +92,6 @@ class WebMaster(master.Master):
data=options_dict data=options_dict
) )
def _sig_settings_update(self, options, updated):
app.ClientConnection.broadcast(
resource="settings",
cmd="update",
data={k: getattr(options, k) for k in updated}
)
def run(self): # pragma: no cover def run(self): # pragma: no cover
AsyncIOMainLoop().install() AsyncIOMainLoop().install()
iol = tornado.ioloop.IOLoop.instance() iol = tornado.ioloop.IOLoop.instance()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,8 +7,7 @@
<link rel="stylesheet" href="/static/vendor.css"/> <link rel="stylesheet" href="/static/vendor.css"/>
<link rel="stylesheet" href="/static/app.css"/> <link rel="stylesheet" href="/static/app.css"/>
<link rel="icon" href="/static/images/favicon.ico" type="image/x-icon"/> <link rel="icon" href="/static/images/favicon.ico" type="image/x-icon"/>
<script src="/static/static.js"></script> <script src="/conf.js"></script>
<script src="/static/vendor.js"></script>
<script src="/static/app.js"></script> <script src="/static/app.js"></script>
</head> </head>
<body> <body>

View File

@ -1,12 +1,19 @@
import asyncio import asyncio
import io
import json
import json as _json import json as _json
import logging import logging
import os import re
import sys import sys
import typing
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock from unittest import mock
import pytest import pytest
from mitmproxy.http import Headers
if sys.platform == 'win32': if sys.platform == 'win32':
# workaround for # workaround for
# https://github.com/tornadoweb/tornado/issues/2751 # https://github.com/tornadoweb/tornado/issues/2751
@ -18,7 +25,7 @@ import tornado.testing # noqa
from tornado import httpclient # noqa from tornado import httpclient # noqa
from tornado import websocket # noqa from tornado import websocket # noqa
from mitmproxy import options # noqa from mitmproxy import options, optmanager # noqa
from mitmproxy.test import tflow # noqa from mitmproxy.test import tflow # noqa
from mitmproxy.tools.web import app # noqa from mitmproxy.tools.web import app # noqa
from mitmproxy.tools.web import master as webmaster # noqa from mitmproxy.tools.web import master as webmaster # noqa
@ -35,7 +42,7 @@ def no_tornado_logging():
logging.getLogger('tornado.general').disabled = False logging.getLogger('tornado.general').disabled = False
def json(resp: httpclient.HTTPResponse): def get_json(resp: httpclient.HTTPResponse):
return _json.loads(resp.body.decode()) return _json.loads(resp.body.decode())
@ -82,8 +89,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
def test_flows(self): def test_flows(self):
resp = self.fetch("/flows") resp = self.fetch("/flows")
assert resp.code == 200 assert resp.code == 200
assert json(resp)[0]["request"]["contentHash"] assert get_json(resp)[0]["request"]["contentHash"]
assert json(resp)[1]["error"] assert get_json(resp)[1]["error"]
def test_flows_dump(self): def test_flows_dump(self):
resp = self.fetch("/flows/dump") resp = self.fetch("/flows/dump")
@ -251,7 +258,7 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
f.revert() f.revert()
def test_flow_content_view(self): def test_flow_content_view(self):
assert json(self.fetch("/flows/42/request/content/raw")) == { assert get_json(self.fetch("/flows/42/request/content/raw")) == {
"lines": [ "lines": [
[["text", "content"]] [["text", "content"]]
], ],
@ -261,17 +268,10 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
def test_events(self): def test_events(self):
resp = self.fetch("/events") resp = self.fetch("/events")
assert resp.code == 200 assert resp.code == 200
assert json(resp)[0]["level"] == "info" assert get_json(resp)[0]["level"] == "info"
def test_settings(self):
assert json(self.fetch("/settings"))["mode"] == "regular"
def test_settings_update(self):
assert self.put_json("/settings", {"anticache": True}).code == 200
assert self.put_json("/settings", {"wtf": True}).code == 400
def test_options(self): def test_options(self):
j = json(self.fetch("/options")) j = get_json(self.fetch("/options"))
assert type(j) == dict assert type(j) == dict
assert type(j['anticache']) == dict assert type(j['anticache']) == dict
@ -296,18 +296,8 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
self.master.options.anticomp = True self.master.options.anticomp = True
r1 = yield ws_client.read_message() r1 = yield ws_client.read_message()
r2 = yield ws_client.read_message() response = _json.loads(r1)
j1 = _json.loads(r1) assert response == {
j2 = _json.loads(r2)
response = dict()
response[j1['resource']] = j1
response[j2['resource']] = j2
assert response['settings'] == {
"resource": "settings",
"cmd": "update",
"data": {"anticomp": True},
}
assert response['options'] == {
"resource": "options", "resource": "options",
"cmd": "update", "cmd": "update",
"data": { "data": {
@ -326,23 +316,69 @@ class TestApp(tornado.testing.AsyncHTTPTestCase):
ws_client2 = yield websocket.websocket_connect(ws_url) ws_client2 = yield websocket.websocket_connect(ws_url)
ws_client2.close() ws_client2.close()
def _test_generate_tflow_js(self): def test_generate_tflow_js(self):
_tflow = app.flow_to_json(tflow.tflow(resp=True, err=True)) tf = tflow.tflow(resp=True, err=True)
tf.request.trailers = Headers(trailer="qvalue")
tf.response.trailers = Headers(trailer="qvalue")
_tflow = app.flow_to_json(tf)
# Set some value as constant, so that _tflow.js would not change every time. # Set some value as constant, so that _tflow.js would not change every time.
_tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"
_tflow['id'] = "d91165be-ca1f-4612-88a9-c0f8696f3e29" _tflow['id'] = "d91165be-ca1f-4612-88a9-c0f8696f3e29"
_tflow['client_conn']['id'] = "4a18d1a0-50a1-48dd-9aa6-d45d74282939"
_tflow['server_conn']['id'] = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8" _tflow['server_conn']['id'] = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8"
_tflow["request"]["trailers"] = [["trailer", "qvalue"]]
_tflow["response"]["trailers"] = [["trailer", "qvalue"]]
tflow_json = _json.dumps(_tflow, indent=4, sort_keys=True) tflow_json = _json.dumps(_tflow, indent=4, sort_keys=True)
here = os.path.abspath(os.path.dirname(__file__))
web_root = os.path.join(here, os.pardir, os.pardir, os.pardir, os.pardir, 'web') tflow_json = re.sub(
tflow_path = os.path.join(web_root, 'src/js/__tests__/ducks/_tflow.js') r'( {8}"(address|is_replay|alpn_proto_negotiated)":)',
r" //@ts-ignore\n\1",
tflow_json
).replace(": null", ": undefined")
content = ( content = (
f"/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */\n" "/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */\n"
f"export default function(){{\n" "import {HTTPFlow} from '../../flow';\n"
"export default function(): HTTPFlow {\n"
f" return {tflow_json}\n" f" return {tflow_json}\n"
f"}}" "}"
)
(Path(__file__).parent / "../../../../web/src/js/__tests__/ducks/_tflow.ts").write_bytes(
content.encode()
)
def test_generate_options_js(self):
o = options.Options()
m = webmaster.WebMaster(o)
opt: optmanager._Option
def ts_type(t):
if t == bool:
return "boolean"
if t == str:
return "string"
if t == int:
return "number"
if t == typing.Sequence[str]:
return "string[]"
if t == typing.Optional[str]:
return "string | undefined"
raise RuntimeError(t)
with redirect_stdout(io.StringIO()) as s:
print("/** Auto-generated by test_app.py:TestApp.test_generate_options_js */")
print("export interface OptionsState {")
for _, opt in 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():
print(f" {opt.name}: {json.dumps(opt.default)},".replace(": null", ": undefined"))
print("}")
(Path(__file__).parent / "../../../../web/src/js/ducks/_options_gen.ts").write_bytes(
s.getvalue().encode()
) )
with open(tflow_path, 'w', newline="\n") as f:
f.write(content)

View File

@ -1,22 +1,16 @@
# Quick Start # Quick Start
**Be sure to follow the Development Setup instructions found in the README.md, - Install mitmproxy as described in [`../CONTRIBUTING.md`](../CONTRIBUTING.md)
and activate your virtualenv environment before proceeding.** - Run `node --version` to make sure that you have at least Node.js 14 or above. If you are on **Ubuntu <= 20.04**, you
need to
- Run `yarn` to install dependencies [upgrade](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions).
- Run `yarn run gulp` to start live-compilation. - Run `npm install` to install dependencies
- Run `mitmweb` and open http://localhost:8081/ - Run `npm start` to start live-compilation
- Run `mitmweb` after activating your Python virtualenv (see [`../CONTRIBUTING.md`](../CONTRIBUTING.md)).
## Testing ## Testing
- Run `yarn test` to run the test suite. - Run `npm test` to run the test suite.
## Advanced Tools
- `yarn run gulp` supports live-reloading if you install a matching
[browser extension](http://livereload.com/extensions/).
- You can debug application state using the [Redux DevTools](https://github.com/reduxjs/redux-devtools).
## Architecture ## Architecture
@ -25,3 +19,18 @@ There are two components:
- Server: [`mitmproxy/tools/web`](../mitmproxy/tools/web) - Server: [`mitmproxy/tools/web`](../mitmproxy/tools/web)
- Client: `web` - Client: `web`
## Contributing
We very much appreciate any (small) improvements to mitmweb. Please do *not* include the compiled assets in
[`mitmproxy/tools/web/static`](https://github.com/mitmproxy/mitmproxy/tree/main/mitmproxy/tools/web/static)
in your pull request. Refreshing them on every commit would massively increase repository size. We will update these
files before every release.
## Developer Tools
- `npm start` supports live-reloading if you install a matching
[browser extension](http://livereload.com/extensions/).
- You can debug application state using the
[React DevTools](https://reactjs.org/blog/2019/08/15/new-react-devtools.html) and
[Redux DevTools](https://github.com/reduxjs/redux-devtools) browser extensions.

View File

@ -1,4 +0,0 @@
/* This currently is only used for jest. We use esbuild for actual bundling. */
module.exports = {
presets: ['@babel/preset-react', '@babel/preset-env', '@babel/preset-typescript'],
};

View File

@ -6,6 +6,7 @@ const cleanCSS = require('gulp-clean-css');
const notify = require("gulp-notify"); const notify = require("gulp-notify");
const compilePeg = require("gulp-peg"); const compilePeg = require("gulp-peg");
const plumber = require("gulp-plumber"); const plumber = require("gulp-plumber");
const replace = require('gulp-replace');
const sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
const through = require("through2"); const through = require("through2");
@ -83,6 +84,9 @@ function peg() {
return gulp.src(peg_src, {base: "src/"}) return gulp.src(peg_src, {base: "src/"})
.pipe(plumber(handleError)) .pipe(plumber(handleError))
.pipe(compilePeg()) .pipe(compilePeg())
.pipe(replace('module.exports = ',
'import * as flowutils from "../flow/utils"\n' +
'export default '))
.pipe(gulp.dest("src/")); .pipe(gulp.dest("src/"));
} }

View File

@ -1,6 +1,8 @@
module.exports = async () => {
process.env.TZ = 'UTC'; process.env.TZ = 'UTC';
module.exports = { return {
"testEnvironment": "jsdom", "testEnvironment": "jsdom",
"testRegex": "__tests__/.*Spec.(js|ts)x?$", "testRegex": "__tests__/.*Spec.(js|ts)x?$",
"roots": [ "roots": [
@ -15,5 +17,18 @@ module.exports = {
], ],
"collectCoverageFrom": [ "collectCoverageFrom": [
"src/js/**/*.{js,jsx,ts,tsx}" "src/js/**/*.{js,jsx,ts,tsx}"
],
"transform": {
"^.+\\.[jt]sx?$": [
"esbuild-jest",
{
"loaders": {
".js": "tsx"
},
"format": "cjs",
"sourcemap": true,
}
] ]
}; }
}
}

9696
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,40 +8,38 @@
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"bootstrap": "^3.3.7", "bootstrap": "^3.4.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"codemirror": "^5.62.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mock-xmlhttprequest": "^1.1.0", "mock-xmlhttprequest": "^1.1.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-codemirror": "^1.0.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-popper": "^2.2.5", "react-popper": "^2.2.5",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
"react-test-renderer": "^17.0.2", "react-test-renderer": "^17.0.2",
"redux": "^4.1.0", "redux": "^4.1.0",
"redux-logger": "^3.0.6",
"redux-mock-store": "^1.5.4", "redux-mock-store": "^1.5.4",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"shallowequal": "^1.1.0", "shallowequal": "^1.1.0",
"stable": "^0.1.8" "stable": "^0.1.8"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.3", "@testing-library/react": "^11.2.7",
"@babel/preset-env": "^7.14.4", "@types/jest": "^26.0.23",
"@babel/preset-react": "^7.13.13", "@types/redux-mock-store": "^1.0.2",
"@babel/preset-typescript": "^7.13.0", "esbuild": "^0.12.9",
"babel-jest": "^27.0.2",
"esbuild": "^0.12.8",
"esbuild-jest": "^0.5.0", "esbuild-jest": "^0.5.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-clean-css": "^4.3.0", "gulp-clean-css": "^4.3.0",
"gulp-esbuild": "^0.8.1", "gulp-esbuild": "^0.8.2",
"gulp-less": "^4.0.1", "gulp-less": "^4.0.1",
"gulp-livereload": "^4.0.2", "gulp-livereload": "^4.0.2",
"gulp-notify": "^4.0.0", "gulp-notify": "^4.0.0",
"gulp-peg": "^0.2.0", "gulp-peg": "^0.2.0",
"gulp-plumber": "^1.2.1", "gulp-plumber": "^1.2.1",
"gulp-replace": "^1.1.3",
"gulp-sourcemaps": "^3.0.0", "gulp-sourcemaps": "^3.0.0",
"jest": "^27.0.4", "jest": "^27.0.4",
"through2": "^4.0.2" "through2": "^4.0.2"

View File

@ -1,3 +1,9 @@
.dropdown-menu > li > a { .dropdown-menu {
// setting a margin is not compatible with popper.
margin: 0 !important;
> li > a {
padding: 3px 10px; padding: 3px 10px;
} }
}

View File

@ -96,10 +96,6 @@
.fa { .fa {
line-height: inherit; line-height: inherit;
&.pull-right {
margin-left: 0;
}
} }
.col-tls { .col-tls {
@ -115,6 +111,10 @@
} }
.col-path { .col-path {
.fa {
margin-left: 0;
}
.fa-repeat { .fa-repeat {
color: green; color: green;
} }
@ -193,4 +193,8 @@
.col-quickactions .fa-play { .col-quickactions .fa-play {
transform: translate(1px, 2px); transform: translate(1px, 2px);
} }
.col-quickactions .fa-repeat {
transform: translate(-0px, 2px);
}
} }

View File

@ -104,15 +104,6 @@ header {
.filter-input { .filter-input {
margin: 4px 0; margin: 4px 0;
@media (max-width: @screen-xs-max) {
> .form-control, > .input-group-addon, > .input-group-btn > .btn {
height: 23.5px;
padding: 1px 5px;
font-size: 12px;
line-height: 1.5;
}
}
} }
.filter-input .popover { .filter-input .popover {

View File

@ -1,26 +0,0 @@
jest.mock('react-codemirror')
import React from 'react'
import renderer from 'react-test-renderer'
import CodeEditor from '../../../components/ContentView/CodeEditor'
describe('CodeEditor Component', () => {
let content = "foo content",
changeFn = jest.fn(),
codeEditor = renderer.create(
<CodeEditor content={content} onChange={changeFn}/>
),
tree = codeEditor.toJSON()
it('should render correctly', () => {
// This actually does not render properly, but getting a full CodeMirror rendering
// is cumbersome. This is hopefully good enough.
// see: https://github.com/mitmproxy/mitmproxy/pull/2365#discussion_r119766850
expect(tree).toMatchSnapshot()
})
it('should handle key down', () => {
let mockEvent = { stopPropagation: jest.fn() }
tree.props.onKeyDown(mockEvent)
expect(mockEvent.stopPropagation).toBeCalled()
})
})

View File

@ -0,0 +1,14 @@
jest.mock("../../../contrib/CodeMirror")
import * as React from 'react';
import CodeEditor from '../../../components/ContentView/CodeEditor'
import {render} from '@testing-library/react'
test("CodeEditor", async () => {
const changeFn = jest.fn(),
{asFragment} = render(
<CodeEditor content="foo" onChange={changeFn}/>
);
expect(asFragment()).toMatchSnapshot()
});

View File

@ -42,7 +42,7 @@ describe('ContentLoader Component', () => {
it('should handle componentWillReceiveProps', () => { it('should handle componentWillReceiveProps', () => {
contentLoader.updateContent = jest.fn() contentLoader.updateContent = jest.fn()
contentLoader.componentWillReceiveProps({flow: tflow, message: tflow.request}) contentLoader.UNSAFE_componentWillReceiveProps({flow: tflow, message: tflow.request})
expect(contentLoader.updateContent).toBeCalled() expect(contentLoader.updateContent).toBeCalled()
}) })

View File

@ -53,7 +53,7 @@ describe('ViewServer Component', () => {
it('should handle componentWillReceiveProps', () => { it('should handle componentWillReceiveProps', () => {
// case of fail to parse content // case of fail to parse content
let viewSever = TestUtils.renderIntoDocument( let viewServer = TestUtils.renderIntoDocument(
<PureViewServer <PureViewServer
showFullContent={true} showFullContent={true}
maxLines={10} maxLines={10}
@ -64,10 +64,10 @@ describe('ViewServer Component', () => {
content={JSON.stringify({lines: [['k1', 'v1']]})} content={JSON.stringify({lines: [['k1', 'v1']]})}
/> />
) )
viewSever.componentWillReceiveProps({...viewSever.props, content: '{foo' }) viewServer.UNSAFE_componentWillReceiveProps({...viewServer.props, content: '{foo' })
let e = '' let e = ''
try {JSON.parse('{foo') } catch(err){ e = err.message} try {JSON.parse('{foo') } catch(err){ e = err.message}
expect(viewSever.data).toEqual({ description: e, lines: [] }) expect(viewServer.data).toEqual({ description: e, lines: [] })
}) })
}) })

View File

@ -1,38 +0,0 @@
import React from 'react'
import renderer from 'react-test-renderer'
import ConnectedComponent, { ViewSelector } from '../../../components/ContentView/ViewSelector'
import { Provider } from 'react-redux'
import { TStore } from '../../ducks/tutils'
describe('ViewSelector Component', () => {
let contentViews = ['Auto', 'Raw', 'Text'],
activeView = 'Auto',
setContentViewFn = jest.fn(),
viewSelector = renderer.create(
<ViewSelector contentViews={contentViews} activeView={activeView} setContentView={setContentViewFn}/>
),
tree = viewSelector.toJSON()
it('should render correctly', () => {
expect(tree).toMatchSnapshot()
})
it('should handle click', () => {
let mockEvent = { preventDefault: jest.fn() },
tab = tree.children[1].children[0].children[1]
tab.props.onClick(mockEvent)
expect(mockEvent.preventDefault).toBeCalled()
})
it('should connect to state', () => {
let store = TStore(),
provider = renderer.create(
<Provider store={store}>
<ConnectedComponent/>
</Provider>
),
tree = provider.toJSON()
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,20 @@
import React from 'react'
import renderer from 'react-test-renderer'
import ViewSelector from '../../../components/ContentView/ViewSelector'
import { Provider } from 'react-redux'
import { TStore } from '../../ducks/tutils'
describe('ViewSelector Component', () => {
let store = TStore(),
viewSelector = renderer.create(
<Provider store={store}>
<ViewSelector/>
</Provider>
),
tree = viewSelector.toJSON()
it('should render correctly', () => {
expect(tree).toMatchSnapshot()
})
})

View File

@ -1,8 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeEditor Component should render correctly 1`] = `
<div
className="codeeditor"
onKeyDown={[Function]}
/>
`;

View File

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeEditor 1`] = `
<DocumentFragment>
<div
class="codeeditor"
/>
</DocumentFragment>
`;

View File

@ -3,13 +3,24 @@
exports[`ContentViewOptions Component should render correctly 1`] = ` exports[`ContentViewOptions Component should render correctly 1`] = `
<div <div
className="view-options" className="view-options"
>
<a
className="btn btn-default btn-xs pull-left"
href="#"
onClick={[Function]}
> >
<span> <span>
<b> <b>
View: View:
</b> </b>
edit
auto
<span
className="caret"
/>
</span> </span>
</a>
   
<a <a
className="btn btn-default btn-xs" className="btn btn-default btn-xs"
@ -21,21 +32,9 @@ exports[`ContentViewOptions Component should render correctly 1`] = `
/> />
</a> </a>
   
<a
className="btn btn-default btn-xs"
href="#"
onClick={[Function]}
title="Upload a file to replace the content."
>
<i
className="fa fa-fw fa-upload"
/>
<input
className="hidden"
onChange={[Function]}
type="file"
/>
</a>
   
<span>
foo
</span>
</div> </div>
`; `;

View File

@ -1,123 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewSelector Component should connect to state 1`] = `
<div
className="dropup pull-left"
>
<a
className="btn btn-default btn-xs"
href="#"
onClick={[Function]}
>
<span>
<b>
View:
</b>
auto
<span
className="caret"
/>
</span>
</a>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
onClick={[Function]}
>
auto
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
raw
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
text
</a>
</li>
</ul>
</div>
`;
exports[`ViewSelector Component should render correctly 1`] = `
<div
className="dropup pull-left"
>
<a
className="btn btn-default btn-xs"
href="#"
onClick={[Function]}
>
<span>
<b>
View:
</b>
auto
<span
className="caret"
/>
</span>
</a>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
onClick={[Function]}
>
auto
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
raw
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
text
</a>
</li>
</ul>
</div>
`;

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewSelector Component should render correctly 1`] = `
<a
className="btn btn-default btn-xs pull-left"
href="#"
onClick={[Function]}
>
<span>
<b>
View:
</b>
auto
<span
className="caret"
/>
</span>
</a>
`;

View File

@ -24,7 +24,8 @@ describe('Flowcolumns Components', () => {
}) })
it('should render IconColumn', () => { it('should render IconColumn', () => {
let iconColumn = renderer.create(<IconColumn flow={tflow}/>), let tflow = TFlow(),
iconColumn = renderer.create(<IconColumn flow={tflow}/>),
tree = iconColumn.toJSON() tree = iconColumn.toJSON()
// plain // plain
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
@ -76,7 +77,8 @@ describe('Flowcolumns Components', () => {
}) })
it('should render pathColumn', () => { it('should render pathColumn', () => {
let pathColumn = renderer.create(<PathColumn flow={tflow}/>), let tflow = TFlow(),
pathColumn = renderer.create(<PathColumn flow={tflow}/>),
tree = pathColumn.toJSON() tree = pathColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
@ -100,14 +102,14 @@ describe('Flowcolumns Components', () => {
}) })
it('should render SizeColumn', () => { it('should render SizeColumn', () => {
tflow = TFlow()
let sizeColumn = renderer.create(<SizeColumn flow={tflow}/>), let sizeColumn = renderer.create(<SizeColumn flow={tflow}/>),
tree = sizeColumn.toJSON() tree = sizeColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
it('should render TimeColumn', () => { it('should render TimeColumn', () => {
let timeColumn = renderer.create(<TimeColumn flow={tflow}/>), let tflow = TFlow(),
timeColumn = renderer.create(<TimeColumn flow={tflow}/>),
tree = timeColumn.toJSON() tree = timeColumn.toJSON()
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()

View File

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

View File

@ -0,0 +1,21 @@
import React from 'react'
import FlowRow from '../../../components/FlowTable/FlowRow'
import {testState} from '../../ducks/tutils'
import {fireEvent, render, screen} from "../../test-utils";
import {createAppStore} from "../../../ducks";
test("FlowRow", async () => {
const store = createAppStore(testState),
tflow2 = store.getState().flows.view[1],
{asFragment} = render(<table>
<tbody>
<FlowRow flow={tflow2} selected highlighted/>
</tbody>
</table>, {store})
expect(asFragment()).toMatchSnapshot()
expect(store.getState().flows.selected[0]).toBe(store.getState().flows.view[0].id)
fireEvent.click(screen.getByText("http://address:22/second"))
expect(store.getState().flows.selected[0]).toBe(store.getState().flows.view[1].id)
})

View File

@ -1,35 +1,29 @@
import React from 'react' import React from 'react'
import renderer from 'react-test-renderer' import FlowTableHead from '../../../components/FlowTable/FlowTableHead'
import ConnectedHead, { FlowTableHead } from '../../../components/FlowTable/FlowTableHead'
import {Provider} from 'react-redux' import {Provider} from 'react-redux'
import {TStore} from '../../ducks/tutils' import {TStore} from '../../ducks/tutils'
import {fireEvent, render, screen} from "@testing-library/react";
import {setSort} from "../../../ducks/flows";
describe('FlowTableHead Component', () => { test("FlowTableHead Component", async () => {
let sortFn = jest.fn(),
store = TStore(), const store = TStore(),
flowTableHead = renderer.create( {asFragment} = render(
<Provider store={store}> <Provider store={store}>
<FlowTableHead setSort={sortFn} sortDesc={true}/> <table>
</Provider>), <thead>
tree =flowTableHead.toJSON() <FlowTableHead/>
</thead>
</table>
</Provider>
)
expect(asFragment()).toMatchSnapshot()
it('should render correctly', () => { fireEvent.click(screen.getByText("Size"))
expect(tree).toMatchSnapshot()
})
it('should handle click', () => { expect(store.getActions()).toStrictEqual([
tree.children[0].props.onClick() setSort("SizeColumn", false)
expect(sortFn).toBeCalledWith('TLSColumn', false) ]
}) )
it('should connect to state', () => {
let store = TStore(),
provider = renderer.create(
<Provider store={store}>
<ConnectedHead/>
</Provider>),
tree = provider.toJSON()
expect(tree).toMatchSnapshot()
})
}) })

View File

@ -110,7 +110,16 @@ exports[`Flowcolumns Components should render QuickActionsColumn 1`] = `
onClick={[Function]} onClick={[Function]}
> >
<i <i
className="fa fa-fw fa-ellipsis-h" className="fa fa-fw fa-repeat text-primary"
/>
</a>
<a
className="quickaction"
href="#"
onClick={[Function]}
>
<i
className="fa fa-fw fa-ellipsis-h text-muted"
/> />
</a> </a>
</div> </div>
@ -130,15 +139,17 @@ exports[`Flowcolumns Components should render StatusColumn 1`] = `
className="col-status" className="col-status"
style={ style={
Object { Object {
"color": "darkred", "color": "darkgreen",
} }
} }
/> >
200
</td>
`; `;
exports[`Flowcolumns Components should render TLSColumn 1`] = ` exports[`Flowcolumns Components should render TLSColumn 1`] = `
<td <td
className="col-tls col-tls-http" className="col-tls col-tls-https"
/> />
`; `;

View File

@ -1,68 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlowRow Component should render correctly 1`] = `
<tr
className="has-request has-response"
onClick={[Function]}
>
<td
className="col-tls col-tls-http"
/>
<td
className="col-icon"
>
<div
className="resource-icon resource-icon-plain"
/>
</td>
<td
className="col-path"
>
<i
className="fa fa-fw fa-exclamation pull-right"
/>
http://address:22/path
</td>
<td
className="col-method"
>
GET
</td>
<td
className="col-status"
style={
Object {
"color": "darkgreen",
}
}
>
200
</td>
<td
className="col-size"
>
14b
</td>
<td
className="col-time"
>
3s
</td>
<td
className="col-quickactions"
onClick={[Function]}
>
<div>
<a
className="quickaction"
href="#"
onClick={[Function]}
>
<i
className="fa fa-fw fa-ellipsis-h"
/>
</a>
</div>
</td>
</tr>
`;

View File

@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlowRow 1`] = `
<DocumentFragment>
<table>
<tbody>
<tr
class="selected highlighted has-request has-response"
>
<td
class="col-tls col-tls-https"
/>
<td
class="col-icon"
>
<div
class="resource-icon resource-icon-plain"
/>
</td>
<td
class="col-path"
>
<i
class="fa fa-fw fa-exclamation pull-right"
/>
http://address:22/second
</td>
<td
class="col-method"
>
GET
</td>
<td
class="col-status"
style="color: darkgreen;"
>
200
</td>
<td
class="col-size"
>
14b
</td>
<td
class="col-time"
>
3s
</td>
<td
class="col-quickactions"
>
<div>
<a
class="quickaction"
href="#"
>
<i
class="fa fa-fw fa-repeat text-primary"
/>
</a>
<a
class="quickaction"
href="#"
>
<i
class="fa fa-fw fa-ellipsis-h text-muted"
/>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</DocumentFragment>
`;

View File

@ -1,113 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FlowTableHead Component should connect to state 1`] = ` exports[`FlowTableHead Component 1`] = `
<DocumentFragment>
<table>
<thead>
<tr> <tr>
<th <th
className="col-tls" class="col-tls"
onClick={[Function]} />
>
</th>
<th <th
className="col-icon" class="col-icon"
onClick={[Function]} />
>
</th>
<th <th
className="col-path sort-desc" class="col-path sort-desc"
onClick={[Function]}
> >
Path Path
</th> </th>
<th <th
className="col-method" class="col-method"
onClick={[Function]}
> >
Method Method
</th> </th>
<th <th
className="col-status" class="col-status"
onClick={[Function]}
> >
Status Status
</th> </th>
<th <th
className="col-size" class="col-size"
onClick={[Function]}
> >
Size Size
</th> </th>
<th <th
className="col-time" class="col-time"
onClick={[Function]}
> >
Time Time
</th> </th>
<th <th
className="col-quickactions" class="col-quickactions"
onClick={[Function]} />
>
</th>
</tr>
`;
exports[`FlowTableHead Component should render correctly 1`] = `
<tr>
<th
className="col-tls"
onClick={[Function]}
>
</th>
<th
className="col-icon"
onClick={[Function]}
>
</th>
<th
className="col-path"
onClick={[Function]}
>
Path
</th>
<th
className="col-method"
onClick={[Function]}
>
Method
</th>
<th
className="col-status"
onClick={[Function]}
>
Status
</th>
<th
className="col-timestamp"
onClick={[Function]}
>
TimeStamp
</th>
<th
className="col-size"
onClick={[Function]}
>
Size
</th>
<th
className="col-time"
onClick={[Function]}
>
Time
</th>
<th
className="col-quickactions"
onClick={[Function]}
>
</th>
</tr> </tr>
</thead>
</table>
</DocumentFragment>
`; `;

View File

@ -1,41 +0,0 @@
// jest.mock('../../../ducks/ui/flow')
import React from 'react'
import renderer from 'react-test-renderer'
import ToggleEdit from '../../../components/FlowView/ToggleEdit'
import { Provider } from 'react-redux'
import { startEdit, stopEdit } from '../../../ducks/ui/flow'
import { TFlow, TStore } from '../../ducks/tutils'
global.fetch = jest.fn()
let tflow = new TFlow()
describe('ToggleEdit Component', () => {
let store = TStore(),
provider = renderer.create(
<Provider store={store}>
<ToggleEdit/>
</Provider>),
tree = provider.toJSON()
afterEach(() => { store.clearActions() })
it('should render correctly', () => {
expect(tree).toMatchSnapshot()
})
it('should handle click on stopEdit', () => {
tree.children[0].props.onClick()
expect(fetch).toBeCalled()
})
it('should handle click on startEdit', () => {
store.getState().ui.flow.modifiedFlow = false
let provider = renderer.create(
<Provider store={store}>
<ToggleEdit/>
</Provider>),
tree = provider.toJSON()
tree.children[0].props.onClick()
expect(store.getActions()).toEqual([startEdit(tflow)])
})
})

View File

@ -0,0 +1,21 @@
import React from 'react'
import ToggleEdit from '../../../components/FlowView/ToggleEdit'
import {TFlow} from '../../ducks/tutils'
import {render} from "../../test-utils"
import {fireEvent, screen} from "@testing-library/react";
let tflow = TFlow();
test("ToggleEdit", async () => {
const {asFragment, store} = render(
<ToggleEdit/>,
);
fireEvent.click(screen.getByTitle("Edit Flow"));
expect(asFragment()).toMatchSnapshot();
expect(store.getState().ui.flow.modifiedFlow).toBeTruthy();
fireEvent.click(screen.getByTitle("Finish Edit"));
expect(asFragment()).toMatchSnapshot();
expect(store.getState().ui.flow.modifiedFlow).toBeFalsy();
});

View File

@ -155,18 +155,6 @@ exports[`Details Component should render correctly 1`] = `
TLSv1.2 TLSv1.2
</td> </td>
</tr> </tr>
<tr>
<td>
<abbr
title="ALPN protocol negotiated"
>
ALPN:
</abbr>
</td>
<td>
http/1.1
</td>
</tr>
<tr> <tr>
<td> <td>
Resolved address: Resolved address:

View File

@ -189,69 +189,19 @@ exports[`Request Component should render correctly 1`] = `
<table <table
className="header-table" className="header-table"
> >
<tbody> <tbody />
<tr>
<td
className="header-name"
>
<div
className="inline-input readonly"
dangerouslySetInnerHTML={
Object {
"__html": "trailer",
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
/>
<span
className="header-colon"
>
:
</span>
</td>
<td
className="header-value"
>
<div
className="inline-input readonly"
dangerouslySetInnerHTML={
Object {
"__html": "qvalue",
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
onPaste={[Function]}
/>
</td>
</tr>
</tbody>
</table> </table>
</article> </article>
<footer> <footer>
<div <div
className="view-options" className="view-options"
>
<div
className="dropup pull-left"
> >
<a <a
className="btn btn-default btn-xs" className="btn btn-default btn-xs pull-left"
href="#" href="#"
onClick={[Function]} onClick={[Function]}
> >
<span> <span>
<b> <b>
View: View:
</b> </b>
@ -261,45 +211,8 @@ exports[`Request Component should render correctly 1`] = `
<span <span
className="caret" className="caret"
/> />
</span> </span>
</a> </a>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
onClick={[Function]}
>
auto
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
raw
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
text
</a>
</li>
</ul>
</div>
   
<a <a
className="btn btn-default btn-xs" className="btn btn-default btn-xs"
@ -542,17 +455,13 @@ exports[`Response Component should render correctly 1`] = `
<footer> <footer>
<div <div
className="view-options" className="view-options"
>
<div
className="dropup pull-left"
> >
<a <a
className="btn btn-default btn-xs" className="btn btn-default btn-xs pull-left"
href="#" href="#"
onClick={[Function]} onClick={[Function]}
> >
<span> <span>
<b> <b>
View: View:
</b> </b>
@ -562,45 +471,8 @@ exports[`Response Component should render correctly 1`] = `
<span <span
className="caret" className="caret"
/> />
</span> </span>
</a> </a>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
onClick={[Function]}
>
auto
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
raw
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
text
</a>
</li>
</ul>
</div>
   
<a <a
className="btn btn-default btn-xs" className="btn btn-default btn-xs"

View File

@ -1,17 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ToggleEdit Component should render correctly 1`] = `
<div
className="edit-flow-container"
>
<a
className="edit-flow"
onClick={[Function]}
title="Finish Edit"
>
<i
className="fa fa-check"
/>
</a>
</div>
`;

View File

@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ToggleEdit 1`] = `
<DocumentFragment>
<div
class="edit-flow-container"
>
<a
class="edit-flow"
title="Finish Edit"
>
<i
class="fa fa-check"
/>
</a>
</div>
</DocumentFragment>
`;
exports[`ToggleEdit 2`] = `
<DocumentFragment>
<div
class="edit-flow-container"
>
<a
class="edit-flow"
title="Edit Flow"
>
<i
class="fa fa-pencil"
/>
</a>
</div>
</DocumentFragment>
`;

View File

@ -1,55 +0,0 @@
import React from 'react'
import renderer from 'react-test-renderer'
import ConnectedIndicator, { ConnectionIndicator } from '../../../components/Header/ConnectionIndicator'
import { ConnectionState } from '../../../ducks/connection'
import { Provider } from 'react-redux'
import { TStore } from '../../ducks/tutils'
describe('ConnectionIndicator Component', () => {
it('should render INIT', () => {
let connectionIndicator = renderer.create(
<ConnectionIndicator state={ConnectionState.INIT}/>),
tree = connectionIndicator.toJSON()
expect(tree).toMatchSnapshot()
})
it('should render FETCHING', () => {
let connectionIndicator = renderer.create(
<ConnectionIndicator state={ConnectionState.FETCHING}/>),
tree = connectionIndicator.toJSON()
expect(tree).toMatchSnapshot()
})
it('should render ESTABLISHED', () => {
let connectionIndicator = renderer.create(
<ConnectionIndicator state={ConnectionState.ESTABLISHED}/>),
tree = connectionIndicator.toJSON()
expect(tree).toMatchSnapshot()
})
it('should render ERROR', () => {
let connectionIndicator = renderer.create(
<ConnectionIndicator state={ConnectionState.ERROR} message="foo"/>),
tree = connectionIndicator.toJSON()
expect(tree).toMatchSnapshot()
})
it('should render OFFLINE', () => {
let connectionIndicator = renderer.create(
<ConnectionIndicator state={ConnectionState.OFFLINE} />),
tree = connectionIndicator.toJSON()
expect(tree).toMatchSnapshot()
})
it('should connect to state', () => {
let store = TStore(),
provider = renderer.create(
<Provider store={store}>
<ConnectedIndicator/>
</Provider>),
tree = provider.toJSON()
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,22 @@
import React from 'react'
import ConnectionIndicator from '../../../components/Header/ConnectionIndicator'
import * as connectionActions from '../../../ducks/connection'
import {render} from "../../test-utils"
test("ConnectionIndicator", async () => {
const {asFragment, store} = render(<ConnectionIndicator/>);
expect(asFragment()).toMatchSnapshot()
store.dispatch(connectionActions.startFetching())
expect(asFragment()).toMatchSnapshot()
store.dispatch(connectionActions.connectionEstablished())
expect(asFragment()).toMatchSnapshot()
store.dispatch(connectionActions.connectionError("wat"))
expect(asFragment()).toMatchSnapshot()
store.dispatch(connectionActions.setOffline())
expect(asFragment()).toMatchSnapshot()
});

View File

@ -1,52 +1,20 @@
import React from 'react' import React from 'react'
import renderer from 'react-test-renderer' import renderer from 'react-test-renderer'
import { FileMenu } from '../../../components/Header/FileMenu' import FileMenu from '../../../components/Header/FileMenu'
import {Provider} from "react-redux";
global.confirm = jest.fn( s => true ) import {TStore} from "../../ducks/tutils";
describe('FileMenu Component', () => { describe('FileMenu Component', () => {
let clearFn = jest.fn(),
loadFn = jest.fn(), let store = TStore(),
saveFn = jest.fn(),
openModalFn = jest.fn(),
mockEvent = {
preventDefault: jest.fn(),
target: { files: ["foo", "bar "] }
},
createNodeMock = () => { return { click: jest.fn() }},
fileMenu = renderer.create( fileMenu = renderer.create(
<FileMenu <Provider store={store}>
clearFlows={clearFn} <FileMenu/>
loadFlows={loadFn} </Provider>
saveFlows={saveFn} ),
openModal={openModalFn}
/>,
{ createNodeMock }),
tree = fileMenu.toJSON() tree = fileMenu.toJSON()
it('should render correctly', () => { it('should render correctly', () => {
expect(tree).toMatchSnapshot() expect(tree).toMatchSnapshot()
}) })
let ul = tree.children[1]
it('should clear flows', () => {
let a = ul.children[0].children[1]
a.props.onClick(mockEvent)
expect(mockEvent.preventDefault).toBeCalled()
expect(clearFn).toBeCalled()
})
it('should load flows', () => {
let fileChooser = ul.children[1].children[1],
input = fileChooser.children[2]
input.props.onChange(mockEvent)
expect(loadFn).toBeCalledWith("foo")
})
it('should save flows', () => {
let a = ul.children[2].children[1]
a.props.onClick(mockEvent)
expect(saveFn).toBeCalled()
})
}) })

View File

@ -1,26 +1,8 @@
jest.mock('../../../ducks/settings')
import React from 'react' import React from 'react'
import renderer from 'react-test-renderer' import MainMenu from '../../../components/Header/MainMenu'
import MainMenu, { setIntercept } from '../../../components/Header/MainMenu' import {render} from "../../test-utils"
import { Provider } from 'react-redux'
import { update as updateSettings } from '../../../ducks/settings'
import { TStore } from '../../ducks/tutils'
describe('MainMenu Component', () => { test("MainMenu", () => {
let store = TStore() const {asFragment} = render(<MainMenu/>);
expect(asFragment()).toMatchSnapshot();
it('should render and connect to state', () => {
let provider = renderer.create(
<Provider store={store}>
<MainMenu/>
</Provider>),
tree = provider.toJSON()
expect(tree).toMatchSnapshot()
})
it('should handle change on interceptInput', () => {
setIntercept('foo')
expect(updateSettings).toBeCalledWith({ intercept: 'foo' })
})
}) })

View File

@ -1,11 +1,10 @@
import React from 'react' import React from 'react'
import renderer from 'react-test-renderer' import renderer from 'react-test-renderer'
import { MenuToggle, SettingsToggle, EventlogToggle } from '../../../components/Header/MenuToggle' import {EventlogToggle, MenuToggle, OptionsToggle} from '../../../components/Header/MenuToggle'
import {Provider} from 'react-redux' import {Provider} from 'react-redux'
import { REQUEST_UPDATE } from '../../../ducks/settings'
import {TStore} from '../../ducks/tutils' import {TStore} from '../../ducks/tutils'
import * as optionsEditorActions from "../../../ducks/ui/optionsEditor"
global.fetch = jest.fn() import {fireEvent, render, screen} from "../../test-utils"
describe('MenuToggle Component', () => { describe('MenuToggle Component', () => {
it('should render correctly', () => { it('should render correctly', () => {
@ -19,37 +18,26 @@ describe('MenuToggle Component', () => {
}) })
}) })
describe('SettingToggle Component', () => { test("OptionsToggle", async () => {
let store = TStore(), const store = TStore(),
provider = renderer.create( {asFragment} = render(
<Provider store={store}> <OptionsToggle name='anticache'>toggle anticache</OptionsToggle>,
<SettingsToggle setting='anticache'> {store}
<p>foo children</p> );
</SettingsToggle>
</Provider>),
tree = provider.toJSON()
it('should render and connect to state', () => { expect(asFragment()).toMatchSnapshot();
expect(tree).toMatchSnapshot() fireEvent.click(screen.getByText("toggle anticache"));
}) expect(store.getActions()).toEqual([optionsEditorActions.startUpdate("anticache", true)])
});
it('should handle change', () => { test("EventlogToggle", async () => {
let menuToggle = tree.children[0].children[0] const {asFragment, store} = render(
menuToggle.props.onChange() <EventlogToggle/>
expect(store.getActions()).toEqual([{ type: REQUEST_UPDATE }]) );
}) expect(asFragment()).toMatchSnapshot();
})
describe('EventlogToggle Component', () => { expect(store.getState().eventLog.visible).toBeTruthy();
let store = TStore(), fireEvent.click(screen.getByText("Display Event Log"));
changFn = jest.fn(),
provider = renderer.create( expect(store.getState().eventLog.visible).toBeFalsy();
<Provider store={store}>
<EventlogToggle value={false} onChange={changFn}/>
</Provider>
),
tree = provider.toJSON()
it('should render and connect to state', () => {
expect(tree).toMatchSnapshot()
})
}) })

View File

@ -1,50 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConnectionIndicator Component should connect to state 1`] = `
<span
className="connection-indicator established"
>
connected
</span>
`;
exports[`ConnectionIndicator Component should render ERROR 1`] = `
<span
className="connection-indicator error"
title="foo"
>
connection lost
</span>
`;
exports[`ConnectionIndicator Component should render ESTABLISHED 1`] = `
<span
className="connection-indicator established"
>
connected
</span>
`;
exports[`ConnectionIndicator Component should render FETCHING 1`] = `
<span
className="connection-indicator fetching"
>
fetching data…
</span>
`;
exports[`ConnectionIndicator Component should render INIT 1`] = `
<span
className="connection-indicator init"
>
connecting…
</span>
`;
exports[`ConnectionIndicator Component should render OFFLINE 1`] = `
<span
className="connection-indicator offline"
>
offline
</span>
`;

View File

@ -0,0 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConnectionIndicator 1`] = `
<DocumentFragment>
<span
class="connection-indicator established"
>
connected
</span>
</DocumentFragment>
`;
exports[`ConnectionIndicator 2`] = `
<DocumentFragment>
<span
class="connection-indicator fetching"
>
fetching data…
</span>
</DocumentFragment>
`;
exports[`ConnectionIndicator 3`] = `
<DocumentFragment>
<span
class="connection-indicator established"
>
connected
</span>
</DocumentFragment>
`;
exports[`ConnectionIndicator 4`] = `
<DocumentFragment>
<span
class="connection-indicator error"
title="wat"
>
connection lost
</span>
</DocumentFragment>
`;
exports[`ConnectionIndicator 5`] = `
<DocumentFragment>
<span
class="connection-indicator offline"
>
offline
</span>
</DocumentFragment>
`;

View File

@ -1,80 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileMenu Component should render correctly 1`] = ` exports[`FileMenu Component should render correctly 1`] = `
<div
className="dropdown pull-left"
>
<a <a
className="special" className="pull-left special"
href="#" href="#"
onClick={[Function]} onClick={[Function]}
> >
mitmproxy mitmproxy
</a> </a>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
onClick={[Function]}
>
<i
className="fa fa-fw fa-trash"
/>
 Clear All
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
<i
className="fa fa-fw fa-folder-open"
/>
 Open...
<input
className="hidden"
onChange={[Function]}
type="file"
/>
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
<i
className="fa fa-fw fa-floppy-o"
/>
 Save...
</a>
</li>
<li>
<hr
className="divider"
/>
<a
href="http://mitm.it/"
target="_blank"
>
<i
className="fa fa-fw fa-external-link"
/>
 Install Certificates...
</a>
</li>
</ul>
</div>
`; `;

View File

@ -1,122 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MainMenu Component should render and connect to state 1`] = ` exports[`MainMenu 1`] = `
<DocumentFragment>
<div <div
className="main-menu" class="main-menu"
> >
<div <div
className="menu-group" class="menu-group"
> >
<div <div
className="menu-content" class="menu-content"
> >
<div <div
className="filter-input input-group" class="filter-input input-group"
> >
<span <span
className="input-group-addon" class="input-group-addon"
> >
<i <i
className="fa fa-fw fa-search" class="fa fa-fw fa-search"
style={ style="color: black;"
Object {
"color": "black",
}
}
/> />
</span> </span>
<input <input
className="form-control" class="form-control"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Search" placeholder="Search"
type="text" type="text"
value="~u foo" value="~d address"
/> />
</div> </div>
<div <div
className="filter-input input-group" class="filter-input input-group"
> >
<span <span
className="input-group-addon" class="input-group-addon"
> >
<i <i
className="fa fa-fw fa-tag" class="fa fa-fw fa-tag"
style={ style="color: rgb(0, 0, 0);"
Object {
"color": "hsl(48, 100%, 50%)",
}
}
/> />
</span> </span>
<input <input
className="form-control" class="form-control"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Highlight" placeholder="Highlight"
type="text" type="text"
value="~a bar" value="~u /path"
/> />
</div> </div>
</div> </div>
<div <div
className="menu-legend" class="menu-legend"
> >
Find Find
</div> </div>
</div> </div>
<div <div
className="menu-group" class="menu-group"
> >
<div <div
className="menu-content" class="menu-content"
> >
<div <div
className="filter-input input-group" class="filter-input input-group"
> >
<span <span
className="input-group-addon" class="input-group-addon"
> >
<i <i
className="fa fa-fw fa-pause" class="fa fa-fw fa-pause"
style={ style="color: rgb(68, 68, 68);"
Object {
"color": "hsl(208, 56%, 53%)",
}
}
/> />
</span> </span>
<input <input
className="form-control" class="form-control"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Intercept" placeholder="Intercept"
type="text" type="text"
value="" value=""
/> />
</div> </div>
<button <button
className="btn-sm btn btn-default" class="btn-sm btn btn-default"
onClick={[Function]}
title="[a]ccept all" title="[a]ccept all"
> >
<i <i
className="fa fa-fw fa-forward text-success" class="fa fa-fw fa-forward text-success"
/> />
Resume All Resume All
</button> </button>
</div> </div>
<div <div
className="menu-legend" class="menu-legend"
> >
Intercept Intercept
</div> </div>
</div> </div>
</div> </div>
</DocumentFragment>
`; `;

View File

@ -1,18 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EventlogToggle Component should render and connect to state 1`] = ` exports[`EventlogToggle 1`] = `
<DocumentFragment>
<div <div
className="menu-entry" class="menu-entry"
> >
<label> <label>
<input <input
checked={true} checked=""
onChange={[Function]}
type="checkbox" type="checkbox"
/> />
Display Event Log Display Event Log
</label> </label>
</div> </div>
</DocumentFragment>
`; `;
exports[`MenuToggle Component should render correctly 1`] = ` exports[`MenuToggle Component should render correctly 1`] = `
@ -32,19 +33,17 @@ exports[`MenuToggle Component should render correctly 1`] = `
</div> </div>
`; `;
exports[`SettingToggle Component should render and connect to state 1`] = ` exports[`OptionsToggle 1`] = `
<DocumentFragment>
<div <div
className="menu-entry" class="menu-entry"
> >
<label> <label>
<input <input
checked={true}
onChange={[Function]}
type="checkbox" type="checkbox"
/> />
<p> toggle anticache
foo children
</p>
</label> </label>
</div> </div>
</DocumentFragment>
`; `;

View File

@ -39,7 +39,7 @@ exports[`OptionMenu Component should render correctly 1`] = `
> >
<label> <label>
<input <input
checked={true} checked={false}
onChange={[Function]} onChange={[Function]}
type="checkbox" type="checkbox"
/> />

View File

@ -1,30 +0,0 @@
import React from 'react'
import renderer from 'react-test-renderer'
import Modal from '../../../components/Modal/Modal'
import { Provider } from 'react-redux'
import { TStore } from '../../ducks/tutils'
describe('Modal Component', () => {
let store = TStore()
it('should render correctly', () => {
// hide modal by default
let provider = renderer.create(
<Provider store={store}>
<Modal/>
</Provider>
),
tree = provider.toJSON()
expect(tree).toMatchSnapshot()
// option modal show up
store.getState().ui.modal.activeModal = 'OptionModal'
provider = renderer.create(
<Provider store={store}>
<Modal/>
</Provider>
)
tree = provider.toJSON()
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,13 @@
import React from 'react'
import Modal from '../../../components/Modal/Modal'
import {render} from "../../test-utils"
import {setActiveModal} from "../../../ducks/ui/modal";
test("Modal Component", async () => {
const {asFragment, store} = render(<Modal/>);
expect(asFragment()).toMatchSnapshot();
store.dispatch(setActiveModal("OptionModal"));
expect(asFragment()).toMatchSnapshot();
})

View File

@ -1,255 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Modal Component should render correctly 1`] = `<div />`;
exports[`Modal Component should render correctly 2`] = `
<div>
<div
className="modal-backdrop fade in"
/>
<div
aria-labelledby="options"
className="modal modal-visible"
id="optionsModal"
role="dialog"
tabIndex="-1"
>
<div
className="modal-dialog modal-lg"
role="document"
>
<div
className="modal-content"
>
<div>
<div
className="modal-header"
>
<button
className="close"
data-dismiss="modal"
onClick={[Function]}
type="button"
>
<i
className="fa fa-fw fa-times"
/>
</button>
<div
className="modal-title"
>
<h4>
Options
</h4>
</div>
</div>
<div
className="modal-body"
>
<div
className="form-horizontal"
>
<div
className="form-group"
>
<div
className="col-xs-6"
>
<label
htmlFor="booleanOption"
>
booleanOption
</label>
<div
className="help-block small"
>
foo
</div>
</div>
<div
className="col-xs-6"
>
<div
className=""
>
<div
className="checkbox"
>
<label>
<input
checked={false}
name="booleanOption"
onChange={[Function]}
onKeyDown={[Function]}
type="checkbox"
/>
Enable
</label>
</div>
</div>
</div>
</div>
<div
className="form-group"
>
<div
className="col-xs-6"
>
<label
htmlFor="choiceOption"
>
choiceOption
</label>
<div
className="help-block small"
>
foo
</div>
</div>
<div
className="col-xs-6"
>
<div
className=""
>
<select
className="form-control"
name="choiceOption"
onChange={[Function]}
onKeyDown={[Function]}
value="b"
>
<option
value="a"
>
a
</option>
<option
value="b"
>
b
</option>
<option
value="c"
>
c
</option>
</select>
</div>
<div
className="small"
>
Default:
<strong>
a
</strong>
</div>
</div>
</div>
<div
className="form-group"
>
<div
className="col-xs-6"
>
<label
htmlFor="intOption"
>
intOption
</label>
<div
className="help-block small"
>
foo
</div>
</div>
<div
className="col-xs-6"
>
<div
className=""
>
<input
className="form-control"
name="intOption"
onChange={[Function]}
onKeyDown={[Function]}
type="number"
value={1}
/>
</div>
<div
className="small"
>
Default:
<strong>
0
</strong>
</div>
</div>
</div>
<div
className="form-group"
>
<div
className="col-xs-6"
>
<label
htmlFor="strOption"
>
strOption
</label>
<div
className="help-block small"
>
foo
</div>
</div>
<div
className="col-xs-6"
>
<div
className="has-error"
>
<input
className="form-control"
name="strOption"
onChange={[Function]}
onKeyDown={[Function]}
type="text"
value="str content"
/>
</div>
<div
className="small text-danger"
/>
<div
className="small"
>
Default:
<strong>
null
</strong>
</div>
</div>
</div>
</div>
</div>
<div
className="modal-footer"
/>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,209 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Modal Component 1`] = `
<DocumentFragment>
<div />
</DocumentFragment>
`;
exports[`Modal Component 2`] = `
<DocumentFragment>
<div>
<div
class="modal-backdrop fade in"
/>
<div
aria-labelledby="options"
class="modal modal-visible"
id="optionsModal"
role="dialog"
tabindex="-1"
>
<div
class="modal-dialog modal-lg"
role="document"
>
<div
class="modal-content"
>
<div>
<div
class="modal-header"
>
<button
class="close"
data-dismiss="modal"
type="button"
>
<i
class="fa fa-fw fa-times"
/>
</button>
<div
class="modal-title"
>
<h4>
Options
</h4>
</div>
</div>
<div
class="modal-body"
>
<div
class="form-horizontal"
>
<div
class="form-group"
>
<div
class="col-xs-6"
>
<label
for="anticache"
>
anticache
</label>
<div
class="help-block small"
>
Strip out request headers that might cause the server to return 304-not-modified.
</div>
</div>
<div
class="col-xs-6"
>
<div
class=""
>
<div
class="checkbox"
>
<label>
<input
name="anticache"
type="checkbox"
/>
Enable
</label>
</div>
</div>
</div>
</div>
<div
class="form-group"
>
<div
class="col-xs-6"
>
<label
for="body_size_limit"
>
body_size_limit
</label>
<div
class="help-block small"
>
Byte size limit of HTTP request and response bodies. Understands k/m/g suffixes, i.e. 3m for 3 megabytes.
</div>
</div>
<div
class="col-xs-6"
>
<div
class=""
>
<input
class="form-control"
name="body_size_limit"
type="text"
value=""
/>
</div>
</div>
</div>
<div
class="form-group"
>
<div
class="col-xs-6"
>
<label
for="connection_strategy"
>
connection_strategy
</label>
<div
class="help-block small"
>
Determine when server connections should be established. When set to lazy, mitmproxy tries to defer establishing an upstream connection as long as possible. This makes it possible to use server replay while being offline. When set to eager, mitmproxy can detect protocols with server-side greetings, as well as accurately mirror TLS ALPN negotiation.
</div>
</div>
<div
class="col-xs-6"
>
<div
class=""
>
<select
class="form-control"
name="connection_strategy"
>
<option
value="eager"
>
eager
</option>
<option
value="lazy"
>
lazy
</option>
</select>
</div>
</div>
</div>
<div
class="form-group"
>
<div
class="col-xs-6"
>
<label
for="listen_port"
>
listen_port
</label>
<div
class="help-block small"
>
Proxy service port.
</div>
</div>
<div
class="col-xs-6"
>
<div
class=""
>
<input
class="form-control"
name="listen_port"
type="number"
value="8080"
/>
</div>
</div>
</div>
</div>
</div>
<div
class="modal-footer"
/>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -1,32 +0,0 @@
import React from 'react'
import renderer from 'react-test-renderer'
import Dropdown, { Divider } from '../../../components/common/Dropdown'
describe('Dropdown Component', () => {
let dropdown = renderer.create(<Dropdown text="open me">
<a href="#">1</a>
<a href="#">2</a>
</Dropdown>)
it('should render correctly', () => {
let tree = dropdown.toJSON()
expect(tree).toMatchSnapshot()
})
it('should handle open/close action', () => {
let tree = dropdown.toJSON(),
e = { preventDefault: jest.fn(), stopPropagation: jest.fn() }
tree.children[0].props.onClick(e)
expect(tree).toMatchSnapshot()
// click action when the state is open
tree.children[0].props.onClick(e)
// open again
tree.children[0].props.onClick(e)
// close
document.body.click()
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,53 @@
import React from "react"
import Dropdown, {Divider, MenuItem, SubMenu} from '../../../components/common/Dropdown'
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
test('Dropdown', async () => {
let onOpen = jest.fn();
const {asFragment} = render(
<Dropdown text="open me" onOpen={onOpen}>
<MenuItem onClick={() => 0}>click me</MenuItem>
<Divider/>
<MenuItem onClick={() => 0}>click me</MenuItem>
</Dropdown>
)
expect(asFragment()).toMatchSnapshot()
fireEvent.click(screen.getByText("open me"))
await waitFor(() => expect(onOpen).toBeCalledWith(true))
expect(asFragment()).toMatchSnapshot()
/*
onOpen.mockClear()
fireEvent.click(document.body)
await waitFor(() => expect(onOpen).toBeCalledWith(false))
*/
})
test('SubMenu', async () => {
const {asFragment} = render(
<SubMenu title="submenu">
<MenuItem onClick={() => 0}>click me</MenuItem>
</SubMenu>
)
expect(asFragment()).toMatchSnapshot()
fireEvent.mouseEnter(screen.getByText("submenu"))
await waitFor(() => screen.getByText("click me"))
expect(asFragment()).toMatchSnapshot()
fireEvent.mouseLeave(screen.getByText("submenu"))
expect(screen.queryByText("click me")).toBeNull()
expect(asFragment()).toMatchSnapshot()
})
test('MenuItem', async () => {
let click = jest.fn();
const {asFragment} = render(
<MenuItem onClick={click}>click me</MenuItem>
)
expect(asFragment()).toMatchSnapshot()
fireEvent.click(screen.getByText("click me"))
expect(click).toBeCalled()
})

View File

@ -1,38 +0,0 @@
import React from 'react'
import renderer from 'react-test-renderer'
import FileChooser from '../../../components/common/FileChooser'
describe('FileChooser Component', () => {
let openFileFunc = jest.fn(),
createNodeMock = () => { return { click: jest.fn() } },
fileChooser = renderer.create(
<FileChooser className="foo" title="bar" onOpenFile={ openFileFunc }/>
, { createNodeMock })
//[test refs with react-test-renderer](https://github.com/facebook/react/issues/7371)
it('should render correctly', () => {
let tree = fileChooser.toJSON()
expect(tree).toMatchSnapshot()
})
it('should handle click action', () => {
let tree = fileChooser.toJSON(),
mockEvent = {
preventDefault: jest.fn(),
target: {
files: [ "foo", "bar" ]
}
}
tree.children[1].props.onChange(mockEvent)
expect(openFileFunc).toBeCalledWith("foo")
tree.props.onClick()
// without files
mockEvent = {
...mockEvent,
target: { files: [ ]}
}
openFileFunc.mockClear()
tree.children[1].props.onChange(mockEvent)
expect(openFileFunc).not.toBeCalled()
})
})

View File

@ -0,0 +1,13 @@
import React from 'react'
import FileChooser from '../../../components/common/FileChooser'
import {render} from '@testing-library/react'
test("FileChooser", async () => {
const {asFragment} = render(
<FileChooser icon="play" text="open" onOpenFile={() => 0}/>
);
expect(asFragment()).toMatchSnapshot()
})

View File

@ -1,162 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dropdown Component should handle open/close action 1`] = `
<div
className="dropup"
>
<a
className="foo"
href="#"
onClick={[Function]}
/>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
>
1
</a>
</li>
<li>
<hr
className="divider"
/>
</li>
<li>
<a
href="#"
>
2
</a>
</li>
</ul>
</div>
`;
exports[`Dropdown Component should handle open/close action 2`] = `
<div
className="dropup"
>
<a
className="foo"
href="#"
onClick={[Function]}
/>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
>
1
</a>
</li>
<li>
<hr
className="divider"
/>
</li>
<li>
<a
href="#"
>
2
</a>
</li>
</ul>
</div>
`;
exports[`Dropdown Component should render correctly 1`] = `
<div
className="dropup"
>
<a
className="foo"
href="#"
onClick={[Function]}
/>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
>
1
</a>
</li>
<li>
<hr
className="divider"
/>
</li>
<li>
<a
href="#"
>
2
</a>
</li>
</ul>
</div>
`;
exports[`Dropdown Component should render correctly 2`] = `
<div
className="dropdown"
>
<a
className="foo"
href="#"
onClick={[Function]}
/>
<ul
className="dropdown-menu"
role="menu"
>
<li>
<a
href="#"
>
1
</a>
</li>
<li>
<a
href="#"
>
2
</a>
</li>
</ul>
</div>
`;

View File

@ -0,0 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dropdown 1`] = `
<DocumentFragment>
<a
class=""
href="#"
>
open me
</a>
</DocumentFragment>
`;
exports[`Dropdown 2`] = `
<DocumentFragment>
<a
class="open"
href="#"
>
open me
</a>
<ul
class="dropdown-menu show"
data-popper-escaped="true"
data-popper-placement="bottom"
data-popper-reference-hidden="true"
style="position: absolute; left: 0px; top: 0px; transform: translate(0px, 0px);"
>
<li>
<a
href="#"
>
click me
</a>
</li>
<li
class="divider"
role="separator"
/>
<li>
<a
href="#"
>
click me
</a>
</li>
</ul>
</DocumentFragment>
`;
exports[`MenuItem 1`] = `
<DocumentFragment>
<li>
<a
href="#"
>
click me
</a>
</li>
</DocumentFragment>
`;
exports[`SubMenu 1`] = `
<DocumentFragment>
<li>
<a>
<i
aria-hidden="true"
class="fa fa-caret-right pull-right"
/>
submenu
</a>
</li>
</DocumentFragment>
`;
exports[`SubMenu 2`] = `
<DocumentFragment>
<li>
<a>
<i
aria-hidden="true"
class="fa fa-caret-right pull-right"
/>
submenu
</a>
<ul
class="dropdown-menu show"
data-popper-escaped="true"
data-popper-placement="right-start"
data-popper-reference-hidden="true"
style="position: absolute; left: 0px; top: 0px; transform: translate(0px, 0px);"
>
<li>
<a
href="#"
>
click me
</a>
</li>
</ul>
</li>
</DocumentFragment>
`;
exports[`SubMenu 3`] = `
<DocumentFragment>
<li>
<a>
<i
aria-hidden="true"
class="fa fa-caret-right pull-right"
/>
submenu
</a>
</li>
</DocumentFragment>
`;

View File

@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileChooser Component should render correctly 1`] = `
<a
className="foo"
href="#"
onClick={[Function]}
title="bar"
>
<i
className="fa fa-fw undefined"
/>
<input
className="hidden"
onChange={[Function]}
type="file"
/>
</a>
`;

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileChooser 1`] = `
<DocumentFragment>
<a
href="#"
>
<i
class="fa fa-fw play"
/>
open
<input
class="hidden"
type="file"
/>
</a>
</DocumentFragment>
`;

View File

@ -1,15 +1,28 @@
/** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */ /** Auto-generated by test_app.py:TestApp._test_generate_tflow_js */
export default function(){ import {HTTPFlow} from '../../flow';
export default function(): HTTPFlow {
return { return {
"client_conn": { "client_conn": {
//@ts-ignore
"address": [ "address": [
"127.0.0.1", "127.0.0.1",
22 22
], ],
"alpn": "http/1.1",
//@ts-ignore
"alpn_proto_negotiated": "http/1.1", "alpn_proto_negotiated": "http/1.1",
"cipher": "cipher",
"cipher_name": "cipher", "cipher_name": "cipher",
"id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939", "id": "4a18d1a0-50a1-48dd-9aa6-d45d74282939",
"peername": [
"127.0.0.1",
22
],
"sni": "address", "sni": "address",
"sockname": [
"",
0
],
"timestamp_end": 946681206, "timestamp_end": 946681206,
"timestamp_start": 946681200, "timestamp_start": 946681200,
"timestamp_tls_setup": 946681201, "timestamp_tls_setup": 946681201,
@ -22,8 +35,8 @@ export default function(){
}, },
"id": "d91165be-ca1f-4612-88a9-c0f8696f3e29", "id": "d91165be-ca1f-4612-88a9-c0f8696f3e29",
"intercepted": false, "intercepted": false,
"is_replay": null, "is_replay": undefined,
"marked": false, "marked": "",
"modified": false, "modified": false,
"request": { "request": {
"contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73", "contentHash": "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
@ -40,6 +53,7 @@ export default function(){
], ],
"host": "address", "host": "address",
"http_version": "HTTP/1.1", "http_version": "HTTP/1.1",
//@ts-ignore
"is_replay": false, "is_replay": false,
"method": "GET", "method": "GET",
"path": "/path", "path": "/path",
@ -47,13 +61,7 @@ export default function(){
"pretty_host": "address", "pretty_host": "address",
"scheme": "http", "scheme": "http",
"timestamp_end": 946681201, "timestamp_end": 946681201,
"timestamp_start": 946681200, "timestamp_start": 946681200
"trailers": [
[
"trailer",
"qvalue"
]
]
}, },
"response": { "response": {
"contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d", "contentHash": "ab530a13e45914982b79f9b7e3fba994cfd1f3fb22f71cea1afbf02b460c6d1d",
@ -69,6 +77,7 @@ export default function(){
] ]
], ],
"http_version": "HTTP/1.1", "http_version": "HTTP/1.1",
//@ts-ignore
"is_replay": false, "is_replay": false,
"reason": "OK", "reason": "OK",
"status_code": 200, "status_code": 200,
@ -82,17 +91,29 @@ export default function(){
] ]
}, },
"server_conn": { "server_conn": {
//@ts-ignore
"address": [ "address": [
"address", "address",
22 22
], ],
"alpn_proto_negotiated": "http/1.1", "alpn": undefined,
//@ts-ignore
"alpn_proto_negotiated": undefined,
"cipher": undefined,
"id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8", "id": "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8",
"ip_address": [ "ip_address": [
"192.168.0.1", "192.168.0.1",
22 22
], ],
"peername": [
"192.168.0.1",
22
],
"sni": "address", "sni": "address",
"sockname": [
"address",
22
],
"source_address": [ "source_address": [
"address", "address",
22 22

View File

@ -6,7 +6,7 @@ describe('connection reducer', () => {
it('should return initial state', () => { it('should return initial state', () => {
expect(reduceConnection(undefined, {})).toEqual({ expect(reduceConnection(undefined, {})).toEqual({
state: ConnectionState.INIT, state: ConnectionState.INIT,
message: null, message: undefined,
}) })
}) })

View File

@ -1,11 +1,10 @@
import reduceState from '../../ducks/index' import {rootReducer} from '../../ducks/index'
describe('reduceState in js/ducks/index.js', () => { describe('reduceState in js/ducks/index.js', () => {
it('should combine flow and header', () => { it('should combine flow and header', () => {
let state = reduceState(undefined, {}) let state = rootReducer(undefined, {})
expect(state.hasOwnProperty('eventLog')).toBeTruthy() expect(state.hasOwnProperty('eventLog')).toBeTruthy()
expect(state.hasOwnProperty('flows')).toBeTruthy() expect(state.hasOwnProperty('flows')).toBeTruthy()
expect(state.hasOwnProperty('settings')).toBeTruthy()
expect(state.hasOwnProperty('connection')).toBeTruthy() expect(state.hasOwnProperty('connection')).toBeTruthy()
expect(state.hasOwnProperty('ui')).toBeTruthy() expect(state.hasOwnProperty('ui')).toBeTruthy()
}) })

View File

@ -1,23 +1,25 @@
import reduceOptions, * as OptionsActions from '../../ducks/options' import reduceOptions, * as OptionsActions from '../../ducks/options'
import configureStore from 'redux-mock-store' import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk' import thunk from 'redux-thunk'
import * as OptionsEditorActions from '../../ducks/ui/optionsEditor' import * as OptionsEditorActions from '../../ducks/ui/optionsEditor'
import {updateError} from "../../ducks/ui/optionsEditor";
const mockStore = configureStore([ thunk ]) const mockStore = configureStore([ thunk ])
describe('option reducer', () => { describe('option reducer', () => {
it('should return initial state', () => { it('should return initial state', () => {
expect(reduceOptions(undefined, {})).toEqual({}) expect(reduceOptions(undefined, {})).toEqual(OptionsActions.defaultState)
}) })
it('should handle receive action', () => { it('should handle receive action', () => {
let action = { type: OptionsActions.RECEIVE, data: 'foo' } let action = { type: OptionsActions.RECEIVE, data: {id: 'foo'} }
expect(reduceOptions(undefined, action)).toEqual('foo') expect(reduceOptions(undefined, action)).toEqual({id: 'foo'})
}) })
it('should handle update action', () => { it('should handle update action', () => {
let action = {type: OptionsActions.UPDATE, data: {id: 1} } let action = {type: OptionsActions.UPDATE, data: {id: 1} }
expect(reduceOptions(undefined, action)).toEqual({id: 1}) expect(reduceOptions(undefined, action)).toEqual({...OptionsActions.defaultState, id: 1})
}) })
}) })
@ -39,13 +41,11 @@ describe('option actions', () => {
describe('sendUpdate', () => { describe('sendUpdate', () => {
it('should handle error', () => { it('should handle error', async () => {
let mockResponse = { status: 400, text: p => Promise.resolve('error') }, global.fetch = () => Promise.reject("fooerror");
promise = Promise.resolve(mockResponse) await store.dispatch(OptionsActions.pureSendUpdate("bar", "error"))
global.fetch = r => { return promise }
OptionsActions.pureSendUpdate('bar', 'error')
expect(store.getActions()).toEqual([ expect(store.getActions()).toEqual([
{ type: OptionsEditorActions.OPTION_UPDATE_SUCCESS, option: 'foo'} OptionsEditorActions.updateError("bar", "fooerror")
]) ])
}) })
}) })

View File

@ -1,25 +0,0 @@
jest.mock('../../utils')
import reduceSettings, * as SettingsActions from '../../ducks/settings'
describe('setting reducer', () => {
it('should return initial state', () => {
expect(reduceSettings(undefined, {})).toEqual({})
})
it('should handle receive action', () => {
let action = { type: SettingsActions.RECEIVE, data: 'foo' }
expect(reduceSettings(undefined, action)).toEqual('foo')
})
it('should handle update action', () => {
let action = {type: SettingsActions.UPDATE, data: {id: 1} }
expect(reduceSettings(undefined, action)).toEqual({id: 1})
})
})
describe('setting actions', () => {
it('should be possible to update setting', () => {
expect(reduceSettings(undefined, SettingsActions.update())).toEqual({})
})
})

View File

@ -1,111 +0,0 @@
import React from 'react'
import { combineReducers, applyMiddleware, createStore as createReduxStore } from 'redux'
import thunk from 'redux-thunk'
import configureStore from 'redux-mock-store'
import { ConnectionState } from '../../ducks/connection'
import TFlow from './_tflow'
const mockStore = configureStore([thunk])
export function createStore(parts) {
return createReduxStore(
combineReducers(parts),
applyMiddleware(...[thunk])
)
}
export { TFlow }
export function TStore(){
let tflow = new TFlow()
return mockStore({
ui: {
flow: {
contentView: 'Auto',
displayLarge: false,
showFullContent: true,
maxContentLines: 10,
content: ['foo', 'bar'],
viewDescription: 'foo',
modifiedFlow: true,
tab: 'request'
},
header: {
tab: 'Start'
},
modal: {
activeModal: undefined
},
optionsEditor: {
booleanOption: { isUpdating: true, error: false },
strOption: { error: true },
intOption: {},
choiceOption: {},
}
},
settings: {
contentViews: ['Auto', 'Raw', 'Text'],
anticache: true,
anticomp: false
},
options: {
booleanOption: {
choices: null,
default: false,
help: "foo",
type: "bool",
value: false
},
strOption: {
choices: null,
default: null,
help: "foo",
type: "str",
value: "str content"
},
intOption: {
choices: null,
default: 0,
help: "foo",
type: "int",
value: 1
},
choiceOption: {
choices: ['a', 'b', 'c'],
default: 'a',
help: "foo",
type: "str",
value: "b"
},
},
flows: {
selected: ["d91165be-ca1f-4612-88a9-c0f8696f3e29"],
byId: {"d91165be-ca1f-4612-88a9-c0f8696f3e29": tflow},
filter: '~u foo',
highlight: '~a bar',
sort: {
desc: true,
column: 'PathColumn'
},
view: [ tflow ]
},
connection: {
state: ConnectionState.ESTABLISHED
},
eventLog: {
visible: true,
filters: {
debug: true,
info: true,
web: false,
warn: true,
error: true
},
view: [
{ id: 1, level: 'info', message: 'foo' },
{ id: 2, level: 'error', message: 'bar' }
]
}
})
}

View File

@ -0,0 +1,120 @@
import {applyMiddleware, combineReducers, createStore as createReduxStore} from 'redux'
import thunk from 'redux-thunk'
import configureStore, {MockStoreCreator, MockStoreEnhanced} from 'redux-mock-store'
import {ConnectionState} from '../../ducks/connection'
import TFlow from './_tflow'
import {RootState} from "../../ducks";
import {HTTPFlow} from "../../flow";
import {defaultState as defaultConf} from "../../ducks/conf"
import {defaultState as defaultOptions} from "../../ducks/options"
const mockStoreCreator: MockStoreCreator<RootState> = configureStore([thunk])
export function createStore(parts) {
return createReduxStore(
combineReducers(parts),
applyMiddleware(...[thunk])
)
}
export {TFlow}
const tflow1: HTTPFlow = TFlow();
const tflow2: HTTPFlow = TFlow();
tflow2.id = "flow2";
tflow2.request.path = "/second";
export const testState: RootState = {
conf: defaultConf,
options_meta: {
anticache: {
"type": "bool",
"default": false,
"value": false,
"help": "Strip out request headers that might cause the server to return 304-not-modified.",
"choices": undefined
},
body_size_limit: {
"type": "optional str",
"default": undefined,
"value": undefined,
"help": "Byte size limit of HTTP request and response bodies. Understands k/m/g suffixes, i.e. 3m for 3 megabytes.",
"choices": undefined,
},
connection_strategy: {
"type": "str",
"default": "eager",
"value": "eager",
"help": "Determine when server connections should be established. When set to lazy, mitmproxy tries to defer establishing an upstream connection as long as possible. This makes it possible to use server replay while being offline. When set to eager, mitmproxy can detect protocols with server-side greetings, as well as accurately mirror TLS ALPN negotiation.",
"choices": [
"eager",
"lazy"
]
},
listen_port: {
"type": "int",
"default": 8080,
"value": 8080,
"help": "Proxy service port.",
"choices": undefined
}
},
ui: {
flow: {
contentView: 'Auto',
displayLarge: false,
showFullContent: true,
maxContentLines: 10,
content: [[['foo', 'bar']]],
viewDescription: 'foo',
modifiedFlow: undefined,
tab: 'request'
},
header: {
tab: 'Start'
},
modal: {
activeModal: undefined
},
optionsEditor: {
booleanOption: {isUpdating: true, error: false},
strOption: {error: true},
intOption: {},
choiceOption: {},
}
},
options: defaultOptions,
flows: {
selected: [tflow1.id],
byId: {[tflow1.id]: tflow1, [tflow2.id]: tflow2},
filter: '~d address',
highlight: '~u /path',
sort: {
desc: true,
column: 'PathColumn'
},
view: [tflow1, tflow2]
},
connection: {
state: ConnectionState.ESTABLISHED
},
eventLog: {
visible: true,
filters: {
debug: true,
info: true,
web: false,
warn: true,
error: true
},
view: [
{id: 1, level: 'info', message: 'foo'},
{id: 2, level: 'error', message: 'bar'}
]
}
}
export function TStore(): MockStoreEnhanced<RootState> {
return mockStoreCreator(testState)
}

View File

@ -19,7 +19,7 @@ describe('flow reducer', () => {
displayLarge: false, displayLarge: false,
viewDescription: '', viewDescription: '',
showFullContent: false, showFullContent: false,
modifiedFlow: false, modifiedFlow: undefined,
contentView: 'Auto', contentView: 'Auto',
tab: 'request', tab: 'request',
content: [], content: [],

View File

@ -0,0 +1,24 @@
import React from 'react'
import {render as rtlRender} from '@testing-library/react'
import {Provider} from 'react-redux'
// Import your own reducer
import {createAppStore} from '../ducks'
import {testState} from "./ducks/tutils";
// re-export everything
export * from '@testing-library/react'
export function render(
ui,
{
store = createAppStore(testState),
...renderOptions
} = {}
) {
function Wrapper({children}) {
return <Provider store={store}>{children}</Provider>
}
const ret = rtlRender(ui, {wrapper: Wrapper, ...renderOptions})
return {...ret, store}
}

View File

@ -1,32 +1,15 @@
import React from 'react' import React from 'react'
import {render} from 'react-dom' import {render} from 'react-dom'
import {applyMiddleware, compose, createStore} from 'redux'
import {Provider} from 'react-redux' import {Provider} from 'react-redux'
import thunk from 'redux-thunk'
import ProxyApp from './components/ProxyApp' import ProxyApp from './components/ProxyApp'
import rootReducer from './ducks/index'
import {add as addLog} from './ducks/eventLog' import {add as addLog} from './ducks/eventLog'
import useUrlState from './urlState' import useUrlState from './urlState'
import WebSocketBackend from './backends/websocket' import WebSocketBackend from './backends/websocket'
import StaticBackend from './backends/static' import StaticBackend from './backends/static'
import {logger} from 'redux-logger' import {store} from "./ducks";
const middlewares = [thunk];
// logger must be last
if (process.env.NODE_ENV !== 'production') {
middlewares.push(logger);
}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(...middlewares))
)
useUrlState(store) useUrlState(store)
if (window.MITMWEB_STATIC) { if (window.MITMWEB_STATIC) {
window.backend = new StaticBackend(store) window.backend = new StaticBackend(store)

View File

@ -12,7 +12,7 @@ export default class StaticBackend {
onOpen() { onOpen() {
this.fetchData("flows") this.fetchData("flows")
this.fetchData("settings") this.fetchData("options")
// this.fetchData("events") # TODO: Add events log to static viewer. // this.fetchData("events") # TODO: Add events log to static viewer.
} }

View File

@ -24,7 +24,6 @@ export default class WebsocketBackend {
} }
onOpen() { onOpen() {
this.fetchData("settings")
this.fetchData("flows") this.fetchData("flows")
this.fetchData("events") this.fetchData("events")
this.fetchData("options") this.fetchData("options")

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Codemirror from 'react-codemirror'; import CodeMirror from "../../contrib/CodeMirror"
CodeEditor.propTypes = { CodeEditor.propTypes = {
@ -15,7 +15,7 @@ export default function CodeEditor ( { content, onChange} ){
}; };
return ( return (
<div className="codeeditor" onKeyDown={e => e.stopPropagation()}> <div className="codeeditor" onKeyDown={e => e.stopPropagation()}>
<Codemirror value={content} onChange={onChange} options={options}/> <CodeMirror value={content} onChange={onChange} options={options}/>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { MessageUtils } from '../../flow/utils.js' import { MessageUtils } from '../../flow/utils'
export default function withContentLoader(View) { export default function withContentLoader(View) {
@ -23,11 +23,11 @@ export default function withContentLoader(View) {
} }
} }
componentWillMount() { componentDidMount() {
this.updateContent(this.props) this.updateContent(this.props)
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
if ( if (
nextProps.message.content !== this.props.message.content || nextProps.message.content !== this.props.message.content ||
nextProps.message.contentHash !== this.props.message.contentHash || nextProps.message.contentHash !== this.props.message.contentHash ||
@ -51,7 +51,7 @@ export default function withContentLoader(View) {
if (props.message.content !== undefined) { if (props.message.content !== undefined) {
return this.setState({request: undefined, content: props.message.content}) return this.setState({request: undefined, content: props.message.content})
} }
if (props.message.contentLength === 0 || props.message.contentLength === null) { if (props.message.contentLength === 0) {
return this.setState({request: undefined, content: ""}) return this.setState({request: undefined, content: ""})
} }

View File

@ -38,12 +38,12 @@ export class PureViewServer extends Component {
setContent: PropTypes.func.isRequired setContent: PropTypes.func.isRequired
} }
componentWillMount(){ UNSAFE_componentWillMount(){
this.setContentView(this.props) this.setContentView(this.props)
} }
componentWillReceiveProps(nextProps){ UNSAFE_componentWillReceiveProps(nextProps){
if (nextProps.content != this.props.content) { if (nextProps.content !== this.props.content) {
this.setContentView(nextProps) this.setContentView(nextProps)
} }
} }
@ -55,7 +55,7 @@ export class PureViewServer extends Component {
this.data = {lines: [], description: err.message} this.data = {lines: [], description: err.message}
} }
props.setContentViewDescription(props.contentView != this.data.description ? this.data.description : '') props.setContentViewDescription(props.contentView !== this.data.description ? this.data.description : '')
props.setContent(this.data.lines) props.setContent(this.data.lines)
} }

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { formatSize } from '../../utils.js' import { formatSize } from '../../utils'
import UploadContentButton from './UploadContentButton' import UploadContentButton from './UploadContentButton'
import DownloadContentButton from './DownloadContentButton' import DownloadContentButton from './DownloadContentButton'

View File

@ -1,38 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {setContentView} from '../../ducks/ui/flow';
import Dropdown, {MenuItem} from '../common/Dropdown'
ViewSelector.propTypes = {
contentViews: PropTypes.array.isRequired,
activeView: PropTypes.string.isRequired,
setContentView: PropTypes.func.isRequired
}
export function ViewSelector({contentViews, activeView, setContentView}) {
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={() => setContentView(name)}>
{name.toLowerCase().replace('_', ' ')}
</MenuItem>
)}
</Dropdown>
)
}
export default connect(
state => ({
contentViews: state.settings.contentViews || [],
activeView: state.ui.flow.contentView,
}), {
setContentView,
}
)(ViewSelector)

View File

@ -0,0 +1,26 @@
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>
)
});

View File

@ -8,13 +8,11 @@ import { calcVScroll } from './helpers/VirtualScroll'
import FlowTableHead from './FlowTable/FlowTableHead' import FlowTableHead from './FlowTable/FlowTableHead'
import FlowRow from './FlowTable/FlowRow' import FlowRow from './FlowTable/FlowRow'
import Filt from "../filt/filt" import Filt from "../filt/filt"
import * as flowsActions from '../ducks/flows'
class FlowTable extends React.Component { class FlowTable extends React.Component {
static propTypes = { static propTypes = {
selectFlow: PropTypes.func.isRequired,
flows: PropTypes.array.isRequired, flows: PropTypes.array.isRequired,
rowHeight: PropTypes.number, rowHeight: PropTypes.number,
highlight: PropTypes.string, highlight: PropTypes.string,
@ -110,7 +108,6 @@ class FlowTable extends React.Component {
flow={flow} flow={flow}
selected={flow === selected} selected={flow === selected}
highlighted={isHighlighted(flow)} highlighted={isHighlighted(flow)}
onSelect={this.props.selectFlow}
/> />
))} ))}
<tr style={{ height: vScroll.paddingBottom }}/> <tr style={{ height: vScroll.paddingBottom }}/>
@ -128,8 +125,5 @@ export default connect(
flows: state.flows.view, flows: state.flows.view,
highlight: state.flows.highlight, highlight: state.flows.highlight,
selected: state.flows.byId[state.flows.selected[0]], selected: state.flows.byId[state.flows.selected[0]],
}), })
{
selectFlow: flowsActions.select,
}
)(PureFlowTable) )(PureFlowTable)

View File

@ -1,236 +0,0 @@
import React, {useCallback, useState} from 'react'
import {useDispatch} from 'react-redux'
import classnames from 'classnames'
import {RequestUtils, ResponseUtils} from '../../flow/utils.js'
import {formatSize, formatTimeDelta, formatTimeStamp} from '../../utils.js'
import * as flowActions from "../../ducks/flows";
import { addInterceptFilter } from "../../ducks/settings"
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
import { fetchApi } from "../../utils"
export const defaultColumnNames = ["tls", "icon", "path", "method", "status", "size", "time"]
export function TLSColumn({flow}) {
return (
<td className={classnames('col-tls', flow.request.scheme === 'https' ? 'col-tls-https' : 'col-tls-http')}/>
)
}
TLSColumn.headerClass = 'col-tls'
TLSColumn.headerName = ''
export function IconColumn({flow}) {
return (
<td className="col-icon">
<div className={classnames('resource-icon', IconColumn.getIcon(flow))}/>
</td>
)
}
IconColumn.headerClass = 'col-icon'
IconColumn.headerName = ''
IconColumn.getIcon = flow => {
if (!flow.response) {
return 'resource-icon-plain'
}
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'
}
if (300 <= flow.response.status_code && flow.response.status_code < 400) {
return 'resource-icon-redirect'
}
if (contentType.indexOf('image') >= 0) {
return 'resource-icon-image'
}
if (contentType.indexOf('javascript') >= 0) {
return 'resource-icon-js'
}
if (contentType.indexOf('css') >= 0) {
return 'resource-icon-css'
}
if (contentType.indexOf('html') >= 0) {
return 'resource-icon-document'
}
return 'resource-icon-plain'
}
export function PathColumn({flow}) {
let err;
if (flow.error) {
if (flow.error.msg === "Connection killed.") {
err = <i className="fa fa-fw fa-times pull-right"/>
} else {
err = <i className="fa fa-fw fa-exclamation pull-right"/>
}
}
return (
<td className="col-path">
{flow.request.is_replay && (
<i className="fa fa-fw fa-repeat pull-right"/>
)}
{flow.intercepted && (
<i className="fa fa-fw fa-pause pull-right"/>
)}
{err}
{RequestUtils.pretty_url(flow.request)}
</td>
)
}
PathColumn.headerClass = 'col-path'
PathColumn.headerName = 'Path'
export function MethodColumn({flow}) {
return (
<td className="col-method">{flow.request.method}</td>
)
}
MethodColumn.headerClass = 'col-method'
MethodColumn.headerName = 'Method'
export function StatusColumn({flow}) {
let color = 'darkred';
if (flow.response && 100 <= flow.response.status_code && flow.response.status_code < 200) {
color = 'green'
} else if (flow.response && 200 <= flow.response.status_code && flow.response.status_code < 300) {
color = 'darkgreen'
} else if (flow.response && 300 <= flow.response.status_code && flow.response.status_code < 400) {
color = 'lightblue'
} else if (flow.response && 400 <= flow.response.status_code && flow.response.status_code < 500) {
color = 'lightred'
} else if (flow.response && 500 <= flow.response.status_code && flow.response.status_code < 600) {
color = 'lightred'
}
return (
<td className="col-status" style={{color: color}}>{flow.response && flow.response.status_code}</td>
)
}
StatusColumn.headerClass = 'col-status'
StatusColumn.headerName = 'Status'
export function SizeColumn({flow}) {
return (
<td className="col-size">{formatSize(SizeColumn.getTotalSize(flow))}</td>
)
}
SizeColumn.getTotalSize = flow => {
let total = flow.request.contentLength
if (flow.response) {
total += flow.response.contentLength || 0
}
return total
}
SizeColumn.headerClass = 'col-size'
SizeColumn.headerName = 'Size'
export function TimeColumn({flow}) {
return (
<td className="col-time">
{flow.response ? (
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
) : (
'...'
)}
</td>
)
}
TimeColumn.headerClass = 'col-time'
TimeColumn.headerName = 'Time'
export function TimeStampColumn({flow}) {
return (
<td className="col-start">
{flow.request.timestamp_start ? (
formatTimeStamp(flow.request.timestamp_start)
) : (
'...'
)}
</td>
)
}
TimeStampColumn.headerClass = 'col-timestamp'
TimeStampColumn.headerName = 'TimeStamp'
export function QuickActionsColumn({flow, intercept}) {
const dispatch = useDispatch()
let [open, setOpen] = useState(false)
const exportAsCURL = useCallback(() => {
if (!flow) {
return
}
fetchApi(`/flows/${flow.id}/export`, { method: 'POST' })
.then(response => response.json())
.then(data => {
navigator.clipboard.writeText(data.export)
})
}, [flow])
let forwardIntercept = null;
if (flow.intercepted) {
forwardIntercept = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.resume(flow))}>
<i className="fa fa-fw fa-play text-success"/>
</a>;
}
return (
<td className={classnames("col-quickactions", {hover: open})} onClick={(e) => e.stopPropagation()}>
<div>
{forwardIntercept}
<Dropdown text={<i className="fa fa-fw fa-ellipsis-h"/>} className="quickaction" onOpen={setOpen} options={{placement: "bottom-end"}}>
<MenuItem onClick={() => exportAsCURL()}>Copy as cURL</MenuItem>
<SubMenu title="Intercept requests like this">
<MenuItem onClick={() =>{dispatch(addInterceptFilter(flow.request.host))}}>
Intercept {flow.request.host}
</MenuItem>
<MenuItem onClick={() =>{dispatch(addInterceptFilter(flow.request.host + flow.request.path))}}>
Intercept {flow.request.host + flow.request.path}
</MenuItem>
<MenuItem onClick={() =>{dispatch(addInterceptFilter(`~m POST & ${flow.request.host}`))}}>
Intercept all POST requests from this host
</MenuItem>
</SubMenu>
<MenuItem onClick={() =>{dispatch(flowActions.remove(flow))}}>
Delete
</MenuItem>
</Dropdown>
</div>
</td>
)
}
QuickActionsColumn.headerClass = 'col-quickactions'
QuickActionsColumn.headerName = ''
export const columns = {};
for (let col of [
TLSColumn,
IconColumn,
PathColumn,
MethodColumn,
StatusColumn,
TimeStampColumn,
SizeColumn,
TimeColumn,
QuickActionsColumn,
]) {
columns[col.name.replace(/Column$/, "").toLowerCase()] = col;
}

View File

@ -0,0 +1,280 @@
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 * as flowActions from "../../ducks/flows";
import {addInterceptFilter} from "../../ducks/options"
import Dropdown, {MenuItem, SubMenu} from "../common/Dropdown";
import {Flow} from "../../flow";
type FlowColumnProps = {
flow: Flow
}
interface FlowColumn {
(props: FlowColumnProps): JSX.Element;
headerClass: string;
headerName: string;
}
export const TLSColumn: FlowColumn = ({flow}) => {
return (
<td className={classnames('col-tls', flow.client_conn.tls_established ? 'col-tls-https' : 'col-tls-http')}/>
)
}
TLSColumn.headerClass = 'col-tls'
TLSColumn.headerName = ''
export const IconColumn: FlowColumn = ({flow}) => {
return (
<td className="col-icon">
<div className={classnames('resource-icon', getIcon(flow))}/>
</td>
)
}
IconColumn.headerClass = 'col-icon'
IconColumn.headerName = ''
const getIcon = (flow: Flow): string => {
if (flow.type !== "http" || !flow.response) {
return 'resource-icon-plain'
}
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'
}
if (300 <= flow.response.status_code && flow.response.status_code < 400) {
return 'resource-icon-redirect'
}
if (contentType.indexOf('image') >= 0) {
return 'resource-icon-image'
}
if (contentType.indexOf('javascript') >= 0) {
return 'resource-icon-js'
}
if (contentType.indexOf('css') >= 0) {
return 'resource-icon-css'
}
if (contentType.indexOf('html') >= 0) {
return 'resource-icon-document'
}
return 'resource-icon-plain'
}
export const PathColumn: FlowColumn = ({flow}) => {
let err;
if (flow.error) {
if (flow.error.msg === "Connection killed.") {
err = <i className="fa fa-fw fa-times pull-right"/>
} else {
err = <i className="fa fa-fw fa-exclamation pull-right"/>
}
}
return (
<td className="col-path">
{flow.is_replay === "request" && (
<i className="fa fa-fw fa-repeat pull-right"/>
)}
{flow.intercepted && (
<i className="fa fa-fw fa-pause pull-right"/>
)}
{err}
{flow.type === "http" ? RequestUtils.pretty_url(flow.request) : null}
</td>
)
};
PathColumn.headerClass = 'col-path'
PathColumn.headerName = 'Path'
export const MethodColumn: FlowColumn = ({flow}) => {
return (
<td className="col-method">{flow.type === "http" ? flow.request.method : flow.type.toLowerCase()}</td>
)
};
MethodColumn.headerClass = 'col-method'
MethodColumn.headerName = 'Method'
export const StatusColumn: FlowColumn = ({flow}) => {
let color = 'darkred';
if (flow.type !== "http" || !flow.response)
return <td className="col-status"/>
if (100 <= flow.response.status_code && flow.response.status_code < 200) {
color = 'green'
} else if (200 <= flow.response.status_code && flow.response.status_code < 300) {
color = 'darkgreen'
} else if (300 <= flow.response.status_code && flow.response.status_code < 400) {
color = 'lightblue'
} else if (400 <= flow.response.status_code && flow.response.status_code < 500) {
color = 'lightred'
} else if (500 <= flow.response.status_code && flow.response.status_code < 600) {
color = 'lightred'
}
return (
<td className="col-status" style={{color: color}}>{flow.response.status_code}</td>
)
}
StatusColumn.headerClass = 'col-status'
StatusColumn.headerName = 'Status'
export const SizeColumn: FlowColumn = ({flow}) => {
return (
<td className="col-size">{formatSize(getTotalSize(flow))}</td>
)
};
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}) => {
return (
<td className="col-time">
{flow.type === "http" && flow.response?.timestamp_end ? (
formatTimeDelta(1000 * (flow.response.timestamp_end - flow.request.timestamp_start))
) : (
'...'
)}
</td>
)
}
TimeColumn.headerClass = 'col-time'
TimeColumn.headerName = 'Time'
export const TimeStampColumn: FlowColumn = ({flow}) => {
return (
<td className="col-start">
{flow.type === "http" && flow.request.timestamp_start ? (
formatTimeStamp(flow.request.timestamp_start)
) : (
'...'
)}
</td>
)
}
TimeStampColumn.headerClass = 'col-timestamp'
TimeStampColumn.headerName = 'TimeStamp'
export const QuickActionsColumn: 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))}>
<i className="fa fa-fw fa-play text-success"/>
</a>;
} else {
resume_or_replay = <a href="#" className="quickaction" onClick={() => dispatch(flowActions.replay(flow))}>
<i className="fa fa-fw fa-repeat text-primary"/>
</a>;
}
if (flow.type !== "http")
return <td className="col-quickactions"/>
const filt = (x) => dispatch(addInterceptFilter(x));
const ct = flow.response && ResponseUtils.getContentType(flow.response);
return (
<td className={classnames("col-quickactions", {hover: open})} onClick={(e) => 0/*e.stopPropagation()*/}>
<div>
{resume_or_replay}
<Dropdown text={<i className="fa fa-fw fa-ellipsis-h text-muted"/>} className="quickaction"
onOpen={setOpen}
options={{placement: "bottom-end"}}>
<SubMenu title="Copy...">
<MenuItem onClick={() => copy("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>
</SubMenu>
<SubMenu title="Intercept requests like this">
<MenuItem onClick={() => filt(`~q ${flow.request.host}`)}>
Requests to {flow.request.host}
</MenuItem>
{flow.request.path !== "/" &&
<MenuItem onClick={() => filt(`~q ${flow.request.host}${flow.request.path}`)}>
Requests to {flow.request.host + flow.request.path}
</MenuItem>}
{flow.request.method !== "GET" &&
<MenuItem onClick={() => filt(`~q ~m ${flow.request.method} ${flow.request.host}`)}>
{flow.request.method} requests to {flow.request.host}
</MenuItem>}
</SubMenu>
<SubMenu title="Intercept responses like this">
<MenuItem onClick={() => filt(`~s ${flow.request.host}`)}>
Responses from {flow.request.host}
</MenuItem>
{flow.request.path !== "/" &&
<MenuItem onClick={() => filt(`~s ${flow.request.host}${flow.request.path}`)}>
Responses from {flow.request.host + flow.request.path}
</MenuItem>}
{!!ct &&
<MenuItem onClick={() => filt(`~ts ${ct}`)}>
Responses with a {ct} content type.
</MenuItem>}
</SubMenu>
</Dropdown>
</div>
</td>
)
}
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;
}

View File

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

View File

@ -0,0 +1,45 @@
import React, {useCallback} from 'react'
import classnames from 'classnames'
import {Flow} from "../../flow";
import {useAppDispatch, useAppSelector} from "../../ducks";
import {select} from '../../ducks/flows'
import {columns, QuickActionsColumn} from "./FlowColumns";
type FlowRowProps = {
flow: Flow
selected: boolean
highlighted: boolean
}
export default React.memo(function FlowRow({flow, selected, highlighted}: FlowRowProps) {
const dispatch = useAppDispatch(),
displayColumnNames = useAppSelector(state => state.options.web_columns),
className = classnames({
'selected': selected,
'highlighted': highlighted,
'intercepted': flow.intercepted,
'has-request': flow.type === "http" && flow.request,
'has-response': flow.type === "http" && flow.response,
})
const onClick = useCallback(e => {
// a bit of a hack to disable row selection for quickactions.
let node = e.target;
while (node.parentNode) {
if (node.classList.contains("col-quickactions"))
return
node = node.parentNode;
}
dispatch(select(flow.id));
}, [flow]);
const displayColumns = displayColumnNames.map(x => columns[x]).concat(QuickActionsColumn);
return (
<tr className={className} onClick={onClick}>
{displayColumns.map(Column => (
<Column key={Column.name} flow={flow}/>
))}
</tr>
)
})

View File

@ -1,50 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import classnames from 'classnames'
import {columns, defaultColumnNames} from './FlowColumns'
import {setSort} from '../../ducks/flows'
FlowTableHead.propTypes = {
setSort: PropTypes.func.isRequired,
sortDesc: PropTypes.bool.isRequired,
sortColumn: PropTypes.string,
displayColumnNames: PropTypes.array,
}
export function getDisplayColumns(displayColumnNames) {
if (typeof displayColumnNames == "undefined") {
return Object.values(columns)
}
return displayColumnNames.map(x => columns[x]).concat([columns.quickactions]);
}
export function FlowTableHead({sortColumn, sortDesc, setSort, displayColumnNames}) {
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
const displayColumns = getDisplayColumns(displayColumnNames)
return (
<tr>
{displayColumns.map(Column => (
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
key={Column.name}
onClick={() => setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc)}>
{Column.headerName}
</th>
))}
</tr>
)
}
export default connect(
state => ({
sortDesc: state.flows.sort.desc,
sortColumn: state.flows.sort.column,
displayColumnNames: state.options["web_columns"] ? state.options["web_columns"].value : defaultColumnNames,
}),
{
setSort
}
)(FlowTableHead)

View File

@ -0,0 +1,28 @@
import React from 'react'
import classnames from 'classnames'
import {columns, QuickActionsColumn} from './FlowColumns'
import {setSort} from '../../ducks/flows'
import {useAppDispatch, useAppSelector} from "../../ducks";
export default React.memo(function FlowTableHead() {
const dispatch = useAppDispatch(),
sortDesc = useAppSelector(state => state.flows.sort.desc),
sortColumn = useAppSelector(state => state.flows.sort.column),
displayColumnNames = useAppSelector(state => state.options.web_columns);
const sortType = sortDesc ? 'sort-desc' : 'sort-asc'
const displayColumns = displayColumnNames.map(x => columns[x]).concat(QuickActionsColumn);
return (
<tr>
{displayColumns.map(Column => (
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
key={Column.name}
onClick={() => dispatch(setSort(Column.name, Column.name !== sortColumn ? false : !sortDesc))}>
{Column.headerName}
</th>
))}
</tr>
)
})

View File

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import _ from 'lodash' import {formatTimeDelta, formatTimeStamp} from '../../utils'
import { formatTimeStamp, formatTimeDelta } from '../../utils.js'
export function TimeStamp({t, deltaTo, title}) { export function TimeStamp({t, deltaTo, title}) {
return t ? ( return t ? (

View File

@ -3,8 +3,8 @@ import PropTypes from 'prop-types'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils.js' import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils'
import { formatTimeStamp } from '../../utils.js' import { formatTimeStamp } from '../../utils'
import ContentView from '../ContentView' import ContentView from '../ContentView'
import ContentViewOptions from '../ContentView/ContentViewOptions' import ContentViewOptions from '../ContentView/ContentViewOptions'
import ValidateEditor from '../ValueEditor/ValidateEditor' import ValidateEditor from '../ValueEditor/ValidateEditor'

View File

@ -1,16 +1,15 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import {formatSize} from '../utils'
import { connect } from 'react-redux'
import { formatSize } from '../utils.js'
import HideInStatic from '../components/common/HideInStatic' import HideInStatic from '../components/common/HideInStatic'
import {useAppSelector} from "../ducks";
Footer.propTypes = { export default function Footer() {
settings: PropTypes.object.isRequired, 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
} = useAppSelector(state => state.options);
function Footer({ settings }) {
let {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, websocket, anticache, anticomp,
stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, version, server} = settings;
return ( return (
<footer> <footer>
{mode && mode !== "regular" && ( {mode && mode !== "regular" && (
@ -22,7 +21,7 @@ function Footer({ settings }) {
{showhost && ( {showhost && (
<span className="label label-success">showhost</span> <span className="label label-success">showhost</span>
)} )}
{no_upstream_cert && ( {!upstream_cert && (
<span className="label label-success">no-upstream-cert</span> <span className="label label-success">no-upstream-cert</span>
)} )}
{!rawtcp && ( {!rawtcp && (
@ -59,15 +58,9 @@ function Footer({ settings }) {
} }
</HideInStatic> </HideInStatic>
<span className="label label-info" title="Mitmproxy Version"> <span className="label label-info" title="Mitmproxy Version">
v{version} {version}
</span> </span>
</div> </div>
</footer> </footer>
) )
} }
export default connect(
state => ({
settings: state.settings,
})
)(Footer)

View File

@ -1,16 +1,14 @@
import React from "react" import React from "react"
import PropTypes from "prop-types"
import { connect } from "react-redux"
import {ConnectionState} from "../../ducks/connection" import {ConnectionState} from "../../ducks/connection"
import {useAppSelector} from "../../ducks";
ConnectionIndicator.propTypes = { export default React.memo(function ConnectionIndicator() {
state: PropTypes.symbol.isRequired,
message: PropTypes.string,
} const connState = useAppSelector(state => state.connection.state),
export function ConnectionIndicator({ state, message }) { message = useAppSelector(state => state.connection.message)
switch (state) {
switch (connState) {
case ConnectionState.INIT: case ConnectionState.INIT:
return <span className="connection-indicator init">connecting</span>; return <span className="connection-indicator init">connecting</span>;
case ConnectionState.FETCHING: case ConnectionState.FETCHING:
@ -22,9 +20,8 @@ export function ConnectionIndicator({ state, message }) {
title={message}>connection lost</span>; title={message}>connection lost</span>;
case ConnectionState.OFFLINE: case ConnectionState.OFFLINE:
return <span className="connection-indicator offline">offline</span>; return <span className="connection-indicator offline">offline</span>;
default:
const exhaustiveCheck: never = connState;
throw "unknown connection state";
} }
} })
export default connect(
state => state.connection,
)(ConnectionIndicator)

View File

@ -1,63 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import FileChooser from '../common/FileChooser'
import Dropdown, {Divider, MenuItem} from '../common/Dropdown'
import * as flowsActions from '../../ducks/flows'
import HideInStatic from "../common/HideInStatic";
FileMenu.propTypes = {
clearFlows: PropTypes.func.isRequired,
loadFlows: PropTypes.func.isRequired,
saveFlows: PropTypes.func.isRequired,
}
FileMenu.onNewClick = (e, clearFlows) => {
e.preventDefault();
if (confirm('Delete all flows?'))
clearFlows()
}
export function FileMenu({clearFlows, loadFlows, saveFlows}) {
return (
<Dropdown className="pull-left special" text="mitmproxy" options={{"placement": "bottom-start"}}>
<MenuItem onClick={e => FileMenu.onNewClick(e, clearFlows)}>
<i className="fa fa-fw fa-trash"/>
&nbsp;Clear All
</MenuItem>
<li>
<FileChooser
icon="fa-folder-open"
text="&nbsp;Open..."
onOpenFile={file => loadFlows(file)}
/>
</li>
<MenuItem onClick={e => {
e.preventDefault();
saveFlows();
}}>
<i className="fa fa-fw fa-floppy-o"/>
&nbsp;Save...
</MenuItem>
<HideInStatic>
<Divider/>
<li>
<a href="http://mitm.it/" target="_blank">
<i className="fa fa-fw fa-external-link"/>
&nbsp;Install Certificates...
</a>
</li>
</HideInStatic>
</Dropdown>
)
}
export default connect(
null,
{
clearFlows: flowsActions.clear,
loadFlows: flowsActions.upload,
saveFlows: flowsActions.download,
}
)(FileMenu)

View File

@ -0,0 +1,43 @@
import React from 'react'
import {useDispatch} from 'react-redux'
import FileChooser from '../common/FileChooser'
import Dropdown, {Divider, MenuItem} from '../common/Dropdown'
import * as flowsActions from '../../ducks/flows'
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"/>&nbsp;Clear All
</MenuItem>
<li>
<FileChooser
icon="fa-folder-open"
text="&nbsp;Open..."
onClick={
// stop event propagation: we must keep the input in DOM for upload to work.
e => e.stopPropagation()
}
onOpenFile={file => {
dispatch(flowsActions.upload(file));
document.body.click(); // "restart" event propagation
}}
/>
</li>
<MenuItem onClick={() => dispatch(flowsActions.download())}>
<i className="fa fa-fw fa-floppy-o"/>&nbsp;Save...
</MenuItem>
<HideInStatic>
<Divider/>
<li>
<a href="http://mitm.it/" target="_blank">
<i className="fa fa-fw fa-external-link"/>&nbsp;Install Certificates...
</a>
</li>
</HideInStatic>
</Dropdown>
)
});

View File

@ -32,7 +32,7 @@ export default class FilterDocs extends Component {
render() { render() {
const { doc } = this.state const { doc } = this.state
return !doc ? ( return !doc ? (
<i className="fa fa-spinner fa-spin"></i> <i className="fa fa-spinner fa-spin"/>
) : ( ) : (
<table className="table table-condensed"> <table className="table table-condensed">
<tbody> <tbody>
@ -46,7 +46,7 @@ export default class FilterDocs extends Component {
<td colSpan="2"> <td colSpan="2">
<a href="https://mitmproxy.org/docs/latest/concepts-filters/" <a href="https://mitmproxy.org/docs/latest/concepts-filters/"
target="_blank"> target="_blank">
<i className="fa fa-external-link"></i> <i className="fa fa-external-link"/>
&nbsp; mitmproxy docs</a> &nbsp; mitmproxy docs</a>
</td> </td>
</tr> </tr>

View File

@ -1,12 +1,25 @@
import React, {Component} from 'react' import React, {Component} from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import classnames from 'classnames' import classnames from 'classnames'
import { Key } from '../../utils.js' import {Key} from '../../utils'
import Filt from '../../filt/filt' import Filt from '../../filt/filt'
import FilterDocs from './FilterDocs' import FilterDocs from './FilterDocs'
export default class FilterInput extends Component { type FilterInputProps = {
type: string
color: any
placeholder: string
value: string
onChange: (value) => void
}
type FilterInputState = {
value: string
focus: boolean
mousefocus: boolean
}
export default class FilterInput extends Component<FilterInputProps, FilterInputState> {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
@ -31,9 +44,8 @@ export default class FilterInput extends Component {
isValid(filt) { isValid(filt) {
try { try {
const str = filt == null ? this.state.value : filt if (filt) {
if (str) { Filt.parse(filt)
Filt.parse(str)
} }
return true return true
} catch (e) { } catch (e) {
@ -104,7 +116,7 @@ export default class FilterInput extends Component {
const {type, color, placeholder} = this.props const {type, color, placeholder} = this.props
const {value, focus, mousefocus} = this.state const {value, focus, mousefocus} = this.state
return ( return (
<div className={classnames('filter-input input-group', { 'has-error': !this.isValid() })}> <div className={classnames('filter-input input-group', {'has-error': !this.isValid(value)})}>
<span className="input-group-addon"> <span className="input-group-addon">
<i className={'fa fa-fw fa-' + type} style={{color}}/> <i className={'fa fa-fw fa-' + type} style={{color}}/>
</span> </span>

View File

@ -1,79 +0,0 @@
import React from "react"
import {connect} from "react-redux"
import FilterInput from "./FilterInput"
import {update as updateSettings} from "../../ducks/settings"
import * as flowsActions from "../../ducks/flows"
import {setFilter, setHighlight} from "../../ducks/flows"
import Button from "../common/Button"
MainMenu.title = "Start"
export default function MainMenu() {
return (
<div className="main-menu">
<div className="menu-group">
<div className="menu-content">
<FlowFilterInput/>
<HighlightInput/>
</div>
<div className="menu-legend">Find</div>
</div>
<div className="menu-group">
<div className="menu-content">
<InterceptInput/>
<ResumeAll/>
</div>
<div className="menu-legend">Intercept</div>
</div>
</div>
)
}
export function setIntercept(intercept) {
return updateSettings({intercept})
}
const InterceptInput = connect(
state => ({
value: state.settings.intercept || '',
placeholder: 'Intercept',
type: 'pause',
color: 'hsl(208, 56%, 53%)'
}),
{onChange: setIntercept}
)(FilterInput);
const FlowFilterInput = connect(
state => ({
value: state.flows.filter || '',
placeholder: 'Search',
type: 'search',
color: 'black'
}),
{onChange: setFilter}
)(FilterInput);
const HighlightInput = connect(
state => ({
value: state.flows.highlight || '',
placeholder: 'Highlight',
type: 'tag',
color: 'hsl(48, 100%, 50%)'
}),
{onChange: setHighlight}
)(FilterInput);
export function ResumeAll({resumeAll}) {
return (
<Button className="btn-sm" title="[a]ccept all"
icon="fa-forward text-success" onClick={() => resumeAll()}>
Resume All
</Button>
)
}
ResumeAll = connect(
null,
{resumeAll: flowsActions.resumeAll}
)(ResumeAll)

Some files were not shown because too many files have changed in this diff Show More