From 5b5c77ed24760efb215e6a5a503e2f64a2c16dd8 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Sun, 20 Dec 2020 14:58:25 +0530 Subject: [PATCH] v2.3.0 (#470) * Update mypy from 0.780 to 0.781 (#379) * Add FilterByClientIpPlugin example (#381) * Update mypy from 0.781 to 0.782 (#382) * Update twine from 3.1.1 to 3.2.0 (#384) * Update tox from 3.15.2 to 3.16.0 (#385) * Update tox from 3.16.0 to 3.16.1 (#386) * Document FilterByClientIpPlugin & ModifyChunkResponsePlugin (#387) * Refactor plugin base classes for plugin specific flags (#388) * Update to latest code signing recommendations * Move HttpProtocolHandlerPlugin into separate file * Dont add subject attributes if not provided by upstream. Also handle subprocess.TimeoutExpired raised during certificate generation. Instead of retries, we simply close the connection on timeout * Remove plugin specific flag initialization methods for now * Update coverage from 5.1 to 5.2 (#390) * Core acceptor pool doc, cleanup and standalone example (#393) * Better document acceptor module and add a TCP Echo Server example * autopep8 formating * Rename ThreadlessWork --> Work class * Make initialize, is_inactive and shutdown as optional interface methods. Also introduce Readables & Writables custom types. * Move websocket code into its own module * Add websocket client example * Cleanup websocket client * Decouple SSL wrap logic into connection classes (#394) * Move wrap functionality within respective connection classes. Also decouple websocket client handshake method * Add a TCP echo client example that works with TCP echo server example * Add SSL echo server & client example (#395) * Move wrap_socket for SSL server within utils. Also complete proxy.common.pki gen_csr and sign_csr actions. Used by Makefile sign-https-certificates. * Add SSL echo server and client example * Add examples documentation * Add core pubsub eventing example and add menubar item skeleton (#396) * Initialize menu bar items with click handler and open a popover for preferences * Add Core PubSub eventing example * Remove hardcoded request ids * Move codecov.yml to top level directory (#400) * Add cross ref for how to generate SSL certs. (#401) * Add plugin "FilterByURLRegexPlugin" (#397) * Initial draft of filter_by_url_regex.py * Add FilterByURLRegexPlugin * Fix dictionary key & add logging * Add proper logging * Add better logging * Add logging * move code to handle_client_request * development logging * development * development * development * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * Fix blocked log * Add to FILTER_LIST, some tidy up * Update FILTER_LIST * dev * remove scheme from url * Add to FILTER_LIST * Add to FILTER_LIST * Update FILTER_LIST * commenting * Update FILTER_LIST * After autopep8 * Fix Anomalous backslash in string (pep8) * Address code quality checks - flake8 F401 & W605 * Address flake8 errors * Attempt to fix flake8 errors * Fix linting issues * Address flake8 W292 * Attempt to create tests * Add FilterByURLRegexPlugin * Rename test * Work on tests * Work on tests * Work on tests Co-authored-by: Abhinav Singh * Update tox from 3.16.1 to 3.17.0 (#402) * Update codecov from 2.1.7 to 2.1.8 (#404) * Update tox from 3.17.0 to 3.17.1 (#403) Co-authored-by: Abhinav Singh * Bump lodash from 4.17.15 to 4.17.19 in /dashboard (#405) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update tox from 3.17.1 to 3.18.0 (#406) * Update coverage from 5.2 to 5.2.1 (#407) * Update tox from 3.18.0 to 3.18.1 (#408) * Fix docker build by using correct pip flags (#417) * Update tox from 3.18.1 to 3.19.0 (#416) Co-authored-by: Abhinav Singh * Update autopep8 from 1.5.3 to 1.5.4 (#412) Co-authored-by: Abhinav Singh * Update pytest from 5.4.3 to 6.0.1 (#410) Co-authored-by: Abhinav Singh * npm upgrade (#418) * Remove test for 'HttpWebServerRouteHandler' This does not exist (as fas as I can see) and it bother the linter (Mypy), when I tell it `klass` is a `type` instance. * Pass a list plugin class objects or bytes to proxy when used in embeded mode. No automated tests for the feature yet. * Tests for Flags.load_plugins method. * Ensure plugins are loaded only once. Also changed module name for plugins passed by type. * Update wheel from 0.34.2 to 0.35.0 (#421) * Allow to use types when embeding Proxy (#420) * Remove test for 'HttpWebServerRouteHandler' This does not exist (as fas as I can see) and it bother the linter (Mypy), when I tell it `klass` is a `type` instance. * Pass a list plugin class objects or bytes to proxy when used in embeded mode. No automated tests for the feature yet. * Tests for Flags.load_plugins method. * Ensure plugins are loaded only once. Also changed module name for plugins passed by type. Co-authored-by: Abhinav Singh * Documentation for plugin loading in embedded mode (#422) * Update pytest-cov from 2.10.0 to 2.10.1 (#423) * Update wheel from 0.35.0 to 0.35.1 (#424) * Update typing-extensions from 3.7.4.2 to 3.7.4.3 (#428) * Update codecov from 2.1.8 to 2.1.9 (#427) Co-authored-by: Abhinav Singh * Update pylint from 2.5.3 to 2.6.0 (#426) Co-authored-by: Abhinav Singh * Update paramiko from 2.7.1 to 2.7.2 (#429) * Update pytest from 6.0.1 to 6.1.0 (#436) * Update coverage from 5.2.1 to 5.3 (#433) Co-authored-by: Abhinav Singh * Update tox from 3.19.0 to 3.20.0 (#430) Co-authored-by: Abhinav Singh * Update flake8 from 3.8.3 to 3.8.4 (#439) * Allow plugins to add custom command line flags (#438) * Allow plugins to add custom command line flags. Addresses #301 * Reduce dependency over Flags class. This will be deprecated so that adhoc flags can be added without any additional manual configuration * Fix: Argument 1 to "mock_default_args" of "TestMain" has incompatible type "Namespace"; expected "Mock" * Reduce Flags class to just the initializer. * Store list of action dest in FlagParser * Update pytest from 6.1.0 to 6.1.1 (#440) * More examples (#444) * Refactor into BaseServerHandler and BaseEchoServerHandler classes * Add connect tunnel example * Update rope from 0.17.0 to 0.18.0 (#445) * Update tox from 3.20.0 to 3.20.1 (#446) * Update codecov from 2.1.9 to 2.1.10 (#447) * Update mypy (#449) * Fix path to devtools websocket endpoint, broken after refactoring (#450) * Relax proxy auth requirement to allow mixed case for the auth type e.g. "basic", "Basic", "BaSiC" are all allowed (#451) * Go flagless to allow custom user defined flags. (#452) * Go flagless to allow custom user defined flags. Fixes #301 * Add --cache-dir flag for cache plugin (when used with on-disk store) * Enable discovery of flags from external plugins, example those that reside outside of proxy.py package and loaded on demand. This also allows external flags to surface in --help section * Define --filtered-client-ips flag for FilterByClientIpPlugin * Separate basic auth plugin outside of core server (#453) * Separate basic auth plugin outside of core * Put basic auth plugin at top * Create codeql-analysis.yml (#454) * Create SECURITY.md (#455) * Refactor (#456) * Update pytest from 6.1.1 to 6.1.2 (#457) * npm update (#460) * Refactor base server interfaces into core modules (#461) * Ensure pending buffers are flushed before shutting down in base_server.py Handle unsupported scheme cases within connect_tunnel.py * Move base implementations within core module * Update ssl_echo_server * Update wheel from 0.35.1 to 0.36.0 (#462) * Update wheel from 0.36.0 to 0.36.1 (#463) * Update pytest from 6.1.2 to 6.2.0 (#465) * Update wheel from 0.36.1 to 0.36.2 (#466) * Update pytest from 6.2.0 to 6.2.1 (#467) * Update codecov from 2.1.10 to 2.1.11 (#469) * Add version check for README.md (#471) Co-authored-by: pyup.io bot Co-authored-by: Mike Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Pascal COMBES --- .github/workflows/codeql-analysis.yml | 71 + Dockerfile | 2 +- Makefile | 23 +- README.md | 102 +- SECURITY.md | 16 + tests/codecov.yml => codecov.yml | 0 dashboard/package-lock.json | 1341 +++++++++++------ dashboard/package.json | 34 +- dashboard/src/proxy.css | 2 +- examples/README.md | 138 ++ examples/https_connect_tunnel.py | 85 ++ examples/pubsub_eventing.py | 107 ++ examples/ssl_echo_client.py | 29 + examples/ssl_echo_server.py | 65 + examples/tcp_echo_client.py | 21 + examples/tcp_echo_server.py | 47 + examples/websocket_client.py | 50 + menubar/proxy.py.xcodeproj/project.pbxproj | 6 +- .../UserInterfaceState.xcuserstate | Bin 0 -> 26859 bytes .../xcschemes/xcschememanagement.plist | 14 + menubar/proxy.py/AppDelegate.swift | 84 +- menubar/proxy.py/ContentView.swift | 3 +- menubar/proxy.py/Info.plist | 2 +- proxy/common/constants.py | 5 + proxy/common/flag.py | 55 + proxy/common/flags.py | 567 ------- proxy/common/pki.py | 28 + proxy/common/types.py | 8 +- proxy/common/utils.py | 24 +- proxy/common/version.py | 2 +- proxy/core/acceptor/__init__.py | 4 + proxy/core/acceptor/acceptor.py | 44 +- proxy/core/acceptor/pool.py | 66 +- proxy/core/{ => acceptor}/threadless.py | 96 +- proxy/core/acceptor/work.py | 90 ++ proxy/core/base/__init__.py | 17 + proxy/core/base/tcp_server.py | 106 ++ proxy/core/base/tcp_tunnel.py | 88 ++ proxy/core/connection/client.py | 12 + proxy/core/connection/connection.py | 1 + proxy/core/connection/server.py | 13 +- proxy/core/event/dispatcher.py | 4 +- proxy/core/event/subscriber.py | 8 +- proxy/dashboard/plugin.py | 4 +- proxy/http/handler.py | 150 +- proxy/http/inspector/devtools.py | 11 + proxy/http/inspector/transformer.py | 4 +- proxy/http/parser.py | 21 +- proxy/http/plugin.py | 99 ++ proxy/http/proxy/__init__.py | 2 + proxy/http/proxy/auth.py | 51 + proxy/http/proxy/plugin.py | 4 +- proxy/http/proxy/server.py | 253 ++-- proxy/http/server/pac_plugin.py | 17 + proxy/http/server/plugin.py | 4 +- proxy/http/server/web.py | 27 +- proxy/http/websocket/__init__.py | 18 + proxy/http/websocket/client.py | 109 ++ .../http/{websocket.py => websocket/frame.py} | 98 +- proxy/plugin/__init__.py | 6 + proxy/plugin/cache/cache_responses.py | 3 +- proxy/plugin/cache/store/disk.py | 10 + proxy/plugin/filter_by_client_ip.py | 50 + proxy/plugin/filter_by_url_regex.py | 136 ++ proxy/plugin/modify_chunk_response.py | 51 + proxy/proxy.py | 370 ++++- requirements-release.txt | 4 +- requirements-testing.txt | 20 +- requirements-tunnel.txt | 2 +- requirements.txt | 2 +- setup.py | 5 +- tests/common/test_flags.py | 115 ++ tests/common/test_pki.py | 4 +- tests/core/test_acceptor.py | 4 +- tests/core/test_acceptor_pool.py | 4 +- tests/http/test_http_parser.py | 15 +- tests/http/test_http_proxy.py | 4 +- .../http/test_http_proxy_tls_interception.py | 16 +- tests/http/test_protocol_handler.py | 46 +- tests/http/test_web_server.py | 48 +- tests/http/test_websocket_client.py | 13 +- tests/plugin/test_http_proxy_plugins.py | 31 +- ...ttp_proxy_plugins_with_tls_interception.py | 10 +- tests/plugin/utils.py | 4 +- tests/test_main.py | 67 +- tests/test_set_open_file_limit.py | 8 +- tests/testing/test_embed.py | 3 +- version-check.py | 24 +- 88 files changed, 3767 insertions(+), 1660 deletions(-) create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 SECURITY.md rename tests/codecov.yml => codecov.yml (100%) create mode 100644 examples/README.md create mode 100644 examples/https_connect_tunnel.py create mode 100644 examples/pubsub_eventing.py create mode 100644 examples/ssl_echo_client.py create mode 100644 examples/ssl_echo_server.py create mode 100644 examples/tcp_echo_client.py create mode 100644 examples/tcp_echo_server.py create mode 100644 examples/websocket_client.py create mode 100644 menubar/proxy.py.xcodeproj/project.xcworkspace/xcuserdata/abhinavsingh.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 menubar/proxy.py.xcodeproj/xcuserdata/abhinavsingh.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 proxy/common/flag.py delete mode 100644 proxy/common/flags.py rename proxy/core/{ => acceptor}/threadless.py (69%) create mode 100644 proxy/core/acceptor/work.py create mode 100644 proxy/core/base/__init__.py create mode 100644 proxy/core/base/tcp_server.py create mode 100644 proxy/core/base/tcp_tunnel.py create mode 100644 proxy/http/plugin.py create mode 100644 proxy/http/proxy/auth.py create mode 100644 proxy/http/websocket/__init__.py create mode 100644 proxy/http/websocket/client.py rename proxy/http/{websocket.py => websocket/frame.py} (58%) create mode 100644 proxy/plugin/filter_by_client_ip.py create mode 100644 proxy/plugin/filter_by_url_regex.py create mode 100644 proxy/plugin/modify_chunk_response.py create mode 100644 tests/common/test_flags.py diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..2dd16165 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [develop, master] + pull_request: + # The branches below must be a subset of the branches above + branches: [develop] + schedule: + - cron: '0 14 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['python', 'javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/Dockerfile b/Dockerfile index fb079fcb..d751a120 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ COPY README.md /app/ COPY proxy/ /app/proxy/ WORKDIR /app RUN pip install --upgrade pip && \ - pip install --install-option="--prefix=/deps" . + pip install --prefix=/deps . FROM base diff --git a/Makefile b/Makefile index 17152b4e..e0331a5e 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,8 @@ IMAGE_TAG := $(NS)/$(IMAGE_NAME):$(VERSION) HTTPS_KEY_FILE_PATH := https-key.pem HTTPS_CERT_FILE_PATH := https-cert.pem +HTTPS_CSR_FILE_PATH := https-csr.pem +HTTPS_SIGNED_CERT_FILE_PATH := https-signed-cert.pem CA_KEY_FILE_PATH := ca-key.pem CA_CERT_FILE_PATH := ca-cert.pem @@ -25,6 +27,7 @@ devtools: pushd dashboard && npm run devtools && popd autopep8: + autopep8 --recursive --in-place --aggressive examples autopep8 --recursive --in-place --aggressive proxy autopep8 --recursive --in-place --aggressive tests autopep8 --recursive --in-place --aggressive setup.py @@ -40,6 +43,20 @@ https-certificates: --private-key-path $(HTTPS_KEY_FILE_PATH) \ --public-key-path $(HTTPS_CERT_FILE_PATH) +sign-https-certificates: + # Generate CSR request + python -m proxy.common.pki gen_csr \ + --csr-path $(HTTPS_CSR_FILE_PATH) \ + --private-key-path $(HTTPS_KEY_FILE_PATH) \ + --public-key-path $(HTTPS_CERT_FILE_PATH) + # Sign CSR with CA + python -m proxy.common.pki sign_csr \ + --csr-path $(HTTPS_CSR_FILE_PATH) \ + --crt-path $(HTTPS_SIGNED_CERT_FILE_PATH) \ + --hostname example.com \ + --private-key-path $(CA_KEY_FILE_PATH) \ + --public-key-path $(CA_CERT_FILE_PATH) + ca-certificates: # Generate CA key python -m proxy.common.pki gen_private_key \ @@ -73,8 +90,8 @@ lib-clean: rm -rf .hypothesis lib-lint: - flake8 --ignore=W504 --max-line-length=127 --max-complexity=19 proxy/ tests/ setup.py - mypy --strict --ignore-missing-imports proxy/ tests/ setup.py + flake8 --ignore=W504 --max-line-length=127 --max-complexity=19 examples/ proxy/ tests/ setup.py + mypy --strict --ignore-missing-imports examples/ proxy/ tests/ setup.py lib-test: lib-clean lib-version lib-lint pytest -v tests/ @@ -93,7 +110,7 @@ lib-coverage: open htmlcov/index.html lib-profile: - sudo py-spy -F -f profile.svg -d 3600 proxy.py + sudo py-spy record -o profile.svg -t -F -s -- python -m proxy dashboard: pushd dashboard && npm run build && popd diff --git a/README.md b/README.md index ed6f8b04..2fad226b 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ Table of Contents * [Cache Responses Plugin](#cacheresponsesplugin) * [Man-In-The-Middle Plugin](#maninthemiddleplugin) * [Proxy Pool Plugin](#proxypoolplugin) + * [FilterByClientIpPlugin](#filterbyclientipplugin) + * [ModifyChunkResponsePlugin](#modifychunkresponseplugin) * [HTTP Web Server Plugins](#http-web-server-plugins) * [Reverse Proxy](#reverse-proxy) * [Web Server Route](#web-server-route) @@ -70,6 +72,7 @@ Table of Contents * [Embed proxy.py](#embed-proxypy) * [Blocking Mode](#blocking-mode) * [Non-blocking Mode](#non-blocking-mode) + * [Loading Plugins](#loading-plugins) * [Unit testing with proxy.py](#unit-testing-with-proxypy) * [proxy.TestCase](#proxytestcase) * [Override Startup Flags](#override-startup-flags) @@ -669,6 +672,57 @@ Make a curl request via `8899` proxy: Verify that `8899` proxy forwards requests to upstream proxies by checking respective logs. +### FilterByClientIpPlugin + +Reject traffic from specific IP addresses. By default this +plugin blocks traffic from `127.0.0.1` and `::1`. + +Start `proxy.py` as: + +```bash +❯ proxy \ + --plugins proxy.plugin.FilterByClientIpPlugin +``` + +Send a request using `curl -v -x localhost:8899 http://google.com`: + +```bash +... [redacted] ... +> Proxy-Connection: Keep-Alive +> +< HTTP/1.1 418 I'm a tea pot +< Connection: close +< +* Closing connection 0 +``` + +Modify plugin to your taste e.g. Allow specific IP addresses only. + +### ModifyChunkResponsePlugin + +This plugin demonstrate how to modify chunked encoded responses. In able to do so, this plugin uses `proxy.py` core to parse the chunked encoded response. Then we reconstruct the response using custom hardcoded chunks, ignoring original chunks received from upstream server. + +Start `proxy.py` as: + +```bash +❯ proxy \ + --plugins proxy.plugin.ModifyChunkResponsePlugin +``` + +Verify using `curl -v -x localhost:8899 http://httpbin.org/stream/5`: + +```bash +... [redacted] ... +modify +chunk +response +plugin +* Connection #0 to host localhost left intact +* Closing connection 0 +``` + +Modify `ModifyChunkResponsePlugin` to your taste. Example, instead of sending hardcoded chunks, parse and modify the original `JSON` chunks received from the upstream server. + ## HTTP Web Server Plugins ### Reverse Proxy @@ -774,6 +828,22 @@ Verify using `curl -x https://localhost:8899 --proxy-cacert https-cert.pem https } ``` +If you want to avoid passing `--proxy-cacert` flag, also consider signing generated SSL certificates. Example: + +First, generate CA certificates: + +```bash +make ca-certificates +``` + +Then, sign SSL certificate: + +```bash +make sign-https-certificates +``` + +Now restart the server with `--cert-file https-signed-cert.pem` flag. Note that you must also trust generated `ca-cert.pem` in your system keychain. + TLS Interception ================= @@ -1103,6 +1173,36 @@ Note that: input arguments e.g. `start(['--port', '8899'])` or by using passing flags as kwargs e.g. `start(port=8899)`. +## Loading Plugins + +You can, of course, list plugins to load in the input arguments list of `proxy.main`, `proxy.start` or the `Proxy` constructor. Use the `--plugins` flag as when starting from command line: + +```python +import proxy + +if __name__ == '__main__': + proxy.main([ + '--plugins', 'proxy.plugin.CacheResponsesPlugin', + ]) +``` + +However, for simplicity you can pass the list of plugins to load as a keyword argument to `proxy.main`, `proxy.start` or the `Proxy` constructor: + +```python +import proxy +from proxy.plugin import FilterByUpstreamHostPlugin + +if __name__ == '__main__': + proxy.main([], plugins=[ + b'proxy.plugin.CacheResponsesPlugin', + FilterByUpstreamHostPlugin, + ]) +``` + +Note that it supports: +1. The fully-qualified name of a class as `bytes` +2. Any `type` instance for a Proxy.py plugin class. This is espacially useful for custom plugins defined locally. + Unit testing with proxy.py ========================== @@ -1603,7 +1703,7 @@ usage: proxy [-h] [--backlog BACKLOG] [--basic-auth BASIC_AUTH] [--static-server-dir STATIC_SERVER_DIR] [--threadless] [--timeout TIMEOUT] [--version] -proxy.py v2.2.0 +proxy.py v2.3.0 optional arguments: -h, --help show this help message and exit diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..606fe61b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 2.x | :white_check_mark: | +| < 2.x | :x: | + +## Reporting a Vulnerability + +Follow these steps: + +1. Start by [emailing developers](mailto:mailsforabhinav+proxy@gmail.com) +2. If unresponsive, [create a public issue](https://github.com/abhinavsingh/proxy.py/issues/new/choose) +3. [Pull requests](https://github.com/abhinavsingh/proxy.py/pulls) are always welcome diff --git a/tests/codecov.yml b/codecov.yml similarity index 100% rename from tests/codecov.yml rename to codecov.yml diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index a28f3279..84c042cb 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -5,22 +5,28 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.0.0" + "@babel/highlight": "^7.10.4" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, @@ -32,14 +38,16 @@ "requires": { "@nodelib/fs.stat": "2.0.3", "run-parallel": "^1.1.9" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + } } }, - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", - "dev": true - }, "@nodelib/fs.walk": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", @@ -63,62 +71,61 @@ "dev": true }, "@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz", + "integrity": "sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==", "dev": true }, "@types/fs-extra": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.1.tgz", - "integrity": "sha512-J00cVDALmi/hJOYsunyT52Hva5TnJeKP5yd1r+mH/ZU0mbYZflR0Z5kw5kITtKTRYMhm1JMClOFYdHnQszEvqw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz", + "integrity": "sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w==", "dev": true, "requires": { "@types/node": "*" } }, "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { - "@types/events": "*", "@types/minimatch": "*", "@types/node": "*" } }, "@types/jasmine": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.4.tgz", - "integrity": "sha512-+/sHcTPyDS1JQacDRRRWb+vNrjBwnD+cKvTaWlxlJ/uOOFvzCkjOwNaqVjYMLfsjzNi0WtDH9RyReDXPG1Cdug==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.1.tgz", + "integrity": "sha512-eeSCVhBsgwHNS1FmaMu4zrLxfykCTWJMLFZv7lmyrZQjw7foUUXoPu4GukSN9v7JvUw7X+/aDH3kCaymirBSTg==", "dev": true }, "@types/jquery": { - "version": "3.3.31", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz", - "integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.4.tgz", + "integrity": "sha512-//9CHhaUt/rurMJTxGI+I6DmsNHgYU6d8aSLFfO5dB7+10lwLnaWT0z5GY/yY82Q/M+B+0Qh3TixlJ8vmBeqIw==", "dev": true, "requires": { "@types/sizzle": "*" } }, "@types/js-cookie": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.4.tgz", - "integrity": "sha512-WTfSE1Eauak/Nrg6cA9FgPTFvVawejsai6zXoq0QYTQ3mxONeRtGhKxa7wMlUzWWmzrmTeV+rwLjHgsCntdrsA==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz", + "integrity": "sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==", "dev": true }, "@types/json-schema": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", - "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, "@types/minimatch": { @@ -128,9 +135,9 @@ "dev": true }, "@types/node": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", - "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==", + "version": "14.0.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", + "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==", "dev": true }, "@types/sizzle": { @@ -146,52 +153,65 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.5.0.tgz", - "integrity": "sha512-ddrJZxp5ns1Lh5ofZQYk3P8RyvKfyz/VcRR4ZiJLHO/ljnQAO8YvTfj268+WJOOadn99mvDiqJA65+HAKoeSPA==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.5.0", - "eslint-utils": "^1.4.2", + "@typescript-eslint/experimental-utils": "2.34.0", "functional-red-black-tree": "^1.0.1", - "regexpp": "^2.0.1", + "regexpp": "^3.0.0", "tsutils": "^3.17.1" } }, "@typescript-eslint/experimental-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.5.0.tgz", - "integrity": "sha512-UgcQGE0GKJVChyRuN1CWqDW8Pnu7+mVst0aWrhiyuUD1J9c+h8woBdT4XddCvhcXDodTDVIfE3DzGHVjp7tUeQ==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.5.0", - "eslint-scope": "^5.0.0" + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } } }, "@typescript-eslint/parser": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.5.0.tgz", - "integrity": "sha512-9UBMiAwIDWSl79UyogaBdj3hidzv6exjKUx60OuZuFnJf56tq/UMpdPcX09YmGqE8f4AnAueYtBxV8IcAT3jdQ==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.5.0", - "@typescript-eslint/typescript-estree": "2.5.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.5.0.tgz", - "integrity": "sha512-AXURyF8NcA3IsnbjNX1v9qbwa0dDoY9YPcKYR2utvMHoUcu3636zrz0gRWtVAyxbPCkhyKuGg6WZIyi2Fc79CA==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", "dev": true, "requires": { "debug": "^4.1.1", - "glob": "^7.1.4", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", "is-glob": "^4.0.1", - "lodash.unescape": "4.0.1", - "semver": "^6.3.0" + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" }, "dependencies": { "debug": { @@ -204,23 +224,23 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true } } }, "abab": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.2.tgz", - "integrity": "sha512-2scffjvioEmNz0OyDSLGWDfKCVwaKc6l9Pm9kOIREU13ClXZvHpg/nRL5xyjSSSLhOnXqft2HpsAzNEEA8cFFg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", + "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==", "dev": true }, "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true }, "acorn-globals": { @@ -231,8 +251,22 @@ "requires": { "acorn": "^6.0.1", "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + } } }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, "acorn-walk": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", @@ -240,12 +274,12 @@ "dev": true }, "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -295,13 +329,14 @@ "dev": true }, "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" } }, "array-union": { @@ -310,6 +345,16 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -346,12 +391,6 @@ "lodash": "^4.17.14" } }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -365,9 +404,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, "babel-polyfill": { @@ -438,19 +477,10 @@ "concat-map": "0.0.1" } }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, "buffer-from": { @@ -459,6 +489,16 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -478,9 +518,9 @@ "dev": true }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -501,9 +541,9 @@ "dev": true }, "chrome-devtools-frontend": { - "version": "1.0.706688", - "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.706688.tgz", - "integrity": "sha512-2zOpA/bTouVt9/hhLQbaNMPVHHJH+dW1OkR/PFagCqYp2+mrPgXr1vi44BdzMOEw+qIhj7x5wg2mZFK02/30MQ==", + "version": "1.0.827632", + "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.827632.tgz", + "integrity": "sha512-8qtv6zxlaoZhmtPFrR/dAyXjCKCSXL8s7SeAaHyICpd7p6mfXS+y+b14E66LLbOIEMwpidmy3xUJPePx8NuWHg==", "dev": true }, "cli-cursor": { @@ -567,9 +607,9 @@ "dev": true }, "colorette": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.1.0.tgz", - "integrity": "sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "dev": true }, "colors": { @@ -635,15 +675,15 @@ "dev": true }, "cssom": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.1.tgz", - "integrity": "sha512-6Aajq0XmukE7HdXUU6IoSWuH1H6gH9z6qmagsstTiN7cW2FNTsb+J2Chs+ufPgZCsV/yo8oaEudQLrb9dGxSVQ==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true }, "cssstyle": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.0.0.tgz", - "integrity": "sha512-QXSAu2WBsSRXCPjvI43Y40m6fMevvyRm8JVAuF9ksQz5jha4pWP1wpaK7Yu5oLFc6+XAY+hj8YhefyXcBB53gg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, "requires": { "cssom": "~0.3.6" @@ -777,9 +817,9 @@ } }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "encoding": { @@ -801,27 +841,28 @@ } }, "es-abstract": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", - "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", "dev": true, "requires": { - "es-to-primitive": "^1.2.0", + "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.0", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.6.0", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -835,10 +876,31 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, "eslint": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.5.1.tgz", - "integrity": "sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -848,19 +910,19 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.2", + "eslint-utils": "^1.4.3", "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.1", + "espree": "^6.1.2", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", - "globals": "^11.7.0", + "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", + "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -869,7 +931,7 @@ "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.2", + "optionator": "^0.8.3", "progress": "^2.0.0", "regexpp": "^2.0.1", "semver": "^6.1.2", @@ -880,36 +942,67 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - }, - "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", - "dev": true - }, "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } }, "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -940,17 +1033,6 @@ "ms": "^2.1.1" } }, - "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", - "dev": true, - "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", - "eslint-visitor-keys": "^1.1.0" - } - }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -962,6 +1044,21 @@ "tmp": "^0.0.33" } }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -969,45 +1066,138 @@ "dev": true }, "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" } } } }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "onetime": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.1.tgz", + "integrity": "sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -1015,24 +1205,47 @@ "dev": true, "requires": { "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true } } }, "eslint-config-standard": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz", - "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz", + "integrity": "sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg==", "dev": true }, "eslint-import-resolver-node": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", - "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", "dev": true, "requires": { "debug": "^2.6.9", - "resolve": "^1.5.0" + "resolve": "^1.13.1" }, "dependencies": { "debug": { @@ -1049,16 +1262,26 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } } } }, "eslint-module-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", - "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", "dev": true, "requires": { - "debug": "^2.6.8", + "debug": "^2.6.9", "pkg-dir": "^2.0.0" }, "dependencies": { @@ -1098,22 +1321,24 @@ } }, "eslint-plugin-import": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", - "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", "dev": true, "requires": { - "array-includes": "^3.0.3", + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-module-utils": "^2.6.0", "has": "^1.0.3", "minimatch": "^3.0.4", - "object.values": "^1.1.0", + "object.values": "^1.1.1", "read-pkg-up": "^2.0.0", - "resolve": "^1.11.0" + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" }, "dependencies": { "debug": { @@ -1140,6 +1365,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } } } }, @@ -1172,15 +1407,15 @@ "dev": true }, "eslint-plugin-standard": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", - "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz", + "integrity": "sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ==", "dev": true }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -1202,6 +1437,17 @@ "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", "dev": true }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, "esprima": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", @@ -1209,12 +1455,20 @@ "dev": true }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } } }, "esrecurse": { @@ -1227,9 +1481,9 @@ } }, "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, "estree-walker": { @@ -1289,28 +1543,15 @@ "dev": true }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.0.tgz", - "integrity": "sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2" - } - }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "fast-levenshtein": { @@ -1320,12 +1561,12 @@ "dev": true }, "fastq": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", - "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", "dev": true, "requires": { - "reusify": "^1.0.0" + "reusify": "^1.0.4" } }, "figures": { @@ -1346,15 +1587,6 @@ "flat-cache": "^2.0.1" } }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -1376,9 +1608,9 @@ } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, "follow-redirects": { @@ -1442,6 +1674,17 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -1458,9 +1701,9 @@ } }, "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -1472,19 +1715,22 @@ } }, "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { "is-glob": "^4.0.1" } }, "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } }, "globby": { "version": "10.0.1", @@ -1500,6 +1746,71 @@ "ignore": "^5.1.1", "merge2": "^1.2.3", "slash": "^3.0.0" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } } }, "google-libphonenumber": { @@ -1521,12 +1832,12 @@ "dev": true }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, @@ -1555,9 +1866,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "he": { @@ -1637,9 +1948,9 @@ "dev": true }, "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -1741,15 +2052,24 @@ "dev": true }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", "dev": true }, + "is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "dev": true }, "is-extglob": { @@ -1776,21 +2096,12 @@ "is-extglob": "^2.1.1" } }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", "dev": true }, - "is-plain-object": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", - "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", - "dev": true, - "requires": { - "isobject": "^4.0.0" - } - }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -1798,12 +2109,12 @@ "dev": true }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { - "has": "^1.0.1" + "has-symbols": "^1.0.1" } }, "is-stream": { @@ -1812,13 +2123,19 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "^1.0.1" } }, "is-typedarray": { @@ -1839,12 +2156,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "isobject": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", - "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", - "dev": true - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1852,19 +2163,19 @@ "dev": true }, "jasmine": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz", - "integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.6.3.tgz", + "integrity": "sha512-Th91zHsbsALWjDUIiU5d/W5zaYQsZFMPTdeNmi8GivZPmAaUAK8MblSG3yQI4VMGC/abF2us7ex60NH1AAIMTA==", "dev": true, "requires": { - "glob": "^7.1.4", - "jasmine-core": "~3.5.0" + "glob": "^7.1.6", + "jasmine-core": "~3.6.0" } }, "jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz", + "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==", "dev": true }, "jasmine-ts": { @@ -2136,9 +2447,9 @@ } }, "jquery": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz", - "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==", "dev": true }, "js-cookie": { @@ -2160,9 +2471,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -2184,9 +2495,9 @@ "dev": true }, "jsdom": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.0.tgz", - "integrity": "sha512-+hRyEfjRPFwTYMmSQ3/f7U9nP8ZNZmbkmUek760ZpxnCPWJIhaaLRuUSvpJ36fZKCGENxLwxClzwpOpnXNfChQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", "dev": true, "requires": { "abab": "^2.0.0", @@ -2199,7 +2510,7 @@ "domexception": "^1.0.1", "escodegen": "^1.11.1", "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.1.4", + "nwsapi": "^2.2.0", "parse5": "5.1.0", "pn": "^1.1.0", "request": "^2.88.0", @@ -2215,33 +2526,6 @@ "whatwg-url": "^7.0.0", "ws": "^7.0.0", "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - }, - "escodegen": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", - "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } } }, "json-schema": { @@ -2268,6 +2552,15 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2331,9 +2624,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.sortby": { @@ -2342,12 +2635,6 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", - "dev": true - }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -2385,21 +2672,11 @@ } }, "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -2407,18 +2684,18 @@ "dev": true }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", "dev": true }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", "dev": true, "requires": { - "mime-db": "1.40.0" + "mime-db": "1.44.0" } }, "mimic-fn": { @@ -2519,9 +2796,9 @@ "dev": true }, "nwsapi": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", - "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, "oauth-sign": { @@ -2537,9 +2814,9 @@ "dev": true }, "object-inspect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", - "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", "dev": true }, "object-keys": { @@ -2548,14 +2825,26 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", + "es-abstract": "^1.17.0-next.1", "function-bind": "^1.1.1", "has": "^1.0.3" } @@ -2766,9 +3055,9 @@ "dev": true }, "picomatch": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", - "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "dev": true }, "pify": { @@ -2837,9 +3126,9 @@ "dev": true }, "psl": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", - "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, "punycode": { @@ -2876,15 +3165,15 @@ } }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -2894,7 +3183,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -2904,17 +3193,11 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" }, "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -2922,33 +3205,33 @@ "dev": true }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "psl": "^1.1.28", + "punycode": "^2.1.1" } } } }, "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", "dev": true, "requires": { - "lodash": "^4.17.11" + "lodash": "^4.17.19" } }, "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", "dev": true, "requires": { - "request-promise-core": "1.1.2", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, @@ -3024,35 +3307,35 @@ } }, "rollup": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.24.0.tgz", - "integrity": "sha512-PiFETY/rPwodQ8TTC52Nz2DSCYUATznGh/ChnxActCr8rV5FIk3afBUb3uxNritQW/Jpbdn3kq1Rwh1HHYMwdQ==", + "version": "1.32.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", + "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", "dev": true, "requires": { "@types/estree": "*", "@types/node": "*", "acorn": "^7.1.0" - }, - "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - } } }, "rollup-plugin-copy": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.1.0.tgz", - "integrity": "sha512-oVw3ljRV5jv7Yw/6eCEHntVs9Mc+NFglc0iU0J8ei76gldYmtBQ0M/j6WAkZUFVRSrhgfCrEakUllnN87V2f4w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.3.0.tgz", + "integrity": "sha512-euDjCUSBXZa06nqnwCNADbkAcYDfzwowfZQkto9K/TFhiH+QG7I4PUsEMwM9tDgomGWJc//z7KLW8t+tZwxADA==", "dev": true, "requires": { - "@types/fs-extra": "^8.0.0", + "@types/fs-extra": "^8.0.1", "colorette": "^1.1.0", "fs-extra": "^8.1.0", "globby": "10.0.1", "is-plain-object": "^3.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true + } } }, "rollup-plugin-javascript-obfuscator": { @@ -3105,18 +3388,18 @@ "dev": true }, "rxjs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", - "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, "safer-buffer": { @@ -3314,24 +3597,68 @@ } } }, - "string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "string.prototype.trimend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, - "string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "string.prototype.trimstart": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } } }, "strip-ansi": { @@ -3356,9 +3683,9 @@ "dev": true }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { @@ -3394,6 +3721,12 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -3443,15 +3776,6 @@ "os-tmpdir": "~1.0.2" } }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -3488,10 +3812,22 @@ "yn": "^2.0.0" } }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", "dev": true }, "tsutils": { @@ -3527,10 +3863,16 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, "union": { @@ -3564,15 +3906,15 @@ "dev": true }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, "validate-npm-package-license": { @@ -3597,12 +3939,12 @@ } }, "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", "dev": true, "requires": { - "browser-process-hrtime": "^0.1.2" + "browser-process-hrtime": "^1.0.0" } }, "w3c-xmlserializer": { @@ -3663,6 +4005,12 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -3708,13 +4056,10 @@ } }, "ws": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", - "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", - "dev": true, - "requires": { - "async-limiter": "^1.0.0" - } + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==", + "dev": true }, "xml-name-validator": { "version": "3.0.0", diff --git a/dashboard/package.json b/dashboard/package.json index 0f63c92d..236e2d15 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -25,31 +25,31 @@ }, "homepage": "https://github.com/abhinavsingh/proxy.py#readme", "devDependencies": { - "@types/jasmine": "^3.4.4", - "@types/jquery": "^3.3.31", - "@types/js-cookie": "^2.2.4", - "@typescript-eslint/eslint-plugin": "^2.5.0", - "@typescript-eslint/parser": "^2.5.0", - "chrome-devtools-frontend": "^1.0.706688", - "eslint": "^6.5.1", - "eslint-config-standard": "^14.1.0", - "eslint-plugin-import": "^2.18.2", + "@types/jasmine": "^3.6.1", + "@types/jquery": "^3.5.4", + "@types/js-cookie": "^2.2.6", + "@typescript-eslint/eslint-plugin": "^2.34.0", + "@typescript-eslint/parser": "^2.34.0", + "chrome-devtools-frontend": "^1.0.827632", + "eslint": "^6.8.0", + "eslint-config-standard": "^14.1.1", + "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^10.0.0", "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-standard": "^4.0.1", + "eslint-plugin-standard": "^4.1.0", "http-server": "^0.12.3", - "jasmine": "^3.5.0", + "jasmine": "^3.6.3", "jasmine-ts": "^0.3.0", - "jquery": "^3.5.0", + "jquery": "^3.5.1", "js-cookie": "^2.2.1", - "jsdom": "^15.2.0", + "jsdom": "^15.2.1", "ncp": "^2.0.0", - "rollup": "^1.24.0", - "rollup-plugin-copy": "^3.1.0", + "rollup": "^1.32.1", + "rollup-plugin-copy": "^3.3.0", "rollup-plugin-javascript-obfuscator": "^1.0.4", "rollup-plugin-typescript": "^1.0.1", "ts-node": "^7.0.1", - "typescript": "^3.6.4", - "ws": "^7.2.0" + "typescript": "^3.9.7", + "ws": "^7.4.0" } } diff --git a/dashboard/src/proxy.css b/dashboard/src/proxy.css index e109ecfc..36126a62 100644 --- a/dashboard/src/proxy.css +++ b/dashboard/src/proxy.css @@ -15,7 +15,7 @@ html, body { } main { - height: 88%; + height: 89.75%; } section { diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..84bf0f44 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,138 @@ +# Proxy Library Examples + +This directory contains examples that demonstrate `proxy.py` core library capabilities. + +Looking for `proxy.py` plugin examples? Check [proxy/plugin](https://github.com/abhinavsingh/proxy.py/tree/develop/proxy/plugin) directory. + +Table of Contents +================= +* [WebSocket Client](#websocket-client) +* [TCP Echo Server](#tcp-echo-server) +* [TCP Echo Client](#tcp-echo-client) +* [SSL Echo Server](#ssl-echo-server) +* [SSL Echo Client](#ssl-echo-client) +* [PubSub Eventing](#pubsub-eventing) +* [Https Connect Tunnel](#https-connect-tunnel) + +## WebSocket Client + +1. Makes use of `proxy.http.websocket.WebsocketClient` which is built on-top of `asyncio` +2. `websocket_client.py` by default opens a WebSocket connection to `ws://echo.websocket.org`. +3. Client will exchange `num_echos = 10` packets with the server and then shutdown. + +Start `websocket_client.py` as: + +```bash +❯ PYTHONPATH=. python examples/websocket_client.py +Received b'hello' after 306 millisec +Received b'hello' after 308 millisec +Received b'hello' after 277 millisec +Received b'hello' after 334 millisec +Received b'hello' after 296 millisec +Received b'hello' after 317 millisec +Received b'hello' after 307 millisec +Received b'hello' after 307 millisec +Received b'hello' after 306 millisec +Received b'hello' after 307 millisec +Received b'hello' after 309 millisec +``` + +## TCP Echo Server + +1. Makes use of `proxy.core.acceptor.AcceptorPool`, same multicore acceptor used internally by `proxy.py` server. +2. Implements `proxy.core.acceptor.Work` interface to handle incoming client connections. + +Start `tcp_echo_server.py` as: + +```bash +❯ PYTHONPATH=. python examples/tcp_echo_server.py +Connection accepted from ('::1', 53285, 0, 0) +Connection closed by client ('::1', 53285, 0, 0) +``` + +## TCP Echo Client + +1. Makes use of `proxy.common.utils.socket_connection` to establish a TCP socket connection with our TCP echo server. +2. Exchanges packet with server in an infinite loop. Press `CTRL+C` to stop. + +Start `tcp_echo_client.py` as: + +```bash +❯ PYTHONPATH=. python examples/tcp_echo_client.py +b'hello' +b'hello' +b'hello' +b'hello' +b'hello' +... +... +... +^CTraceback (most recent call last): + File "examples/tcp_echo_client.py", line 18, in + data = client.recv(DEFAULT_BUFFER_SIZE) +KeyboardInterrupt +``` + +## SSL Echo Server + +1. Same as `tcp_echo_server.py`. +2. Internally uses `proxy.common.utils.wrap_socket` to enable SSL encryption. +3. Uses `https-key.pem` and `https-signed-cert.pem` for SSL encryption. See [End-to-End Encryption](https://github.com/abhinavsingh/proxy.py#end-to-end-encryption) for instructions on how to generate SSL certificates. + +Start `ssl_echo_server.py` as: + +```bash +❯ PYTHONPATH=. python examples/ssl_echo_server.py +``` + +## SSL Echo Client + +1. Makes use of `proxy.core.connection.TcpServerConnection` to establish a SSL connection with our `ssl_echo_server.py`. +2. Uses generated `ca-cert.pem` for SSL certificate verification. + +Start `ssl_echo_client.py` as: + +```bash +❯ PYTHONPATH=. python examples/ssl_echo_client.py +``` + +## PubSub Eventing + +1. Makes use of `proxy.py` core eventing module. +2. A `proxy.core.event.EventDispatcher` thread is started. +3. A `proxy.core.event.EventSubscriber` thread is started. +4. A `multiprocessing.Process` publisher is started. +5. Main thread also publishes into `EventDispatcher` queue. +6. Events from both the main thread and another process are received by the subscriber. + +Start `pubsub_eventing.py` as: + +```bash +❯ PYTHONPATH=. python examples/pubsub_eventing.py +DEBUG:proxy.core.event.subscriber:Subscribed relay sub id 5eb22010764f4d44900f41e2fb408ca6 from core events +publisher starting +^Cpublisher shutdown +bye!!! +DEBUG:proxy.core.event.subscriber:Un-subscribed relay sub id 5eb22010764f4d44900f41e2fb408ca6 from core events +Received 52724 events from main thread, 60172 events from another process, in 21.50117802619934 seconds +``` + +## HTTPS Connect Tunnel + +A simple HTTP proxy server supporting only CONNECT (https) requests. + +1. Uses `HttpParser` for request parsing. +2. Uses `TcpServerConnection` to establish upstream connection. +3. Overrides `BaseServer` methods to also register read/write events for upstream connection. + +Start `https_connect_tunnel.py` as: + +``` +❯ PYTHONPATH=. python examples/https_connect_tunnel.py +``` + +Send https requests via tunnel as: + +``` +❯ curl -x localhost:12345 https://httpbin.org/get +``` diff --git a/examples/https_connect_tunnel.py b/examples/https_connect_tunnel.py new file mode 100644 index 00000000..95018646 --- /dev/null +++ b/examples/https_connect_tunnel.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import time +from typing import Any, Optional + +from proxy.proxy import Proxy +from proxy.common.utils import build_http_response +from proxy.http.codes import httpStatusCodes +from proxy.http.parser import httpParserStates +from proxy.http.methods import httpMethods +from proxy.core.acceptor import AcceptorPool +from proxy.core.base import BaseTcpTunnelHandler + + +class HttpsConnectTunnelHandler(BaseTcpTunnelHandler): + """A https CONNECT tunnel.""" + + PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT = memoryview(build_http_response( + httpStatusCodes.OK, + reason=b'Connection established' + )) + + PROXY_TUNNEL_UNSUPPORTED_SCHEME = memoryview(build_http_response( + httpStatusCodes.BAD_REQUEST, + headers={b'Connection': b'close'}, + reason=b'Unsupported protocol scheme' + )) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + def handle_data(self, data: memoryview) -> Optional[bool]: + # Queue for upstream if connection has been established + if self.upstream and self.upstream._conn is not None: + self.upstream.queue(data) + return None + + # Parse client request + self.request.parse(data) + + # Drop the request if not a CONNECT request + if self.request.method != httpMethods.CONNECT: + self.client.queue( + HttpsConnectTunnelHandler.PROXY_TUNNEL_UNSUPPORTED_SCHEME) + return True + + # CONNECT requests are short and we need not worry about + # receiving partial request bodies here. + assert self.request.state == httpParserStates.COMPLETE + + # Establish connection with upstream + self.connect_upstream() + + # Queue tunnel established response to client + self.client.queue( + HttpsConnectTunnelHandler.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) + + return None + + +def main() -> None: + # This example requires `threadless=True` + pool = AcceptorPool( + flags=Proxy.initialize(port=12345, num_workers=1, threadless=True), + work_klass=HttpsConnectTunnelHandler) + try: + pool.setup() + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + pool.shutdown() + + +if __name__ == '__main__': + main() diff --git a/examples/pubsub_eventing.py b/examples/pubsub_eventing.py new file mode 100644 index 00000000..3e247c38 --- /dev/null +++ b/examples/pubsub_eventing.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import time +import threading +import multiprocessing +import logging + +from typing import Dict, Any + +from proxy.core.event import EventQueue, EventSubscriber, EventDispatcher, eventNames + +# Enable debug logging to view core event logs +logging.basicConfig(level=logging.DEBUG) + +# Eventing requires a multiprocess safe queue +# so that events can be safely published and received +# between processes. +manager = multiprocessing.Manager() + +main_publisher_request_id = '1234' +process_publisher_request_id = '12345' +num_events_received = [0, 0] + + +def publisher_process(shutdown_event: multiprocessing.synchronize.Event, + dispatcher_queue: EventQueue) -> None: + print('publisher starting') + try: + while not shutdown_event.is_set(): + dispatcher_queue.publish( + request_id=process_publisher_request_id, + event_name=eventNames.WORK_STARTED, + event_payload={'time': time.time()}, + publisher_id='eventing_pubsub_process' + ) + except KeyboardInterrupt: + pass + print('publisher shutdown') + + +def on_event(payload: Dict[str, Any]) -> None: + '''Subscriber callback.''' + global num_events_received + if payload['request_id'] == main_publisher_request_id: + num_events_received[0] += 1 + else: + num_events_received[1] += 1 + # print(payload) + + +if __name__ == '__main__': + start_time = time.time() + + # Start dispatcher thread + dispatcher_queue = EventQueue(manager.Queue()) + dispatcher_shutdown_event = threading.Event() + dispatcher = EventDispatcher( + shutdown=dispatcher_shutdown_event, + event_queue=dispatcher_queue) + dispatcher_thread = threading.Thread(target=dispatcher.run) + dispatcher_thread.start() + + # Create a subscriber + subscriber = EventSubscriber(dispatcher_queue) + # Internally, subscribe will start a separate thread + # to receive incoming published messages + subscriber.subscribe(on_event) + + # Start a publisher process to demonstrate safe exchange + # of messages between processes. + publisher_shutdown_event = multiprocessing.Event() + publisher = multiprocessing.Process( + target=publisher_process, args=( + publisher_shutdown_event, dispatcher_queue, )) + publisher.start() + + try: + while True: + # Dispatch event from main process + dispatcher_queue.publish( + request_id=main_publisher_request_id, + event_name=eventNames.WORK_STARTED, + event_payload={'time': time.time()}, + publisher_id='eventing_pubsub_main' + ) + except KeyboardInterrupt: + print('bye!!!') + finally: + # Stop publisher + publisher_shutdown_event.set() + publisher.join() + # Stop subscriber thread + subscriber.unsubscribe() + # Signal dispatcher to shutdown + dispatcher_shutdown_event.set() + # Wait for dispatcher shutdown + dispatcher_thread.join() + print('Received {0} events from main thread, {1} events from another process, in {2} seconds'.format( + num_events_received[0], num_events_received[1], time.time() - start_time)) diff --git a/examples/ssl_echo_client.py b/examples/ssl_echo_client.py new file mode 100644 index 00000000..227b26c9 --- /dev/null +++ b/examples/ssl_echo_client.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from proxy.core.connection import TcpServerConnection +from proxy.common.constants import DEFAULT_BUFFER_SIZE + +if __name__ == '__main__': + client = TcpServerConnection('::', 12345) + client.connect() + client.wrap('example.com', ca_file='ca-cert.pem') + # wrap() will by default set connection to nonblocking + # flip it back to blocking + client.connection.setblocking(True) + try: + while True: + client.send(b'hello') + data = client.recv(DEFAULT_BUFFER_SIZE) + if data is None: + break + print(data.tobytes()) + finally: + client.close() diff --git a/examples/ssl_echo_server.py b/examples/ssl_echo_server.py new file mode 100644 index 00000000..013bc3a5 --- /dev/null +++ b/examples/ssl_echo_server.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import time +from typing import Optional + +from proxy.proxy import Proxy +from proxy.common.utils import wrap_socket +from proxy.core.acceptor import AcceptorPool +from proxy.core.connection import TcpClientConnection + +from proxy.core.base import BaseTcpServerHandler + + +class EchoSSLServerHandler(BaseTcpServerHandler): + """Wraps client socket during initialization.""" + + def initialize(self) -> None: + # Acceptors don't perform TLS handshake. Perform the same + # here using wrap_socket() utility. + assert self.flags.keyfile is not None and self.flags.certfile is not None + conn = wrap_socket( + self.client.connection, + self.flags.keyfile, + self.flags.certfile) + conn.setblocking(False) + # Upgrade plain TcpClientConnection to SSL connection object + self.client = TcpClientConnection( + conn=conn, addr=self.client.addr) + + def handle_data(self, data: memoryview) -> Optional[bool]: + # echo back to client + self.client.queue(data) + return None + + +def main() -> None: + # This example requires `threadless=True` + pool = AcceptorPool( + flags=Proxy.initialize( + port=12345, + num_workers=1, + threadless=True, + keyfile='https-key.pem', + certfile='https-signed-cert.pem'), + work_klass=EchoSSLServerHandler) + try: + pool.setup() + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + pool.shutdown() + + +if __name__ == '__main__': + main() diff --git a/examples/tcp_echo_client.py b/examples/tcp_echo_client.py new file mode 100644 index 00000000..decabb50 --- /dev/null +++ b/examples/tcp_echo_client.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from proxy.common.utils import socket_connection +from proxy.common.constants import DEFAULT_BUFFER_SIZE + +if __name__ == '__main__': + with socket_connection(('::', 12345)) as client: + while True: + client.send(b'hello') + data = client.recv(DEFAULT_BUFFER_SIZE) + if data is None: + break + print(data) diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py new file mode 100644 index 00000000..c468b7ea --- /dev/null +++ b/examples/tcp_echo_server.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import time +from typing import Optional + +from proxy.proxy import Proxy +from proxy.core.acceptor import AcceptorPool +from proxy.core.base import BaseTcpServerHandler + + +class EchoServerHandler(BaseTcpServerHandler): + """Sets client socket to non-blocking during initialization.""" + + def initialize(self) -> None: + self.client.connection.setblocking(False) + + def handle_data(self, data: memoryview) -> Optional[bool]: + # echo back to client + self.client.queue(data) + return None + + +def main() -> None: + # This example requires `threadless=True` + pool = AcceptorPool( + flags=Proxy.initialize(port=12345, num_workers=1, threadless=True), + work_klass=EchoServerHandler) + try: + pool.setup() + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + pool.shutdown() + + +if __name__ == '__main__': + main() diff --git a/examples/websocket_client.py b/examples/websocket_client.py new file mode 100644 index 00000000..c87ed3e4 --- /dev/null +++ b/examples/websocket_client.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import time +from proxy.http.websocket import WebsocketClient, WebsocketFrame, websocketOpcodes + + +# globals +client: WebsocketClient +last_dispatch_time: float +static_frame = memoryview(WebsocketFrame.text(b'hello')) +num_echos = 10 + + +def on_message(frame: WebsocketFrame) -> None: + """WebsocketClient on_message callback.""" + global client, num_echos, last_dispatch_time + print('Received %r after %d millisec' % + (frame.data, (time.time() - last_dispatch_time) * 1000)) + assert(frame.data == b'hello' and frame.opcode == + websocketOpcodes.TEXT_FRAME) + if num_echos > 0: + client.queue(static_frame) + last_dispatch_time = time.time() + num_echos -= 1 + else: + client.close() + + +if __name__ == '__main__': + # Constructor establishes socket connection + client = WebsocketClient( + b'echo.websocket.org', + 80, + b'/', + on_message=on_message) + # Perform handshake + client.handshake() + # Queue some data for client + client.queue(static_frame) + last_dispatch_time = time.time() + # Start event loop + client.run() diff --git a/menubar/proxy.py.xcodeproj/project.pbxproj b/menubar/proxy.py.xcodeproj/project.pbxproj index 90e415dc..5c446215 100644 --- a/menubar/proxy.py.xcodeproj/project.pbxproj +++ b/menubar/proxy.py.xcodeproj/project.pbxproj @@ -198,7 +198,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1120; - LastUpgradeCheck = 1120; + LastUpgradeCheck = 1200; ORGANIZATIONNAME = "Abhinav Singh"; TargetAttributes = { AD1F92A2238864240088A917 = { @@ -340,6 +340,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -400,6 +401,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -432,6 +434,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = proxy.py/proxy_py.entitlements; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"proxy.py/Preview Content\""; @@ -455,6 +458,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = proxy.py/proxy_py.entitlements; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"proxy.py/Preview Content\""; diff --git a/menubar/proxy.py.xcodeproj/project.xcworkspace/xcuserdata/abhinavsingh.xcuserdatad/UserInterfaceState.xcuserstate b/menubar/proxy.py.xcodeproj/project.xcworkspace/xcuserdata/abhinavsingh.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..f4e17991b460897ec013f5be4772e272a742d3d1 GIT binary patch literal 26859 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 W4;w39Fepm=Z`G&Wg#(hqn*ImsT0Zdr literal 0 HcmV?d00001 diff --git a/menubar/proxy.py.xcodeproj/xcuserdata/abhinavsingh.xcuserdatad/xcschemes/xcschememanagement.plist b/menubar/proxy.py.xcodeproj/xcuserdata/abhinavsingh.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..f806210a --- /dev/null +++ b/menubar/proxy.py.xcodeproj/xcuserdata/abhinavsingh.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + proxy.py.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/menubar/proxy.py/AppDelegate.swift b/menubar/proxy.py/AppDelegate.swift index f4ac4988..3a1506ff 100644 --- a/menubar/proxy.py/AppDelegate.swift +++ b/menubar/proxy.py/AppDelegate.swift @@ -3,7 +3,8 @@ // proxy.py // // Created by Abhinav Singh on 11/22/19. -// Copyright © 2019 Abhinav Singh. All rights reserved. +// Copyright © 2013-present by Abhinav Singh and contributors. +// All rights reserved. // import Cocoa @@ -13,31 +14,92 @@ import SwiftUI class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! - - let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength) + var statusItem: NSStatusItem! + var preferences: NSPopover! func applicationDidFinishLaunching(_ aNotification: Notification) { + // Create the SwiftUI view that provides the window contents. + let contentView = ContentView() + + self.statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.variableLength) + + self.preferences = NSPopover() + preferences.contentSize = NSSize(width: 400, height: 500) + preferences.behavior = .transient + preferences.contentViewController = NSHostingController(rootView: contentView) + if let button = statusItem.button { - button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) - // button.action = #selector(helloWorld(_:)) + button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) + // button.action = #selector(closePreferences) } constructMenu() } func applicationWillTerminate(_ aNotification: Notification) { // Insert code here to tear down your application - } - - @objc func helloWorld(_ sender: Any?) { - print("Hello World") + print("Tearing down") } func constructMenu() { let menu = NSMenu() + menu.addItem(NSMenuItem(title: "proxy.py is running", action: #selector(AppDelegate.status(_:)), keyEquivalent: "S")) menu.addItem(NSMenuItem.separator()) - menu.addItem(NSMenuItem(title: "About proxy.py", action: #selector(AppDelegate.helloWorld(_:)), keyEquivalent: "A")) + menu.addItem(NSMenuItem(title: "About proxy.py", action: #selector(AppDelegate.about(_:)), keyEquivalent: "A")) menu.addItem(NSMenuItem.separator()) - menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) + menu.addItem(NSMenuItem(title: "Preferences", action: #selector(AppDelegate.preferences(_:)), keyEquivalent: ",")) + menu.addItem(NSMenuItem(title: "Dashboard", action: #selector(AppDelegate.dashboard(_:)), keyEquivalent: "D")) + menu.addItem(NSMenuItem.separator()) + menu.addItem(NSMenuItem(title: "Report a bug", action: #selector(AppDelegate.reportbug(_:)), keyEquivalent: "B")) + menu.addItem(NSMenuItem(title: "Learn", action: #selector(AppDelegate.learn(_:)), keyEquivalent: "L")) + menu.addItem(NSMenuItem.separator()) + menu.addItem(NSMenuItem(title: "Restart", action: #selector(AppDelegate.restart(_:)), keyEquivalent: "R")) + menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "Q")) statusItem.menu = menu } + + @objc func status(_ sender: Any?) { + print("Status clicked") + } + + @objc func about(_ sender: Any?) { + print("About clicked") + } + + @objc func closePreferences(_ sender: Any?) { + if self.statusItem.button != nil { + if self.preferences.isShown { + self.preferences.performClose(sender) + } + } + } + + @objc func preferences(_ sender: Any?) { + print("Preferences clicked") + if let button = self.statusItem.button { + self.preferences.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) + self.preferences.contentViewController?.view.window?.becomeKey() + } + } + + @objc func dashboard(_ sender: Any?) { + print("Dashboard clicked") + } + + @objc func reportbug(_ sender: Any?) { + print("Report bug clicked") + } + + @objc func learn(_ sender: Any?) { + print("Learn clicked") + } + + @objc func restart(_ sender: Any?) { + print("Restart clicked") + } +} + +struct AppDelegate_Previews: PreviewProvider { + static var previews: some View { + /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/ + } } diff --git a/menubar/proxy.py/ContentView.swift b/menubar/proxy.py/ContentView.swift index 52fee636..8cf29f44 100644 --- a/menubar/proxy.py/ContentView.swift +++ b/menubar/proxy.py/ContentView.swift @@ -3,7 +3,8 @@ // proxy.py // // Created by Abhinav Singh on 11/22/19. -// Copyright © 2019 Abhinav Singh. All rights reserved. +// Copyright © 2013-present by Abhinav Singh and contributors. +// All rights reserved. // import SwiftUI diff --git a/menubar/proxy.py/Info.plist b/menubar/proxy.py/Info.plist index 5acca6d2..ada1ded3 100644 --- a/menubar/proxy.py/Info.plist +++ b/menubar/proxy.py/Info.plist @@ -23,7 +23,7 @@ LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copyright © 2019 Abhinav Singh. All rights reserved. + Copyright © 2013-present by Abhinav Singh and contributors. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/proxy/common/constants.py b/proxy/common/constants.py index 1479523a..21e39550 100644 --- a/proxy/common/constants.py +++ b/proxy/common/constants.py @@ -86,3 +86,8 @@ PLUGIN_PAC_FILE = 'proxy.http.server.HttpWebServerPacFilePlugin' PLUGIN_DEVTOOLS_PROTOCOL = 'proxy.http.inspector.DevtoolsProtocolPlugin' PLUGIN_DASHBOARD = 'proxy.dashboard.dashboard.ProxyDashboard' PLUGIN_INSPECT_TRAFFIC = 'proxy.dashboard.inspect_traffic.InspectTrafficPlugin' +PLUGIN_PROXY_AUTH = 'proxy.http.proxy.AuthPlugin' + +PY2_DEPRECATION_MESSAGE = '''DEPRECATION: proxy.py no longer supports Python 2.7. Kindly upgrade to Python 3+. ' + 'If for some reasons you cannot upgrade, use' + '"pip install proxy.py==0.3".''' diff --git a/proxy/common/flag.py b/proxy/common/flag.py new file mode 100644 index 00000000..13ae8e6f --- /dev/null +++ b/proxy/common/flag.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import argparse +from typing import Optional, List, Any + +from .version import __version__ + +__homepage__ = 'https://github.com/abhinavsingh/proxy.py' + + +class FlagParser: + """Wrapper around argparse module. + + proxy.py core and plugin classes must import `flag.flags` and + use `add_argument` to define their own flags within respective + class files. + + Best Practice: + 1. Define flags at the top of your class files. + 2. DO NOT add flags within your class `__init__` method OR + within class methods. It MAY result into runtime exception, + especially if your class is initialized multiple times or if + class method registering the flag gets invoked multiple times. + """ + + def __init__(self) -> None: + self.args: Optional[argparse.Namespace] = None + self.actions: List[str] = [] + self.parser = argparse.ArgumentParser( + description='proxy.py v%s' % __version__, + epilog='Proxy.py not working? Report at: %s/issues/new' % __homepage__ + ) + + def add_argument(self, *args: Any, **kwargs: Any) -> argparse.Action: + """Register a flag.""" + action = self.parser.add_argument(*args, **kwargs) + self.actions.append(action.dest) + return action + + def parse_args( + self, input_args: Optional[List[str]]) -> argparse.Namespace: + """Parse flags from input arguments.""" + self.args = self.parser.parse_args(input_args) + return self.args + + +flags = FlagParser() diff --git a/proxy/common/flags.py b/proxy/common/flags.py deleted file mode 100644 index 182c2c35..00000000 --- a/proxy/common/flags.py +++ /dev/null @@ -1,567 +0,0 @@ -# -*- coding: utf-8 -*- -""" - proxy.py - ~~~~~~~~ - ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on - Network monitoring, controls & Application development, testing, debugging. - - :copyright: (c) 2013-present by Abhinav Singh and contributors. - :license: BSD, see LICENSE for more details. -""" -import abc -import logging -import importlib -import collections -import argparse -import base64 -import ipaddress -import os -import socket -import multiprocessing -import sys -import inspect - -from typing import Optional, Union, Dict, List, TypeVar, Type, cast, Any, Tuple - -from .utils import text_, bytes_ -from .constants import DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_BACKLOG, DEFAULT_BASIC_AUTH -from .constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_DISABLE_HEADERS -from .constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_EVENTS, DEFAULT_ENABLE_DEVTOOLS -from .constants import DEFAULT_ENABLE_WEB_SERVER, DEFAULT_THREADLESS, DEFAULT_CERT_FILE, DEFAULT_KEY_FILE, DEFAULT_CA_FILE -from .constants import DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE -from .constants import DEFAULT_PAC_FILE_URL_PATH, DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT -from .constants import DEFAULT_NUM_WORKERS, DEFAULT_VERSION, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME -from .constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_STATIC_SERVER_DIR -from .constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_DATA_DIRECTORY_PATH, COMMA, DOT -from .constants import PLUGIN_HTTP_PROXY, PLUGIN_WEB_SERVER, PLUGIN_PAC_FILE -from .constants import PLUGIN_DEVTOOLS_PROTOCOL, PLUGIN_DASHBOARD, PLUGIN_INSPECT_TRAFFIC -from .version import __version__ - -__homepage__ = 'https://github.com/abhinavsingh/proxy.py' - -if os.name != 'nt': - import resource - -logger = logging.getLogger(__name__) - -T = TypeVar('T', bound='Flags') - - -class Flags: - """Contains all input flags and inferred input parameters.""" - - def __init__( - self, - auth_code: Optional[bytes] = DEFAULT_BASIC_AUTH, - server_recvbuf_size: int = DEFAULT_SERVER_RECVBUF_SIZE, - client_recvbuf_size: int = DEFAULT_CLIENT_RECVBUF_SIZE, - pac_file: Optional[str] = DEFAULT_PAC_FILE, - pac_file_url_path: Optional[bytes] = DEFAULT_PAC_FILE_URL_PATH, - plugins: Optional[Dict[bytes, List[type]]] = None, - disable_headers: Optional[List[bytes]] = None, - certfile: Optional[str] = None, - keyfile: Optional[str] = None, - ca_cert_dir: Optional[str] = None, - ca_key_file: Optional[str] = None, - ca_cert_file: Optional[str] = None, - ca_signing_key_file: Optional[str] = None, - ca_file: Optional[str] = None, - num_workers: int = 0, - hostname: Union[ipaddress.IPv4Address, - ipaddress.IPv6Address] = DEFAULT_IPV6_HOSTNAME, - port: int = DEFAULT_PORT, - backlog: int = DEFAULT_BACKLOG, - static_server_dir: str = DEFAULT_STATIC_SERVER_DIR, - enable_static_server: bool = DEFAULT_ENABLE_STATIC_SERVER, - devtools_ws_path: bytes = DEFAULT_DEVTOOLS_WS_PATH, - timeout: int = DEFAULT_TIMEOUT, - threadless: bool = DEFAULT_THREADLESS, - enable_events: bool = DEFAULT_ENABLE_EVENTS, - pid_file: Optional[str] = DEFAULT_PID_FILE) -> None: - self.pid_file = pid_file - self.threadless = threadless - self.timeout = timeout - self.auth_code = auth_code - self.server_recvbuf_size = server_recvbuf_size - self.client_recvbuf_size = client_recvbuf_size - self.pac_file = pac_file - self.pac_file_url_path = pac_file_url_path - if plugins is None: - plugins = {} - self.plugins: Dict[bytes, List[type]] = plugins - if disable_headers is None: - disable_headers = DEFAULT_DISABLE_HEADERS - self.disable_headers = disable_headers - self.certfile: Optional[str] = certfile - self.keyfile: Optional[str] = keyfile - self.ca_key_file: Optional[str] = ca_key_file - self.ca_cert_file: Optional[str] = ca_cert_file - self.ca_signing_key_file: Optional[str] = ca_signing_key_file - self.ca_file = ca_file - self.num_workers: int = num_workers if num_workers > 0 else multiprocessing.cpu_count() - self.hostname: Union[ipaddress.IPv4Address, - ipaddress.IPv6Address] = hostname - self.family: socket.AddressFamily = socket.AF_INET6 if hostname.version == 6 else socket.AF_INET - self.port: int = port - self.backlog: int = backlog - - self.enable_static_server: bool = enable_static_server - self.static_server_dir: str = static_server_dir - self.devtools_ws_path: bytes = devtools_ws_path - self.enable_events: bool = enable_events - - self.proxy_py_data_dir = DEFAULT_DATA_DIRECTORY_PATH - os.makedirs(self.proxy_py_data_dir, exist_ok=True) - - self.ca_cert_dir: Optional[str] = ca_cert_dir - if self.ca_cert_dir is None: - self.ca_cert_dir = os.path.join( - self.proxy_py_data_dir, 'certificates') - os.makedirs(self.ca_cert_dir, exist_ok=True) - - def tls_interception_enabled(self) -> bool: - return self.ca_key_file is not None and \ - self.ca_cert_dir is not None and \ - self.ca_signing_key_file is not None and \ - self.ca_cert_file is not None - - def encryption_enabled(self) -> bool: - return self.keyfile is not None and \ - self.certfile is not None - - @classmethod - def initialize( - cls: Type[T], - input_args: Optional[List[str]], - **opts: Any) -> T: - if not Flags.is_py3(): - print( - 'DEPRECATION: "develop" branch no longer supports Python 2.7. Kindly upgrade to Python 3+. ' - 'If for some reasons you cannot upgrade, consider using "master" branch or simply ' - '"pip install proxy.py==0.3".' - '\n\n' - 'DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. ' - 'Please upgrade your Python as Python 2.7 won\'t be maintained after that date. ' - 'A future version of pip will drop support for Python 2.7.') - sys.exit(1) - - parser = Flags.init_parser() - args = parser.parse_args(input_args) - - # Print version and exit - if args.version: - print(__version__) - sys.exit(0) - - # Setup logging module - Flags.setup_logger(args.log_file, args.log_level, args.log_format) - - # Setup limits - Flags.set_open_file_limit(args.open_file_limit) - - default_plugins: List[Tuple[str, bool]] = [] - if args.enable_dashboard: - default_plugins.append((PLUGIN_WEB_SERVER, True)) - args.enable_static_server = True - default_plugins.append((PLUGIN_DASHBOARD, True)) - default_plugins.append((PLUGIN_INSPECT_TRAFFIC, True)) - args.enable_events = True - args.enable_devtools = True - if args.enable_devtools: - default_plugins.append((PLUGIN_DEVTOOLS_PROTOCOL, True)) - default_plugins.append((PLUGIN_WEB_SERVER, True)) - if not args.disable_http_proxy: - default_plugins.append((PLUGIN_HTTP_PROXY, True)) - if args.enable_web_server or \ - args.pac_file is not None or \ - args.enable_static_server: - default_plugins.append((PLUGIN_WEB_SERVER, True)) - if args.pac_file is not None: - default_plugins.append((PLUGIN_PAC_FILE, True)) - - plugins = Flags.load_plugins( - bytes_( - '%s,%s' % - (text_(COMMA).join(collections.OrderedDict(default_plugins).keys()), - opts.get('plugins', args.plugins)))) - - # proxy.py currently cannot serve over HTTPS and perform TLS interception - # at the same time. Check if user is trying to enable both feature - # at the same time. - if (args.cert_file and args.key_file) and \ - (args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file): - print('You can either enable end-to-end encryption OR TLS interception,' - 'not both together.') - sys.exit(1) - - # Generate auth_code required for basic authentication if enabled - auth_code = None - if args.basic_auth: - auth_code = b'Basic %s' % base64.b64encode(bytes_(args.basic_auth)) - - return cls( - auth_code=cast(Optional[bytes], opts.get('auth_code', auth_code)), - server_recvbuf_size=cast( - int, - opts.get( - 'server_recvbuf_size', - args.server_recvbuf_size)), - client_recvbuf_size=cast( - int, - opts.get( - 'client_recvbuf_size', - args.client_recvbuf_size)), - pac_file=cast( - Optional[str], opts.get( - 'pac_file', bytes_( - args.pac_file))), - pac_file_url_path=cast( - Optional[bytes], opts.get( - 'pac_file_url_path', bytes_( - args.pac_file_url_path))), - disable_headers=cast(Optional[List[bytes]], opts.get('disable_headers', [ - header.lower() for header in bytes_( - args.disable_headers).split(COMMA) if header.strip() != b''])), - certfile=cast( - Optional[str], opts.get( - 'cert_file', args.cert_file)), - keyfile=cast(Optional[str], opts.get('key_file', args.key_file)), - ca_cert_dir=cast( - Optional[str], opts.get( - 'ca_cert_dir', args.ca_cert_dir)), - ca_key_file=cast( - Optional[str], opts.get( - 'ca_key_file', args.ca_key_file)), - ca_cert_file=cast( - Optional[str], opts.get( - 'ca_cert_file', args.ca_cert_file)), - ca_signing_key_file=cast( - Optional[str], - opts.get( - 'ca_signing_key_file', - args.ca_signing_key_file)), - ca_file=cast( - Optional[str], - opts.get( - 'ca_file', - args.ca_file)), - hostname=cast(Union[ipaddress.IPv4Address, - ipaddress.IPv6Address], - opts.get('hostname', ipaddress.ip_address(args.hostname))), - port=cast(int, opts.get('port', args.port)), - backlog=cast(int, opts.get('backlog', args.backlog)), - num_workers=cast(int, opts.get('num_workers', args.num_workers)), - static_server_dir=cast( - str, - opts.get( - 'static_server_dir', - args.static_server_dir)), - enable_static_server=cast( - bool, - opts.get( - 'enable_static_server', - args.enable_static_server)), - devtools_ws_path=cast( - bytes, - opts.get( - 'devtools_ws_path', - args.devtools_ws_path)), - timeout=cast(int, opts.get('timeout', args.timeout)), - threadless=cast(bool, opts.get('threadless', args.threadless)), - enable_events=cast( - bool, - opts.get( - 'enable_events', - args.enable_events)), - plugins=plugins, - pid_file=cast(Optional[str], opts.get('pid_file', args.pid_file))) - - @staticmethod - def init_parser() -> argparse.ArgumentParser: - """Initializes and returns argument parser.""" - parser = argparse.ArgumentParser( - description='proxy.py v%s' % __version__, - epilog='Proxy.py not working? Report at: %s/issues/new' % __homepage__ - ) - # Argument names are ordered alphabetically. - parser.add_argument( - '--backlog', - type=int, - default=DEFAULT_BACKLOG, - help='Default: 100. Maximum number of pending connections to proxy server') - parser.add_argument( - '--basic-auth', - type=str, - default=DEFAULT_BASIC_AUTH, - help='Default: No authentication. Specify colon separated user:password ' - 'to enable basic authentication.') - parser.add_argument( - '--ca-key-file', - type=str, - default=DEFAULT_CA_KEY_FILE, - help='Default: None. CA key to use for signing dynamically generated ' - 'HTTPS certificates. If used, must also pass --ca-cert-file and --ca-signing-key-file' - ) - parser.add_argument( - '--ca-cert-dir', - type=str, - default=DEFAULT_CA_CERT_DIR, - help='Default: ~/.proxy.py. Directory to store dynamically generated certificates. ' - 'Also see --ca-key-file, --ca-cert-file and --ca-signing-key-file' - ) - parser.add_argument( - '--ca-cert-file', - type=str, - default=DEFAULT_CA_CERT_FILE, - help='Default: None. Signing certificate to use for signing dynamically generated ' - 'HTTPS certificates. If used, must also pass --ca-key-file and --ca-signing-key-file' - ) - parser.add_argument( - '--ca-file', - type=str, - default=DEFAULT_CA_FILE, - help='Default: None. Provide path to custom CA file for peer certificate validation. ' - 'Specially useful on MacOS.' - ) - parser.add_argument( - '--ca-signing-key-file', - type=str, - default=DEFAULT_CA_SIGNING_KEY_FILE, - help='Default: None. CA signing key to use for dynamic generation of ' - 'HTTPS certificates. If used, must also pass --ca-key-file and --ca-cert-file' - ) - parser.add_argument( - '--cert-file', - type=str, - default=DEFAULT_CERT_FILE, - help='Default: None. Server certificate to enable end-to-end TLS encryption with clients. ' - 'If used, must also pass --key-file.' - ) - parser.add_argument( - '--client-recvbuf-size', - type=int, - default=DEFAULT_CLIENT_RECVBUF_SIZE, - help='Default: 1 MB. Maximum amount of data received from the ' - 'client in a single recv() operation. Bump this ' - 'value for faster uploads at the expense of ' - 'increased RAM.') - parser.add_argument( - '--devtools-ws-path', - type=str, - default=DEFAULT_DEVTOOLS_WS_PATH, - help='Default: /devtools. Only applicable ' - 'if --enable-devtools is used.' - ) - parser.add_argument( - '--disable-headers', - type=str, - default=COMMA.join(DEFAULT_DISABLE_HEADERS), - help='Default: None. Comma separated list of headers to remove before ' - 'dispatching client request to upstream server.') - parser.add_argument( - '--disable-http-proxy', - action='store_true', - default=DEFAULT_DISABLE_HTTP_PROXY, - help='Default: False. Whether to disable proxy.HttpProxyPlugin.') - parser.add_argument( - '--enable-dashboard', - action='store_true', - default=DEFAULT_ENABLE_DASHBOARD, - help='Default: False. Enables proxy.py dashboard.' - ) - parser.add_argument( - '--enable-devtools', - action='store_true', - default=DEFAULT_ENABLE_DEVTOOLS, - help='Default: False. Enables integration with Chrome Devtool Frontend. Also see --devtools-ws-path.' - ) - parser.add_argument( - '--enable-events', - action='store_true', - default=DEFAULT_ENABLE_EVENTS, - help='Default: False. Enables core to dispatch lifecycle events. ' - 'Plugins can be used to subscribe for core events.' - ) - parser.add_argument( - '--enable-static-server', - action='store_true', - default=DEFAULT_ENABLE_STATIC_SERVER, - help='Default: False. Enable inbuilt static file server. ' - 'Optionally, also use --static-server-dir to serve static content ' - 'from custom directory. By default, static file server serves ' - 'out of installed proxy.py python module folder.' - ) - parser.add_argument( - '--enable-web-server', - action='store_true', - default=DEFAULT_ENABLE_WEB_SERVER, - help='Default: False. Whether to enable proxy.HttpWebServerPlugin.') - parser.add_argument( - '--hostname', - type=str, - default=str(DEFAULT_IPV6_HOSTNAME), - help='Default: ::1. Server IP address.') - parser.add_argument( - '--key-file', - type=str, - default=DEFAULT_KEY_FILE, - help='Default: None. Server key file to enable end-to-end TLS encryption with clients. ' - 'If used, must also pass --cert-file.' - ) - parser.add_argument( - '--log-level', - type=str, - default=DEFAULT_LOG_LEVEL, - help='Valid options: DEBUG, INFO (default), WARNING, ERROR, CRITICAL. ' - 'Both upper and lowercase values are allowed. ' - 'You may also simply use the leading character e.g. --log-level d') - parser.add_argument('--log-file', type=str, default=DEFAULT_LOG_FILE, - help='Default: sys.stdout. Log file destination.') - parser.add_argument('--log-format', type=str, default=DEFAULT_LOG_FORMAT, - help='Log format for Python logger.') - parser.add_argument('--num-workers', type=int, default=DEFAULT_NUM_WORKERS, - help='Defaults to number of CPU cores.') - parser.add_argument( - '--open-file-limit', - type=int, - default=DEFAULT_OPEN_FILE_LIMIT, - help='Default: 1024. Maximum number of files (TCP connections) ' - 'that proxy.py can open concurrently.') - parser.add_argument( - '--pac-file', - type=str, - default=DEFAULT_PAC_FILE, - help='A file (Proxy Auto Configuration) or string to serve when ' - 'the server receives a direct file request. ' - 'Using this option enables proxy.HttpWebServerPlugin.') - parser.add_argument( - '--pac-file-url-path', - type=str, - default=text_(DEFAULT_PAC_FILE_URL_PATH), - help='Default: %s. Web server path to serve the PAC file.' % - text_(DEFAULT_PAC_FILE_URL_PATH)) - parser.add_argument( - '--pid-file', - type=str, - default=DEFAULT_PID_FILE, - help='Default: None. Save parent process ID to a file.') - parser.add_argument( - '--plugins', - type=str, - default=DEFAULT_PLUGINS, - help='Comma separated plugins') - parser.add_argument('--port', type=int, default=DEFAULT_PORT, - help='Default: 8899. Server port.') - parser.add_argument( - '--server-recvbuf-size', - type=int, - default=DEFAULT_SERVER_RECVBUF_SIZE, - help='Default: 1 MB. Maximum amount of data received from the ' - 'server in a single recv() operation. Bump this ' - 'value for faster downloads at the expense of ' - 'increased RAM.') - parser.add_argument( - '--static-server-dir', - type=str, - default=DEFAULT_STATIC_SERVER_DIR, - help='Default: "public" folder in directory where proxy.py is placed. ' - 'This option is only applicable when static server is also enabled. ' - 'See --enable-static-server.' - ) - parser.add_argument( - '--threadless', - action='store_true', - default=DEFAULT_THREADLESS, - help='Default: False. When disabled a new thread is spawned ' - 'to handle each client connection.' - ) - parser.add_argument( - '--timeout', - type=int, - default=DEFAULT_TIMEOUT, - help='Default: ' + str(DEFAULT_TIMEOUT) + - '. Number of seconds after which ' - 'an inactive connection must be dropped. Inactivity is defined by no ' - 'data sent or received by the client.' - ) - parser.add_argument( - '--version', - '-v', - action='store_true', - default=DEFAULT_VERSION, - help='Prints proxy.py version.') - return parser - - @staticmethod - def set_open_file_limit(soft_limit: int) -> None: - """Configure open file description soft limit on supported OS.""" - if os.name != 'nt': # resource module not available on Windows OS - curr_soft_limit, curr_hard_limit = resource.getrlimit( - resource.RLIMIT_NOFILE) - if curr_soft_limit < soft_limit < curr_hard_limit: - resource.setrlimit( - resource.RLIMIT_NOFILE, (soft_limit, curr_hard_limit)) - logger.debug( - 'Open file soft limit set to %d', soft_limit) - - @staticmethod - def load_plugins(plugins: bytes) -> Dict[bytes, List[type]]: - """Accepts a comma separated list of Python modules and returns - a list of respective Python classes.""" - p: Dict[bytes, List[type]] = { - b'HttpProtocolHandlerPlugin': [], - b'HttpProxyBasePlugin': [], - b'HttpWebServerBasePlugin': [], - b'ProxyDashboardWebsocketPlugin': [] - } - for plugin_ in plugins.split(COMMA): - plugin = text_(plugin_.strip()) - if plugin == '': - continue - module_name, klass_name = plugin.rsplit(text_(DOT), 1) - klass = getattr( - importlib.import_module( - module_name.replace( - os.path.sep, text_(DOT))), - klass_name) - mro = list(inspect.getmro(klass)) - mro.reverse() - iterator = iter(mro) - while next(iterator) is not abc.ABC: - pass - base_klass = next(iterator) - p[bytes_(base_klass.__name__)].append(klass) - logger.info( - 'Loaded %s %s.%s', - 'plugin' if klass.__name__ != 'HttpWebServerRouteHandler' else 'route', - module_name, - # HttpWebServerRouteHandler route decorator adds a special - # staticmethod to return decorated function name - klass.__name__ if klass.__name__ != 'HttpWebServerRouteHandler' else klass.name()) - return p - - @staticmethod - def setup_logger( - log_file: Optional[str] = DEFAULT_LOG_FILE, - log_level: str = DEFAULT_LOG_LEVEL, - log_format: str = DEFAULT_LOG_FORMAT) -> None: - ll = getattr( - logging, - {'D': 'DEBUG', - 'I': 'INFO', - 'W': 'WARNING', - 'E': 'ERROR', - 'C': 'CRITICAL'}[log_level.upper()[0]]) - if log_file: - logging.basicConfig( - filename=log_file, - filemode='a', - level=ll, - format=log_format) - else: - logging.basicConfig(level=ll, format=log_format) - - @staticmethod - def is_py3() -> bool: - """Exists only to avoid mocking sys.version_info in tests.""" - return sys.version_info[0] == 3 diff --git a/proxy/common/pki.py b/proxy/common/pki.py index 6e6e512e..80711fd4 100644 --- a/proxy/common/pki.py +++ b/proxy/common/pki.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import time import sys import argparse import contextlib @@ -256,6 +257,24 @@ if __name__ == '__main__': default='/CN=example.com', help='Subject to use for public key generation. Default: /CN=example.com', ) + parser.add_argument( + '--csr-path', + type=str, + default=None, + help='CSR file path. Use with gen_csr and sign_csr action.', + ) + parser.add_argument( + '--crt-path', + type=str, + default=None, + help='Signed certificate path. Use with sign_csr action.', + ) + parser.add_argument( + '--hostname', + type=str, + default=None, + help='Alternative subject names to use during CSR signing.', + ) args = parser.parse_args(sys.argv[1:]) # Validation @@ -280,3 +299,12 @@ if __name__ == '__main__': elif args.action == 'remove_passphrase': remove_passphrase(args.private_key_path, args.password, args.private_key_path) + elif args.action == 'gen_csr': + gen_csr( + args.csr_path, + args.private_key_path, + args.password, + args.public_key_path) + elif args.action == 'sign_csr': + sign_csr(args.csr_path, args.crt_path, args.private_key_path, args.password, + args.public_key_path, str(int(time.time())), alt_subj_names=[args.hostname, ]) diff --git a/proxy/common/types.py b/proxy/common/types.py index c4110484..279211e4 100644 --- a/proxy/common/types.py +++ b/proxy/common/types.py @@ -9,8 +9,9 @@ :license: BSD, see LICENSE for more details. """ import queue +import ipaddress -from typing import TYPE_CHECKING, Dict, Any +from typing import TYPE_CHECKING, Dict, Any, List, Union from typing_extensions import Protocol @@ -23,3 +24,8 @@ else: class HasFileno(Protocol): def fileno(self) -> int: ... # pragma: no cover + + +Readables = List[Union[int, HasFileno]] +Writables = List[Union[int, HasFileno]] +IpAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address] diff --git a/proxy/common/utils.py b/proxy/common/utils.py index ecdc4e9f..203a17ae 100644 --- a/proxy/common/utils.py +++ b/proxy/common/utils.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import ssl import contextlib import functools import ipaddress @@ -101,7 +102,8 @@ def build_http_pkt(line: List[bytes], def build_websocket_handshake_request( key: bytes, method: bytes = b'GET', - url: bytes = b'/') -> bytes: + url: bytes = b'/', + host: bytes = b'localhost') -> bytes: """ Build and returns a Websocket handshake request packet. @@ -112,6 +114,7 @@ def build_websocket_handshake_request( return build_http_request( method, url, headers={ + b'Host': host, b'Connection': b'upgrade', b'Upgrade': b'websocket', b'Sec-WebSocket-Key': key, @@ -148,6 +151,21 @@ def find_http_line(raw: bytes) -> Tuple[Optional[bytes], bytes]: return line, rest +def wrap_socket(conn: socket.socket, keyfile: str, + certfile: str) -> ssl.SSLSocket: + ctx = ssl.create_default_context( + ssl.Purpose.CLIENT_AUTH) + ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + ctx.verify_mode = ssl.CERT_NONE + ctx.load_cert_chain( + certfile=certfile, + keyfile=keyfile) + return ctx.wrap_socket( + conn, + server_side=True, + ) + + def new_socket_connection( addr: Tuple[str, int], timeout: int = DEFAULT_TIMEOUT) -> socket.socket: conn = None @@ -193,8 +211,8 @@ class socket_connection(contextlib.ContextDecorator): if self.conn: self.conn.close() - def __call__(self, func: Callable[..., Any] - ) -> Callable[[Tuple[Any, ...], Dict[str, Any]], Any]: + def __call__( # type: ignore + self, func: Callable[..., Any]) -> Callable[[Tuple[Any, ...], Dict[str, Any]], Any]: @functools.wraps(func) def decorated(*args: Any, **kwargs: Any) -> Any: with self as conn: diff --git a/proxy/common/version.py b/proxy/common/version.py index e8af2693..d7dc8e48 100644 --- a/proxy/common/version.py +++ b/proxy/common/version.py @@ -8,5 +8,5 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ -VERSION = (2, 2, 0) +VERSION = (2, 3, 0) __version__ = '.'.join(map(str, VERSION[0:3])) diff --git a/proxy/core/acceptor/__init__.py b/proxy/core/acceptor/__init__.py index 9c0a97b3..cca3bcdb 100644 --- a/proxy/core/acceptor/__init__.py +++ b/proxy/core/acceptor/__init__.py @@ -10,8 +10,12 @@ """ from .acceptor import Acceptor from .pool import AcceptorPool +from .work import Work +from .threadless import Threadless __all__ = [ 'Acceptor', 'AcceptorPool', + 'Work', + 'Threadless', ] diff --git a/proxy/core/acceptor/acceptor.py b/proxy/core/acceptor/acceptor.py index eeb29552..3a9ca618 100644 --- a/proxy/core/acceptor/acceptor.py +++ b/proxy/core/acceptor/acceptor.py @@ -8,38 +8,60 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import logging import multiprocessing import multiprocessing.synchronize import selectors import socket import threading -# import time + from multiprocessing import connection from multiprocessing.reduction import send_handle, recv_handle from typing import Optional, Type, Tuple +from .work import Work +from .threadless import Threadless + from ..connection import TcpClientConnection -from ..threadless import ThreadlessWork, Threadless from ..event import EventQueue, eventNames -from ...common.flags import Flags +from ...common.constants import DEFAULT_THREADLESS +from ...common.flag import flags logger = logging.getLogger(__name__) -class Acceptor(multiprocessing.Process): - """Socket client acceptor. +flags.add_argument( + '--threadless', + action='store_true', + default=DEFAULT_THREADLESS, + help='Default: False. When disabled a new thread is spawned ' + 'to handle each client connection.' +) - Accepts client connection over received server socket handle and - starts a new work thread. + +class Acceptor(multiprocessing.Process): + """Socket server acceptor process. + + Accepts a server socket fd over `work_queue` and start listening for client + connections over the passed server socket. By default, it spawns a separate thread + to handle each client request. + + However, if `--threadless` option is enabled, Acceptor process will also pre-spawns a `Threadless` + process at startup. Accepted client connections are then passed to the `Threadless` process + which internally uses asyncio event loop to handle client connections. + + TODO(abhinavsingh): Instead of starting `Threadless` process, can we work with a `Threadless` thread? + What are the performance implications of sharing fds between threads vs processes? How much performance + degradation happen when processes are running on separate CPU cores? """ def __init__( self, idd: int, work_queue: connection.Connection, - flags: Flags, - work_klass: Type[ThreadlessWork], + flags: argparse.Namespace, + work_klass: Type[Work], lock: multiprocessing.synchronize.Lock, event_queue: Optional[EventQueue] = None) -> None: super().__init__() @@ -108,11 +130,7 @@ class Acceptor(multiprocessing.Process): if len(events) == 0: return conn, addr = self.sock.accept() - - # now = time.time() - # fileno: int = conn.fileno() self.start_work(conn, addr) - # logger.info('Work started for fd %d in %f seconds', fileno, time.time() - now) def run(self) -> None: self.selector = selectors.DefaultSelector() diff --git a/proxy/core/acceptor/pool.py b/proxy/core/acceptor/pool.py index 48cedaf9..65b98c72 100644 --- a/proxy/core/acceptor/pool.py +++ b/proxy/core/acceptor/pool.py @@ -8,34 +8,86 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import logging import multiprocessing import socket import threading -# import time from multiprocessing import connection from multiprocessing.reduction import send_handle from typing import List, Optional, Type from .acceptor import Acceptor -from ..threadless import ThreadlessWork +from .work import Work + from ..event import EventQueue, EventDispatcher -from ...common.flags import Flags +from ...common.flag import flags +from ...common.constants import DEFAULT_BACKLOG, DEFAULT_ENABLE_EVENTS +from ...common.constants import DEFAULT_IPV6_HOSTNAME, DEFAULT_NUM_WORKERS, DEFAULT_PORT logger = logging.getLogger(__name__) +# Lock shared by worker processes LOCK = multiprocessing.Lock() +flags.add_argument( + '--backlog', + type=int, + default=DEFAULT_BACKLOG, + help='Default: 100. Maximum number of pending connections to proxy server') + +flags.add_argument( + '--enable-events', + action='store_true', + default=DEFAULT_ENABLE_EVENTS, + help='Default: False. Enables core to dispatch lifecycle events. ' + 'Plugins can be used to subscribe for core events.' +) + +flags.add_argument( + '--hostname', + type=str, + default=str(DEFAULT_IPV6_HOSTNAME), + help='Default: ::1. Server IP address.') + +flags.add_argument( + '--port', type=int, default=DEFAULT_PORT, + help='Default: 8899. Server port.') + +flags.add_argument( + '--num-workers', + type=int, + default=DEFAULT_NUM_WORKERS, + help='Defaults to number of CPU cores.') + + class AcceptorPool: """AcceptorPool. - Pre-spawns worker processes to utilize all cores available on the system. Server socket connection is - dispatched over a pipe to workers. Each worker accepts incoming client request and spawns a - separate thread to handle the client request. + Pre-spawns worker processes to utilize all cores available on the system. + A server socket is initialized and dispatched over a pipe to these workers. + Each worker process then accepts new client connection. + + Example usage: + + pool = AcceptorPool(flags=..., work_klass=...) + try: + pool.setup() + while True: + time.sleep(1) + finally: + pool.shutdown() + + `work_klass` must implement `work.Work` class. + + Optionally, AcceptorPool also initialize a global event queue. + It is a multiprocess safe queue which can be used to build pubsub patterns + for message sharing or signaling within proxy.py. """ - def __init__(self, flags: Flags, work_klass: Type[ThreadlessWork]) -> None: + def __init__(self, flags: argparse.Namespace, + work_klass: Type[Work]) -> None: self.flags = flags self.socket: Optional[socket.socket] = None self.acceptors: List[Acceptor] = [] diff --git a/proxy/core/threadless.py b/proxy/core/acceptor/threadless.py similarity index 69% rename from proxy/core/threadless.py rename to proxy/core/acceptor/threadless.py index 87be7e5b..87ef4aba 100644 --- a/proxy/core/threadless.py +++ b/proxy/core/acceptor/threadless.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import os import socket import logging @@ -18,83 +19,19 @@ import multiprocessing from multiprocessing import connection from multiprocessing.reduction import recv_handle -from abc import ABC, abstractmethod -from typing import Dict, Optional, Tuple, List, Union, Generator, Any, Type -from uuid import uuid4, UUID +from typing import Dict, Optional, Tuple, List, Generator, Any, Type -from .connection import TcpClientConnection -from .event import EventQueue, eventNames +from .work import Work -from ..common.flags import Flags -from ..common.types import HasFileno -from ..common.constants import DEFAULT_TIMEOUT +from ..connection import TcpClientConnection +from ..event import EventQueue, eventNames + +from ...common.types import Readables, Writables +from ...common.constants import DEFAULT_TIMEOUT logger = logging.getLogger(__name__) -class ThreadlessWork(ABC): - """Implement ThreadlessWork to hook into the event loop provided by Threadless process.""" - - @abstractmethod - def __init__( - self, - client: TcpClientConnection, - flags: Optional[Flags], - event_queue: Optional[EventQueue] = None, - uid: Optional[UUID] = None) -> None: - self.client = client - self.flags = flags if flags else Flags() - self.event_queue = event_queue - self.uid: UUID = uid if uid is not None else uuid4() - - @abstractmethod - def initialize(self) -> None: - pass # pragma: no cover - - @abstractmethod - def is_inactive(self) -> bool: - return False # pragma: no cover - - @abstractmethod - def get_events(self) -> Dict[socket.socket, int]: - return {} # pragma: no cover - - @abstractmethod - def handle_events( - self, - readables: List[Union[int, HasFileno]], - writables: List[Union[int, HasFileno]]) -> bool: - """Return True to shutdown work.""" - return False # pragma: no cover - - @abstractmethod - def run(self) -> None: - pass - - def publish_event( - self, - event_name: int, - event_payload: Dict[str, Any], - publisher_id: Optional[str] = None) -> None: - if not self.flags.enable_events: - return - assert self.event_queue - self.event_queue.publish( - self.uid.hex, - event_name, - event_payload, - publisher_id - ) - - def shutdown(self) -> None: - """Must close any opened resources and call super().shutdown().""" - self.publish_event( - event_name=eventNames.WORK_FINISHED, - event_payload={}, - publisher_id=self.__class__.__name__ - ) - - class Threadless(multiprocessing.Process): """Threadless provides an event loop. Use it by implementing Threadless class. @@ -103,15 +40,15 @@ class Threadless(multiprocessing.Process): for each accepted client connection, Acceptor process sends accepted client connection to Threadless process over a pipe. - HttpProtocolHandler implements ThreadlessWork class and hooks into the - event loop provided by Threadless. + Example, HttpProtocolHandler implements Work class to hooks into the + event loop provided by Threadless process. """ def __init__( self, client_queue: connection.Connection, - flags: Flags, - work_klass: Type[ThreadlessWork], + flags: argparse.Namespace, + work_klass: Type[Work], event_queue: Optional[EventQueue] = None) -> None: super().__init__() self.client_queue = client_queue @@ -120,13 +57,12 @@ class Threadless(multiprocessing.Process): self.event_queue = event_queue self.running = multiprocessing.Event() - self.works: Dict[int, ThreadlessWork] = {} + self.works: Dict[int, Work] = {} self.selector: Optional[selectors.DefaultSelector] = None self.loop: Optional[asyncio.AbstractEventLoop] = None @contextlib.contextmanager - def selected_events(self) -> Generator[Tuple[List[Union[int, HasFileno]], - List[Union[int, HasFileno]]], + def selected_events(self) -> Generator[Tuple[Readables, Writables], None, None]: events: Dict[socket.socket, int] = {} for work in self.works.values(): @@ -148,8 +84,8 @@ class Threadless(multiprocessing.Process): async def handle_events( self, fileno: int, - readables: List[Union[int, HasFileno]], - writables: List[Union[int, HasFileno]]) -> bool: + readables: Readables, + writables: Writables) -> bool: return self.works[fileno].handle_events(readables, writables) # TODO: Use correct future typing annotations diff --git a/proxy/core/acceptor/work.py b/proxy/core/acceptor/work.py new file mode 100644 index 00000000..6bf3880e --- /dev/null +++ b/proxy/core/acceptor/work.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import argparse +import socket + +from abc import ABC, abstractmethod +from uuid import uuid4, UUID +from typing import Optional, Dict, Any + +from ..event import eventNames, EventQueue +from ..connection import TcpClientConnection +from ...common.types import Readables, Writables + + +class Work(ABC): + """Implement Work to hook into the event loop provided by Threadless process.""" + + def __init__( + self, + client: TcpClientConnection, + flags: argparse.Namespace, + event_queue: Optional[EventQueue] = None, + uid: Optional[UUID] = None) -> None: + self.client = client + self.flags = flags + self.event_queue = event_queue + self.uid: UUID = uid if uid is not None else uuid4() + + @abstractmethod + def get_events(self) -> Dict[socket.socket, int]: + """Return sockets and events (read or write) that we are interested in.""" + return {} # pragma: no cover + + @abstractmethod + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + """Handle readable and writable sockets. + + Return True to shutdown work.""" + return False # pragma: no cover + + def initialize(self) -> None: + """Perform any resource initialization.""" + pass # pragma: no cover + + def is_inactive(self) -> bool: + """Return True if connection should be considered inactive.""" + return False # pragma: no cover + + def shutdown(self) -> None: + """Implementation must close any opened resources here + and call super().shutdown().""" + self.publish_event( + event_name=eventNames.WORK_FINISHED, + event_payload={}, + publisher_id=self.__class__.__name__ + ) + + def run(self) -> None: + """run() method is not used by Threadless. It's here for backward + compatibility with threaded mode where work class is started as + a separate thread. + """ + pass + + def publish_event( + self, + event_name: int, + event_payload: Dict[str, Any], + publisher_id: Optional[str] = None) -> None: + """Convenience method provided to publish events into the global event queue.""" + if not self.flags.enable_events: + return + assert self.event_queue + self.event_queue.publish( + self.uid.hex, + event_name, + event_payload, + publisher_id + ) diff --git a/proxy/core/base/__init__.py b/proxy/core/base/__init__.py new file mode 100644 index 00000000..c60f1477 --- /dev/null +++ b/proxy/core/base/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from .tcp_server import BaseTcpServerHandler +from .tcp_tunnel import BaseTcpTunnelHandler + +__all__ = [ + 'BaseTcpServerHandler', + 'BaseTcpTunnelHandler', +] diff --git a/proxy/core/base/tcp_server.py b/proxy/core/base/tcp_server.py new file mode 100644 index 00000000..bff04311 --- /dev/null +++ b/proxy/core/base/tcp_server.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from abc import abstractmethod +import socket +import selectors + +from typing import Dict, Any, Optional + +from proxy.core.acceptor import Work +from proxy.common.types import Readables, Writables + + +class BaseTcpServerHandler(Work): + """BaseTcpServerHandler implements Work interface. + + An instance of BaseTcpServerHandler is created for each client + connection. BaseServerHandler lifecycle is controlled by + Threadless core using asyncio. + + BaseServerHandler ensures that pending buffers are flushed + before client connection is closed. + + Implementations must provide: + a) handle_data(data: memoryview) + c) (optionally) intialize, is_inactive and shutdown methods + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.must_flush_before_shutdown = False + print('Connection accepted from {0}'.format(self.client.addr)) + + @abstractmethod + def handle_data(self, data: memoryview) -> Optional[bool]: + """Optionally return True to close client connection.""" + pass # pragma: no cover + + def get_events(self) -> Dict[socket.socket, int]: + events = {} + # We always want to read from client + # Register for EVENT_READ events + if self.must_flush_before_shutdown is False: + events[self.client.connection] = selectors.EVENT_READ + # If there is pending buffer for client + # also register for EVENT_WRITE events + if self.client.has_buffer(): + if self.client.connection in events: + events[self.client.connection] |= selectors.EVENT_WRITE + else: + events[self.client.connection] = selectors.EVENT_WRITE + return events + + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + """Return True to shutdown work.""" + do_shutdown = False + if self.client.connection in readables: + try: + data = self.client.recv() + if data is None: + # Client closed connection, signal shutdown + print( + 'Connection closed by client {0}'.format( + self.client.addr)) + do_shutdown = True + else: + r = self.handle_data(data) + if isinstance(r, bool) and r is True: + print( + 'Implementation signaled shutdown for client {0}'.format( + self.client.addr)) + if self.client.has_buffer(): + print( + 'Client {0} has pending buffer, will be flushed before shutting down'.format( + self.client.addr)) + self.must_flush_before_shutdown = True + else: + do_shutdown = True + except ConnectionResetError: + print( + 'Connection reset by client {0}'.format( + self.client.addr)) + do_shutdown = True + + if self.client.connection in writables: + print('Flushing buffer to client {0}'.format(self.client.addr)) + self.client.flush() + if self.must_flush_before_shutdown is True: + do_shutdown = True + self.must_flush_before_shutdown = False + + if do_shutdown: + print( + 'Shutting down client {0} connection'.format( + self.client.addr)) + return do_shutdown diff --git a/proxy/core/base/tcp_tunnel.py b/proxy/core/base/tcp_tunnel.py new file mode 100644 index 00000000..d83053ae --- /dev/null +++ b/proxy/core/base/tcp_tunnel.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from abc import abstractmethod +import socket +import selectors +from typing import Any, Optional, Dict + +from ...http.parser import HttpParser, httpParserTypes +from ...common.types import Readables, Writables +from ...common.utils import text_ + +from ..connection import TcpServerConnection +from .tcp_server import BaseTcpServerHandler + + +class BaseTcpTunnelHandler(BaseTcpServerHandler): + """Base TCP tunnel interface.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.request = HttpParser(httpParserTypes.REQUEST_PARSER) + self.upstream: Optional[TcpServerConnection] = None + + @abstractmethod + def handle_data(self, data: memoryview) -> Optional[bool]: + pass # pragma: no cover + + def initialize(self) -> None: + self.client.connection.setblocking(False) + + def shutdown(self) -> None: + if self.upstream: + print('Connection closed with upstream {0}:{1}'.format( + text_(self.request.host), self.request.port)) + self.upstream.close() + super().shutdown() + + def get_events(self) -> Dict[socket.socket, int]: + # Get default client events + ev: Dict[socket.socket, int] = super().get_events() + # Read from server if we are connected + if self.upstream and self.upstream._conn is not None: + ev[self.upstream.connection] = selectors.EVENT_READ + # If there is pending buffer for server + # also register for EVENT_WRITE events + if self.upstream and self.upstream.has_buffer(): + if self.upstream.connection in ev: + ev[self.upstream.connection] |= selectors.EVENT_WRITE + else: + ev[self.upstream.connection] = selectors.EVENT_WRITE + return ev + + def handle_events( + self, + readables: Readables, + writables: Writables) -> bool: + # Handle client events + do_shutdown: bool = super().handle_events(readables, writables) + if do_shutdown: + return do_shutdown + # Handle server events + if self.upstream and self.upstream.connection in readables: + data = self.upstream.recv() + if data is None: + # Server closed connection + print('Connection closed by server') + return True + # tunnel data to client + self.client.queue(data) + if self.upstream and self.upstream.connection in writables: + self.upstream.flush() + return False + + def connect_upstream(self) -> None: + assert self.request.host and self.request.port + self.upstream = TcpServerConnection( + text_(self.request.host), self.request.port) + self.upstream.connect() + print('Connection established with upstream {0}:{1}'.format( + text_(self.request.host), self.request.port)) diff --git a/proxy/core/connection/client.py b/proxy/core/connection/client.py index 28995a58..62597a10 100644 --- a/proxy/core/connection/client.py +++ b/proxy/core/connection/client.py @@ -30,3 +30,15 @@ class TcpClientConnection(TcpConnection): if self._conn is None: raise TcpConnectionUninitializedException() return self._conn + + def wrap(self, keyfile: str, certfile: str) -> None: + self.connection.setblocking(True) + self.flush() + self._conn = ssl.wrap_socket( + self.connection, + server_side=True, + # ca_certs=self.flags.ca_cert_file, + certfile=certfile, + keyfile=keyfile, + ssl_version=ssl.PROTOCOL_TLS) + self.connection.setblocking(False) diff --git a/proxy/core/connection/connection.py b/proxy/core/connection/connection.py index 3aa72eeb..73eabe20 100644 --- a/proxy/core/connection/connection.py +++ b/proxy/core/connection/connection.py @@ -88,5 +88,6 @@ class TcpConnection(ABC): self.buffer.pop(0) else: self.buffer[0] = memoryview(mv[sent:]) + del mv logger.debug('flushed %d bytes to %s' % (sent, self.tag)) return sent diff --git a/proxy/core/connection/server.py b/proxy/core/connection/server.py index cbb9806a..c5636e6a 100644 --- a/proxy/core/connection/server.py +++ b/proxy/core/connection/server.py @@ -8,8 +8,8 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ -import socket import ssl +import socket from typing import Optional, Union, Tuple from .connection import TcpConnection, tcpConnectionTypes, TcpConnectionUninitializedException @@ -34,3 +34,14 @@ class TcpServerConnection(TcpConnection): if self._conn is not None: return self._conn = new_socket_connection(self.addr) + + def wrap(self, hostname: str, ca_file: Optional[str]) -> None: + ctx = ssl.create_default_context( + ssl.Purpose.SERVER_AUTH, cafile=ca_file) + ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 + ctx.check_hostname = True + self.connection.setblocking(True) + self._conn = ctx.wrap_socket( + self.connection, + server_hostname=hostname) + self.connection.setblocking(False) diff --git a/proxy/core/event/dispatcher.py b/proxy/core/event/dispatcher.py index f6bb849e..fb7c5275 100644 --- a/proxy/core/event/dispatcher.py +++ b/proxy/core/event/dispatcher.py @@ -38,7 +38,7 @@ class EventDispatcher: event at a time. When --enable-events is used, a multiprocessing.Queue is created and - attached to global Flags. This queue can then be used for + attached to global argparse. This queue can then be used for dispatching an Event dict object into the queue. When --enable-events is used, dispatcher module is automatically @@ -83,6 +83,8 @@ class EventDispatcher: self.run_once() except queue.Empty: pass + except BrokenPipeError: + pass except EOFError: pass except KeyboardInterrupt: diff --git a/proxy/core/event/subscriber.py b/proxy/core/event/subscriber.py index ec6afe62..90648e0d 100644 --- a/proxy/core/event/subscriber.py +++ b/proxy/core/event/subscriber.py @@ -57,7 +57,13 @@ class EventSubscriber: assert self.relay_channel assert self.relay_sub_id - self.event_queue.unsubscribe(self.relay_sub_id) + try: + self.event_queue.unsubscribe(self.relay_sub_id) + except BrokenPipeError: + pass + except EOFError: + pass + self.relay_shutdown.set() self.relay_thread.join() logger.debug( diff --git a/proxy/dashboard/plugin.py b/proxy/dashboard/plugin.py index 7c5b9a3b..b3787ac2 100644 --- a/proxy/dashboard/plugin.py +++ b/proxy/dashboard/plugin.py @@ -8,12 +8,12 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import json from abc import ABC, abstractmethod from typing import List, Dict, Any from ..common.utils import bytes_ -from ..common.flags import Flags from ..http.websocket import WebsocketFrame from ..core.connection import TcpClientConnection from ..core.event import EventQueue @@ -24,7 +24,7 @@ class ProxyDashboardWebsocketPlugin(ABC): def __init__( self, - flags: Flags, + flags: argparse.Namespace, client: TcpClientConnection, event_queue: EventQueue) -> None: self.flags = flags diff --git a/proxy/http/handler.py b/proxy/http/handler.py index 9c7dd90c..474ef622 100644 --- a/proxy/http/handler.py +++ b/proxy/http/handler.py @@ -8,6 +8,7 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import argparse import socket import selectors import ssl @@ -15,106 +16,60 @@ import time import contextlib import errno import logging -from abc import ABC, abstractmethod + from typing import Tuple, List, Union, Optional, Generator, Dict from uuid import UUID + +from .plugin import HttpProtocolHandlerPlugin from .parser import HttpParser, httpParserStates, httpParserTypes from .exception import HttpProtocolException -from ..common.flags import Flags -from ..common.types import HasFileno -from ..core.threadless import ThreadlessWork +from ..common.types import Readables, Writables +from ..common.utils import wrap_socket +from ..core.acceptor.work import Work from ..core.event import EventQueue from ..core.connection import TcpClientConnection +from ..common.flag import flags +from ..common.constants import DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_KEY_FILE, DEFAULT_TIMEOUT + logger = logging.getLogger(__name__) -class HttpProtocolHandlerPlugin(ABC): - """Base HttpProtocolHandler Plugin class. - - NOTE: This is an internal plugin and in most cases only useful for core contributors. - If you are looking for proxy server plugins see ``. - - Implements various lifecycle events for an accepted client connection. - Following events are of interest: - - 1. Client Connection Accepted - A new plugin instance is created per accepted client connection. - Add your logic within __init__ constructor for any per connection setup. - 2. Client Request Chunk Received - on_client_data is called for every chunk of data sent by the client. - 3. Client Request Complete - on_request_complete is called once client request has completed. - 4. Server Response Chunk Received - on_response_chunk is called for every chunk received from the server. - 5. Client Connection Closed - Add your logic within `on_client_connection_close` for any per connection teardown. - """ - - def __init__( - self, - uid: UUID, - flags: Flags, - client: TcpClientConnection, - request: HttpParser, - event_queue: EventQueue): - self.uid: UUID = uid - self.flags: Flags = flags - self.client: TcpClientConnection = client - self.request: HttpParser = request - self.event_queue = event_queue - super().__init__() - - def name(self) -> str: - """A unique name for your plugin. - - Defaults to name of the class. This helps plugin developers to directly - access a specific plugin by its name.""" - return self.__class__.__name__ - - @abstractmethod - def get_descriptors( - self) -> Tuple[List[socket.socket], List[socket.socket]]: - return [], [] # pragma: no cover - - @abstractmethod - def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool: - return False # pragma: no cover - - @abstractmethod - def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool: - return False # pragma: no cover - - @abstractmethod - def on_client_data(self, raw: memoryview) -> Optional[memoryview]: - return raw # pragma: no cover - - @abstractmethod - def on_request_complete(self) -> Union[socket.socket, bool]: - """Called right after client request parser has reached COMPLETE state.""" - return False # pragma: no cover - - @abstractmethod - def on_response_chunk(self, chunk: List[memoryview]) -> List[memoryview]: - """Handle data chunks as received from the server. - - Return optionally modified chunk to return back to client.""" - return chunk # pragma: no cover - - @abstractmethod - def on_client_connection_close(self) -> None: - pass # pragma: no cover +flags.add_argument( + '--client-recvbuf-size', + type=int, + default=DEFAULT_CLIENT_RECVBUF_SIZE, + help='Default: 1 MB. Maximum amount of data received from the ' + 'client in a single recv() operation. Bump this ' + 'value for faster uploads at the expense of ' + 'increased RAM.') +flags.add_argument( + '--key-file', + type=str, + default=DEFAULT_KEY_FILE, + help='Default: None. Server key file to enable end-to-end TLS encryption with clients. ' + 'If used, must also pass --cert-file.' +) +flags.add_argument( + '--timeout', + type=int, + default=DEFAULT_TIMEOUT, + help='Default: ' + str(DEFAULT_TIMEOUT) + + '. Number of seconds after which ' + 'an inactive connection must be dropped. Inactivity is defined by no ' + 'data sent or received by the client.' +) -class HttpProtocolHandler(ThreadlessWork): +class HttpProtocolHandler(Work): """HTTP, HTTPS, HTTP2, WebSockets protocol handler. Accepts `Client` connection object and manages HttpProtocolHandlerPlugin invocations. """ def __init__(self, client: TcpClientConnection, - flags: Optional[Flags] = None, + flags: argparse.Namespace, event_queue: Optional[EventQueue] = None, uid: Optional[UUID] = None): super().__init__(client, flags, event_queue, uid) @@ -127,11 +82,15 @@ class HttpProtocolHandler(ThreadlessWork): self.client: TcpClientConnection = client self.plugins: Dict[str, HttpProtocolHandlerPlugin] = {} + def encryption_enabled(self) -> bool: + return self.flags.keyfile is not None and \ + self.flags.certfile is not None + def initialize(self) -> None: """Optionally upgrades connection to HTTPS, set conn in non-blocking mode and initializes plugins.""" conn = self.optionally_wrap_socket(self.client.connection) conn.setblocking(False) - if self.flags.encryption_enabled(): + if self.encryption_enabled(): self.client = TcpClientConnection(conn=conn, addr=self.client.addr) if b'HttpProtocolHandlerPlugin' in self.flags.plugins: for klass in self.flags.plugins[b'HttpProtocolHandlerPlugin']: @@ -175,8 +134,8 @@ class HttpProtocolHandler(ThreadlessWork): def handle_events( self, - readables: List[Union[int, HasFileno]], - writables: List[Union[int, HasFileno]]) -> bool: + readables: Readables, + writables: Writables) -> bool: """Returns True if proxy must teardown.""" # Flush buffer for ready to write sockets teardown = self.handle_writables(writables) @@ -218,7 +177,7 @@ class HttpProtocolHandler(ThreadlessWork): conn = self.client.connection # Unwrap if wrapped before shutdown. - if self.flags.encryption_enabled() and \ + if self.encryption_enabled() and \ isinstance(self.client.connection, ssl.SSLSocket): conn = self.client.connection.unwrap() conn.shutdown(socket.SHUT_WR) @@ -236,19 +195,9 @@ class HttpProtocolHandler(ThreadlessWork): Shutdown and closes client connection upon error. """ - if self.flags.encryption_enabled(): - ctx = ssl.create_default_context( - ssl.Purpose.CLIENT_AUTH) - ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 - ctx.verify_mode = ssl.CERT_NONE + if self.encryption_enabled(): assert self.flags.keyfile and self.flags.certfile - ctx.load_cert_chain( - certfile=self.flags.certfile, - keyfile=self.flags.keyfile) - conn = ctx.wrap_socket( - conn, - server_side=True, - ) + conn = wrap_socket(conn, self.flags.keyfile, self.flags.certfile) return conn def connection_inactive_for(self) -> float: @@ -272,7 +221,7 @@ class HttpProtocolHandler(ThreadlessWork): finally: self.selector.unregister(self.client.connection) - def handle_writables(self, writables: List[Union[int, HasFileno]]) -> bool: + def handle_writables(self, writables: Writables) -> bool: if self.client.has_buffer() and self.client.connection in writables: logger.debug('Client is ready for writes, flushing buffer') self.last_activity = time.time() @@ -297,7 +246,7 @@ class HttpProtocolHandler(ThreadlessWork): return True return False - def handle_readables(self, readables: List[Union[int, HasFileno]]) -> bool: + def handle_readables(self, readables: Readables) -> bool: if self.client.connection in readables: logger.debug('Client is ready for reads, reading') self.last_activity = time.time() @@ -367,8 +316,7 @@ class HttpProtocolHandler(ThreadlessWork): @contextlib.contextmanager def selected_events(self) -> \ - Generator[Tuple[List[Union[int, HasFileno]], - List[Union[int, HasFileno]]], + Generator[Tuple[Readables, Writables], None, None]: events = self.get_events() for fd in events: diff --git a/proxy/http/inspector/devtools.py b/proxy/http/inspector/devtools.py index 2e9a983b..50b34192 100644 --- a/proxy/http/inspector/devtools.py +++ b/proxy/http/inspector/devtools.py @@ -19,10 +19,21 @@ from ..server import HttpWebServerBasePlugin, httpProtocolTypes from ...common.utils import bytes_, text_ from ...core.event import EventSubscriber +from ...common.flag import flags +from ...common.constants import DEFAULT_DEVTOOLS_WS_PATH logger = logging.getLogger(__name__) +flags.add_argument( + '--devtools-ws-path', + type=str, + default=DEFAULT_DEVTOOLS_WS_PATH, + help='Default: /devtools. Only applicable ' + 'if --enable-devtools is used.' +) + + class DevtoolsProtocolPlugin(HttpWebServerBasePlugin): """Speaks DevTools protocol with client over websocket. diff --git a/proxy/http/inspector/transformer.py b/proxy/http/inspector/transformer.py index 32d6b65e..450ce7bb 100644 --- a/proxy/http/inspector/transformer.py +++ b/proxy/http/inspector/transformer.py @@ -61,8 +61,10 @@ class CoreEventsToDevtoolsProtocol: 'wallTime': now, 'hasUserGesture': False, 'type': event['event_payload']['headers']['content-type'] - if event['event_payload']['headers'].has_header('content-type') + if 'content-type' in event['event_payload']['headers'] else 'Other', + # TODO(abhinavsingh): Bring this inline with devtools protocol + 'method': 'Network.requestWillBeSent', 'request': { 'url': event['event_payload']['url'], 'method': event['event_payload']['method'], diff --git a/proxy/http/parser.py b/proxy/http/parser.py index 63c62b8b..638959ac 100644 --- a/proxy/http/parser.py +++ b/proxy/http/parser.py @@ -15,7 +15,7 @@ from .methods import httpMethods from .chunk_parser import ChunkParser, chunkParserStates from ..common.constants import DEFAULT_DISABLE_HEADERS, COLON, CRLF, WHITESPACE, HTTP_1_1, DEFAULT_HTTP_PORT -from ..common.utils import build_http_request, find_http_line, text_ +from ..common.utils import build_http_request, build_http_response, find_http_line, text_ HttpParserStates = NamedTuple('HttpParserStates', [ @@ -110,7 +110,7 @@ class HttpParser: # For CONNECT requests, request line contains # upstream_host:upstream_port which is not complaint # with urlsplit, which expects a fully qualified url. - if self.method == b'CONNECT': + if self.method == httpMethods.CONNECT: url = b'https://' + url self.url = urlparse.urlsplit(url) self.set_line_attributes() @@ -171,7 +171,8 @@ class HttpParser: self.state = httpParserStates.COMPLETE more = False else: - raise NotImplementedError('Parser shouldn\'t have reached here') + raise NotImplementedError( + 'Parser shouldn\'t have reached here') else: more, raw = self.process(raw) self.buffer = raw @@ -237,7 +238,8 @@ class HttpParser: return url def build(self, disable_headers: Optional[List[bytes]] = None) -> bytes: - assert self.method and self.version and self.path + """Rebuild the request object.""" + assert self.method and self.version and self.path and self.type == httpParserTypes.REQUEST_PARSER if disable_headers is None: disable_headers = DEFAULT_DISABLE_HEADERS body: Optional[bytes] = ChunkParser.to_chunks(self.body) \ @@ -250,6 +252,17 @@ class HttpParser: body=body ) + def build_response(self) -> bytes: + """Rebuild the response object.""" + assert self.code and self.version and self.body and self.type == httpParserTypes.RESPONSE_PARSER + return build_http_response( + status_code=int(self.code), + protocol_version=self.version, + reason=self.reason, + headers={} if not self.headers else { + self.headers[k][0]: self.headers[k][1] for k in self.headers}, + body=self.body if not self.is_chunked_encoded() else ChunkParser.to_chunks(self.body)) + def has_upstream_server(self) -> bool: """Host field SHOULD be None for incoming local WebServer requests.""" return True if self.host is not None else False diff --git a/proxy/http/plugin.py b/proxy/http/plugin.py new file mode 100644 index 00000000..2e7fed45 --- /dev/null +++ b/proxy/http/plugin.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import argparse +import socket + +from abc import ABC, abstractmethod +from uuid import UUID +from typing import Tuple, List, Union, Optional + +from .parser import HttpParser + +from ..common.types import Readables, Writables +from ..core.event import EventQueue +from ..core.connection import TcpClientConnection + + +class HttpProtocolHandlerPlugin(ABC): + """Base HttpProtocolHandler Plugin class. + + NOTE: This is an internal plugin and in most cases only useful for core contributors. + If you are looking for proxy server plugins see ``. + + Implements various lifecycle events for an accepted client connection. + Following events are of interest: + + 1. Client Connection Accepted + A new plugin instance is created per accepted client connection. + Add your logic within __init__ constructor for any per connection setup. + 2. Client Request Chunk Received + on_client_data is called for every chunk of data sent by the client. + 3. Client Request Complete + on_request_complete is called once client request has completed. + 4. Server Response Chunk Received + on_response_chunk is called for every chunk received from the server. + 5. Client Connection Closed + Add your logic within `on_client_connection_close` for any per connection teardown. + """ + + def __init__( + self, + uid: UUID, + flags: argparse.Namespace, + client: TcpClientConnection, + request: HttpParser, + event_queue: EventQueue): + self.uid: UUID = uid + self.flags: argparse.Namespace = flags + self.client: TcpClientConnection = client + self.request: HttpParser = request + self.event_queue = event_queue + super().__init__() + + def name(self) -> str: + """A unique name for your plugin. + + Defaults to name of the class. This helps plugin developers to directly + access a specific plugin by its name.""" + return self.__class__.__name__ + + @abstractmethod + def get_descriptors( + self) -> Tuple[List[socket.socket], List[socket.socket]]: + return [], [] # pragma: no cover + + @abstractmethod + def write_to_descriptors(self, w: Writables) -> bool: + return False # pragma: no cover + + @abstractmethod + def read_from_descriptors(self, r: Readables) -> bool: + return False # pragma: no cover + + @abstractmethod + def on_client_data(self, raw: memoryview) -> Optional[memoryview]: + return raw # pragma: no cover + + @abstractmethod + def on_request_complete(self) -> Union[socket.socket, bool]: + """Called right after client request parser has reached COMPLETE state.""" + return False # pragma: no cover + + @abstractmethod + def on_response_chunk(self, chunk: List[memoryview]) -> List[memoryview]: + """Handle data chunks as received from the server. + + Return optionally modified chunk to return back to client.""" + return chunk # pragma: no cover + + @abstractmethod + def on_client_connection_close(self) -> None: + pass # pragma: no cover diff --git a/proxy/http/proxy/__init__.py b/proxy/http/proxy/__init__.py index afd35271..4e18002c 100644 --- a/proxy/http/proxy/__init__.py +++ b/proxy/http/proxy/__init__.py @@ -10,8 +10,10 @@ """ from .plugin import HttpProxyBasePlugin from .server import HttpProxyPlugin +from .auth import AuthPlugin __all__ = [ 'HttpProxyBasePlugin', 'HttpProxyPlugin', + 'AuthPlugin', ] diff --git a/proxy/http/proxy/auth.py b/proxy/http/proxy/auth.py new file mode 100644 index 00000000..263ec9bd --- /dev/null +++ b/proxy/http/proxy/auth.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from typing import Optional + +from ..exception import ProxyAuthenticationFailed +from ...common.flag import flags +from ...common.constants import DEFAULT_BASIC_AUTH +from ...http.parser import HttpParser +from ...http.proxy import HttpProxyBasePlugin + + +flags.add_argument( + '--basic-auth', + type=str, + default=DEFAULT_BASIC_AUTH, + help='Default: No authentication. Specify colon separated user:password ' + 'to enable basic authentication.') + + +class AuthPlugin(HttpProxyBasePlugin): + """Performs proxy authentication.""" + + def before_upstream_connection( + self, request: HttpParser) -> Optional[HttpParser]: + if self.flags.auth_code: + if b'proxy-authorization' not in request.headers: + raise ProxyAuthenticationFailed() + parts = request.headers[b'proxy-authorization'][1].split() + if len(parts) != 2 \ + and parts[0].lower() != b'basic' \ + and parts[1] != self.flags.auth_code: + raise ProxyAuthenticationFailed() + return request + + def handle_client_request( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + return chunk + + def on_upstream_connection_close(self) -> None: + pass diff --git a/proxy/http/proxy/plugin.py b/proxy/http/proxy/plugin.py index 44129ed8..7e5c1b0b 100644 --- a/proxy/http/proxy/plugin.py +++ b/proxy/http/proxy/plugin.py @@ -9,11 +9,11 @@ :license: BSD, see LICENSE for more details. """ from abc import ABC, abstractmethod +import argparse from typing import Optional from uuid import UUID from ..parser import HttpParser -from ...common.flags import Flags from ...core.event import EventQueue from ...core.connection import TcpClientConnection @@ -27,7 +27,7 @@ class HttpProxyBasePlugin(ABC): def __init__( self, uid: UUID, - flags: Flags, + flags: argparse.Namespace, client: TcpClientConnection, event_queue: EventQueue) -> None: self.uid = uid # pragma: no cover diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py index 5ce2948d..01c5e064 100644 --- a/proxy/http/proxy/server.py +++ b/proxy/http/proxy/server.py @@ -10,6 +10,7 @@ """ import logging import threading +import subprocess import os import ssl import socket @@ -18,23 +19,85 @@ import errno from typing import Optional, List, Union, Dict, cast, Any, Tuple from .plugin import HttpProxyBasePlugin -from ..handler import HttpProtocolHandlerPlugin -from ..exception import HttpProtocolException, ProxyConnectionFailed, ProxyAuthenticationFailed +from ..plugin import HttpProtocolHandlerPlugin +from ..exception import HttpProtocolException, ProxyConnectionFailed from ..codes import httpStatusCodes from ..parser import HttpParser, httpParserStates, httpParserTypes from ..methods import httpMethods -from ...common.types import HasFileno -from ...common.constants import PROXY_AGENT_HEADER_VALUE +from ...common.types import Readables, Writables +from ...common.constants import DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_FILE +from ...common.constants import DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE +from ...common.constants import COMMA, DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CERT_FILE +from ...common.constants import PROXY_AGENT_HEADER_VALUE, DEFAULT_DISABLE_HEADERS from ...common.utils import build_http_response, text_ from ...common.pki import gen_public_key, gen_csr, sign_csr from ...core.event import eventNames from ...core.connection import TcpServerConnection, TcpConnectionUninitializedException +from ...common.flag import flags logger = logging.getLogger(__name__) +flags.add_argument( + '--ca-key-file', + type=str, + default=DEFAULT_CA_KEY_FILE, + help='Default: None. CA key to use for signing dynamically generated ' + 'HTTPS certificates. If used, must also pass --ca-cert-file and --ca-signing-key-file' +) +flags.add_argument( + '--ca-cert-dir', + type=str, + default=DEFAULT_CA_CERT_DIR, + help='Default: ~/.proxy.py. Directory to store dynamically generated certificates. ' + 'Also see --ca-key-file, --ca-cert-file and --ca-signing-key-file' +) +flags.add_argument( + '--ca-cert-file', + type=str, + default=DEFAULT_CA_CERT_FILE, + help='Default: None. Signing certificate to use for signing dynamically generated ' + 'HTTPS certificates. If used, must also pass --ca-key-file and --ca-signing-key-file' +) +flags.add_argument( + '--ca-file', + type=str, + default=DEFAULT_CA_FILE, + help='Default: None. Provide path to custom CA file for peer certificate validation. ' + 'Specially useful on MacOS.' +) +flags.add_argument( + '--ca-signing-key-file', + type=str, + default=DEFAULT_CA_SIGNING_KEY_FILE, + help='Default: None. CA signing key to use for dynamic generation of ' + 'HTTPS certificates. If used, must also pass --ca-key-file and --ca-cert-file' +) +flags.add_argument( + '--cert-file', + type=str, + default=DEFAULT_CERT_FILE, + help='Default: None. Server certificate to enable end-to-end TLS encryption with clients. ' + 'If used, must also pass --key-file.' +) +flags.add_argument( + '--disable-headers', + type=str, + default=COMMA.join(DEFAULT_DISABLE_HEADERS), + help='Default: None. Comma separated list of headers to remove before ' + 'dispatching client request to upstream server.') +flags.add_argument( + '--server-recvbuf-size', + type=int, + default=DEFAULT_SERVER_RECVBUF_SIZE, + help='Default: 1 MB. Maximum amount of data received from the ' + 'server in a single recv() operation. Bump this ' + 'value for faster downloads at the expense of ' + 'increased RAM.') + + class HttpProxyPlugin(HttpProtocolHandlerPlugin): """HttpProtocolHandler plugin which implements HttpProxy specifications.""" @@ -43,8 +106,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): reason=b'Connection established' )) - # Used to synchronize with other HttpProxyPlugin instances while - # generating certificates + # Used to synchronization during certificate generation. lock = threading.Lock() def __init__( @@ -67,6 +129,12 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): self.event_queue) self.plugins[instance.name()] = instance + def tls_interception_enabled(self) -> bool: + return self.flags.ca_key_file is not None and \ + self.flags.ca_cert_dir is not None and \ + self.flags.ca_signing_key_file is not None and \ + self.flags.ca_cert_file is not None + def get_descriptors( self) -> Tuple[List[socket.socket], List[socket.socket]]: if not self.request.has_upstream_server(): @@ -81,7 +149,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): w.append(self.server.connection) return r, w - def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool: + def write_to_descriptors(self, w: Writables) -> bool: if self.request.has_upstream_server() and \ self.server and not self.server.closed and \ self.server.has_buffer() and \ @@ -90,20 +158,24 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): try: self.server.flush() except ssl.SSLWantWriteError: - logger.warning('SSLWantWriteError while trying to flush to server, will retry') + logger.warning( + 'SSLWantWriteError while trying to flush to server, will retry') return False except BrokenPipeError: logger.error( 'BrokenPipeError when flushing buffer for server') return True except OSError as e: - logger.exception('OSError when flushing buffer to server', exc_info=e) + logger.exception( + 'OSError when flushing buffer to server', exc_info=e) return True return False - def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool: - if self.request.has_upstream_server( - ) and self.server and not self.server.closed and self.server.connection in r: + def read_from_descriptors(self, r: Readables) -> bool: + if self.request.has_upstream_server() \ + and self.server \ + and not self.server.closed \ + and self.server.connection in r: logger.debug('Server is ready for reads, reading...') try: raw = self.server.recv(self.flags.server_recvbuf_size) @@ -146,7 +218,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): # See https://github.com/abhinavsingh/proxy.py/issues/127 for why # currently response parsing is disabled when TLS interception is enabled. # - # or self.config.tls_interception_enabled(): + # or self.tls_interception_enabled(): if self.response.state == httpParserStates.COMPLETE: self.handle_pipeline_response(raw) else: @@ -209,7 +281,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): if self.server and not self.server.closed: if self.request.state == httpParserStates.COMPLETE and ( self.request.method != httpMethods.CONNECT or - self.flags.tls_interception_enabled()): + self.tls_interception_enabled()): if self.pipeline_request is not None and \ self.pipeline_request.is_connection_upgrade(): # Previous pipelined request was a WebSocket @@ -252,8 +324,6 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): self.emit_request_complete() - self.authenticate() - # Note: can raise HttpRequestRejected exception # Invoke plugin.before_upstream_connection do_connect = True @@ -278,27 +348,8 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): if self.request.method == httpMethods.CONNECT: self.client.queue( HttpProxyPlugin.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT) - # If interception is enabled - if self.flags.tls_interception_enabled(): - # Perform SSL/TLS handshake with upstream - self.wrap_server() - # Generate certificate and perform handshake with client - try: - # wrap_client also flushes client data before wrapping - # sending to client can raise, handle expected exceptions - self.wrap_client() - except BrokenPipeError: - logger.error( - 'BrokenPipeError when wrapping client') - return True - except OSError as e: - logger.exception( - 'OSError when wrapping client', exc_info=e) - return True - # Update all plugin connection reference - for plugin in self.plugins.values(): - plugin.client._conn = self.client.connection - return self.client.connection + if self.tls_interception_enabled(): + return self.intercept() elif self.server: # - proxy-connection header is a mistake, it doesn't seem to be # officially documented in any specification, drop it. @@ -334,7 +385,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): server_host, server_port = self.server.addr if self.server else ( None, None) connection_time_ms = (time.time() - self.start_time) * 1000 - if self.request.method == b'CONNECT': + if self.request.method == httpMethods.CONNECT: logger.info( '%s:%s - %s %s:%s - %s bytes - %.2f ms' % (self.client.addr[0], @@ -356,7 +407,32 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): self.response.total_size, connection_time_ms)) - def gen_ca_signed_certificate(self, cert_file_path: str, certificate: Dict[str, Any]) -> None: + def connect_upstream(self) -> None: + host, port = self.request.host, self.request.port + if host and port: + self.server = TcpServerConnection(text_(host), port) + try: + logger.debug( + 'Connecting to upstream %s:%s' % + (text_(host), port)) + self.server.connect() + self.server.connection.setblocking(False) + logger.debug( + 'Connected to upstream %s:%s' % + (text_(host), port)) + except Exception as e: # TimeoutError, socket.gaierror + self.server.closed = True + raise ProxyConnectionFailed(text_(host), port, repr(e)) from e + else: + logger.exception('Both host and port must exist') + raise HttpProtocolException() + + # + # Interceptor related methods + # + + def gen_ca_signed_certificate( + self, cert_file_path: str, certificate: Dict[str, Any]) -> None: '''CA signing key (default) is used for generating a public key for common_name, if one already doesn't exist. Using generated public key a CSR request is generated, which is then signed by @@ -372,13 +448,21 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): '{0}.{1}'.format(text_(self.request.host), 'pub')) private_key_path = self.flags.ca_signing_key_file private_key_password = '' - subject = '/CN={0}/C={1}/ST={2}/L={3}/O={4}/OU={5}'.format( - upstream_subject.get('commonName', text_(self.request.host)), - upstream_subject.get('countryName', 'NA'), - upstream_subject.get('stateOrProvinceName', 'Unavailable'), - upstream_subject.get('localityName', 'Unavailable'), - upstream_subject.get('organizationName', 'Unavailable'), - upstream_subject.get('organizationalUnitName', 'Unavailable')) + + # Build certificate subject + keys = { + 'CN': 'commonName', + 'C': 'countryName', + 'ST': 'stateOrProvinceName', + 'L': 'localityName', + 'O': 'organizationName', + 'OU': 'organizationalUnitName', + } + subject = '' + for key in keys: + if upstream_subject.get(keys[key], None): + subject += '/{0}={1}'.format(key, + upstream_subject.get(keys[key])) alt_subj_names = [text_(self.request.host), ] validity_in_days = 365 * 2 timeout = 10 @@ -435,61 +519,50 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin): self.gen_ca_signed_certificate(cert_file_path, certificate) return cert_file_path + def intercept(self) -> Union[socket.socket, bool]: + # Perform SSL/TLS handshake with upstream + self.wrap_server() + # Generate certificate and perform handshake with client + try: + # wrap_client also flushes client data before wrapping + # sending to client can raise, handle expected exceptions + self.wrap_client() + except subprocess.TimeoutExpired as e: # Popen communicate timeout + logger.exception( + 'TimeoutExpired during certificate generation', exc_info=e) + return True + except BrokenPipeError: + logger.error( + 'BrokenPipeError when wrapping client') + return True + except OSError as e: + logger.exception( + 'OSError when wrapping client', exc_info=e) + return True + # Update all plugin connection reference + # TODO(abhinavsingh): Is this required? + for plugin in self.plugins.values(): + plugin.client._conn = self.client.connection + return self.client.connection + def wrap_server(self) -> None: assert self.server is not None assert isinstance(self.server.connection, socket.socket) - ctx = ssl.create_default_context( - ssl.Purpose.SERVER_AUTH, cafile=self.flags.ca_file) - ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 - ctx.check_hostname = True - self.server.connection.setblocking(True) - self.server._conn = ctx.wrap_socket( - self.server.connection, - server_hostname=text_(self.request.host)) - self.server.connection.setblocking(False) + self.server.wrap(text_(self.request.host), self.flags.ca_file) + assert isinstance(self.server.connection, ssl.SSLSocket) def wrap_client(self) -> None: - assert self.server is not None + assert self.server is not None and self.flags.ca_signing_key_file is not None assert isinstance(self.server.connection, ssl.SSLSocket) generated_cert = self.generate_upstream_certificate( cast(Dict[str, Any], self.server.connection.getpeercert())) - self.client.connection.setblocking(True) - self.client.flush() - self.client._conn = ssl.wrap_socket( - self.client.connection, - server_side=True, - certfile=generated_cert, - keyfile=self.flags.ca_signing_key_file, - ssl_version=ssl.PROTOCOL_TLSv1_2) - self.client.connection.setblocking(False) + self.client.wrap(self.flags.ca_signing_key_file, generated_cert) logger.debug( 'TLS interception using %s', generated_cert) - def authenticate(self) -> None: - if self.flags.auth_code: - if b'proxy-authorization' not in self.request.headers or \ - self.request.headers[b'proxy-authorization'][1] != self.flags.auth_code: - raise ProxyAuthenticationFailed() - - def connect_upstream(self) -> None: - host, port = self.request.host, self.request.port - if host and port: - self.server = TcpServerConnection(text_(host), port) - try: - logger.debug( - 'Connecting to upstream %s:%s' % - (text_(host), port)) - self.server.connect() - self.server.connection.setblocking(False) - logger.debug( - 'Connected to upstream %s:%s' % - (text_(host), port)) - except Exception as e: # TimeoutError, socket.gaierror - self.server.closed = True - raise ProxyConnectionFailed(text_(host), port, repr(e)) from e - else: - logger.exception('Both host and port must exist') - raise HttpProtocolException() + # + # Event emitter callbacks + # def emit_request_complete(self) -> None: if not self.flags.enable_events: diff --git a/proxy/http/server/pac_plugin.py b/proxy/http/server/pac_plugin.py index 0dfa0b49..20f131dd 100644 --- a/proxy/http/server/pac_plugin.py +++ b/proxy/http/server/pac_plugin.py @@ -16,6 +16,23 @@ from .protocols import httpProtocolTypes from ..websocket import WebsocketFrame from ..parser import HttpParser from ...common.utils import bytes_, text_, build_http_response +from ...common.flag import flags +from ...common.constants import DEFAULT_PAC_FILE, DEFAULT_PAC_FILE_URL_PATH + + +flags.add_argument( + '--pac-file', + type=str, + default=DEFAULT_PAC_FILE, + help='A file (Proxy Auto Configuration) or string to serve when ' + 'the server receives a direct file request. ' + 'Using this option enables proxy.HttpWebServerPlugin.') +flags.add_argument( + '--pac-file-url-path', + type=str, + default=text_(DEFAULT_PAC_FILE_URL_PATH), + help='Default: %s. Web server path to serve the PAC file.' % + text_(DEFAULT_PAC_FILE_URL_PATH)) class HttpWebServerPacFilePlugin(HttpWebServerBasePlugin): diff --git a/proxy/http/server/plugin.py b/proxy/http/server/plugin.py index a1e17c1e..3cb737ea 100644 --- a/proxy/http/server/plugin.py +++ b/proxy/http/server/plugin.py @@ -9,12 +9,12 @@ :license: BSD, see LICENSE for more details. """ from abc import ABC, abstractmethod +import argparse from typing import List, Tuple from uuid import UUID from ..websocket import WebsocketFrame from ..parser import HttpParser -from ...common.flags import Flags from ...core.connection import TcpClientConnection from ...core.event import EventQueue @@ -25,7 +25,7 @@ class HttpWebServerBasePlugin(ABC): def __init__( self, uid: UUID, - flags: Flags, + flags: argparse.Namespace, client: TcpClientConnection, event_queue: EventQueue): self.uid = uid diff --git a/proxy/http/server/web.py b/proxy/http/server/web.py index b1b9475e..d46e0110 100644 --- a/proxy/http/server/web.py +++ b/proxy/http/server/web.py @@ -23,15 +23,26 @@ from ..exception import HttpProtocolException from ..websocket import WebsocketFrame, websocketOpcodes from ..codes import httpStatusCodes from ..parser import HttpParser, httpParserStates, httpParserTypes -from ..handler import HttpProtocolHandlerPlugin +from ..plugin import HttpProtocolHandlerPlugin from ...common.utils import bytes_, text_, build_http_response, build_websocket_handshake_response -from ...common.constants import PROXY_AGENT_HEADER_VALUE -from ...common.types import HasFileno +from ...common.constants import DEFAULT_STATIC_SERVER_DIR, PROXY_AGENT_HEADER_VALUE +from ...common.types import Readables, Writables +from ...common.flag import flags logger = logging.getLogger(__name__) +flags.add_argument( + '--static-server-dir', + type=str, + default=DEFAULT_STATIC_SERVER_DIR, + help='Default: "public" folder in directory where proxy.py is placed. ' + 'This option is only applicable when static server is also enabled. ' + 'See --enable-static-server.' +) + + class HttpWebServerPlugin(HttpProtocolHandlerPlugin): """HttpProtocolHandler plugin which handles incoming requests to local web server.""" @@ -73,6 +84,10 @@ class HttpWebServerPlugin(HttpProtocolHandlerPlugin): for (protocol, route) in instance.routes(): self.routes[protocol][re.compile(route)] = instance + def encryption_enabled(self) -> bool: + return self.flags.keyfile is not None and \ + self.flags.certfile is not None + @staticmethod def read_and_build_static_file_response(path: str) -> memoryview: with open(path, 'rb') as f: @@ -146,7 +161,7 @@ class HttpWebServerPlugin(HttpProtocolHandlerPlugin): # Routing for Http(s) requests protocol = httpProtocolTypes.HTTPS \ - if self.flags.encryption_enabled() else \ + if self.encryption_enabled() else \ httpProtocolTypes.HTTP for route in self.routes[protocol]: match = route.match(text_(self.request.path)) @@ -166,10 +181,10 @@ class HttpWebServerPlugin(HttpProtocolHandlerPlugin): self.client.queue(self.DEFAULT_404_RESPONSE) return True - def write_to_descriptors(self, w: List[Union[int, HasFileno]]) -> bool: + def write_to_descriptors(self, w: Writables) -> bool: pass - def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool: + def read_from_descriptors(self, r: Readables) -> bool: pass def on_client_data(self, raw: memoryview) -> Optional[memoryview]: diff --git a/proxy/http/websocket/__init__.py b/proxy/http/websocket/__init__.py new file mode 100644 index 00000000..2870e3b2 --- /dev/null +++ b/proxy/http/websocket/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from .frame import WebsocketFrame, websocketOpcodes +from .client import WebsocketClient + +__all__ = [ + 'websocketOpcodes', + 'WebsocketFrame', + 'WebsocketClient', +] diff --git a/proxy/http/websocket/client.py b/proxy/http/websocket/client.py new file mode 100644 index 00000000..716d0fae --- /dev/null +++ b/proxy/http/websocket/client.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +import base64 +import selectors +import socket +import secrets +import ssl + +from typing import Optional, Union, Callable + +from .frame import WebsocketFrame + +from ..parser import httpParserTypes, HttpParser + +from ...common.constants import DEFAULT_BUFFER_SIZE +from ...common.utils import new_socket_connection, build_websocket_handshake_request, text_ +from ...core.connection import tcpConnectionTypes, TcpConnection + + +class WebsocketClient(TcpConnection): + + def __init__(self, + hostname: bytes, + port: int, + path: bytes = b'/', + on_message: Optional[Callable[[WebsocketFrame], None]] = None) -> None: + super().__init__(tcpConnectionTypes.CLIENT) + self.hostname: bytes = hostname + self.port: int = port + self.path: bytes = path + self.sock: socket.socket = new_socket_connection( + (socket.gethostbyname(text_(self.hostname)), self.port)) + self.on_message: Optional[Callable[[ + WebsocketFrame], None]] = on_message + self.selector: selectors.DefaultSelector = selectors.DefaultSelector() + + @property + def connection(self) -> Union[ssl.SSLSocket, socket.socket]: + return self.sock + + def handshake(self) -> None: + self.upgrade() + self.sock.setblocking(False) + + def upgrade(self) -> None: + key = base64.b64encode(secrets.token_bytes(16)) + self.sock.send( + build_websocket_handshake_request( + key, + url=self.path, + host=self.hostname)) + response = HttpParser(httpParserTypes.RESPONSE_PARSER) + response.parse(self.sock.recv(DEFAULT_BUFFER_SIZE)) + accept = response.header(b'Sec-Websocket-Accept') + assert WebsocketFrame.key_to_accept(key) == accept + + def ping(self, data: Optional[bytes] = None) -> None: + pass + + def pong(self, data: Optional[bytes] = None) -> None: + pass + + def shutdown(self, _data: Optional[bytes] = None) -> None: + """Closes connection with the server.""" + super().close() + + def run_once(self) -> bool: + ev = selectors.EVENT_READ + if self.has_buffer(): + ev |= selectors.EVENT_WRITE + self.selector.register(self.sock.fileno(), ev) + events = self.selector.select(timeout=1) + self.selector.unregister(self.sock) + for _, mask in events: + if mask & selectors.EVENT_READ and self.on_message: + raw = self.recv() + if raw is None or raw.tobytes() == b'': + self.closed = True + return True + frame = WebsocketFrame() + # TODO(abhinavsingh): Remove .tobytes after parser is + # memoryview compliant + frame.parse(raw.tobytes()) + self.on_message(frame) + elif mask & selectors.EVENT_WRITE: + self.flush() + return False + + def run(self) -> None: + try: + while not self.closed: + teardown = self.run_once() + if teardown: + break + except KeyboardInterrupt: + pass + finally: + if not self.closed: + self.selector.unregister(self.sock) + self.sock.shutdown(socket.SHUT_WR) + self.sock.close() diff --git a/proxy/http/websocket.py b/proxy/http/websocket/frame.py similarity index 58% rename from proxy/http/websocket.py rename to proxy/http/websocket/frame.py index a6eb5a33..55f9d91b 100644 --- a/proxy/http/websocket.py +++ b/proxy/http/websocket/frame.py @@ -10,22 +10,12 @@ """ import hashlib import base64 -import selectors import struct -import socket import secrets -import ssl -import ipaddress import logging import io -from typing import TypeVar, Type, Optional, NamedTuple, Union, Callable - -from .parser import httpParserTypes, HttpParser - -from ..common.constants import DEFAULT_BUFFER_SIZE -from ..common.utils import new_socket_connection, build_websocket_handshake_request -from ..core.connection import tcpConnectionTypes, TcpConnection +from typing import TypeVar, Type, Optional, NamedTuple WebsocketOpcodes = NamedTuple('WebsocketOpcodes', [ @@ -180,89 +170,3 @@ class WebsocketFrame: sha1 = hashlib.sha1() sha1.update(key + WebsocketFrame.GUID) return base64.b64encode(sha1.digest()) - - -class WebsocketClient(TcpConnection): - - def __init__(self, - hostname: Union[ipaddress.IPv4Address, ipaddress.IPv6Address], - port: int, - path: bytes = b'/', - on_message: Optional[Callable[[WebsocketFrame], None]] = None) -> None: - super().__init__(tcpConnectionTypes.CLIENT) - self.hostname: Union[ipaddress.IPv4Address, - ipaddress.IPv6Address] = hostname - self.port: int = port - self.path: bytes = path - self.sock: socket.socket = new_socket_connection( - (str(self.hostname), self.port)) - self.on_message: Optional[Callable[[ - WebsocketFrame], None]] = on_message - self.upgrade() - self.sock.setblocking(False) - self.selector: selectors.DefaultSelector = selectors.DefaultSelector() - - @property - def connection(self) -> Union[ssl.SSLSocket, socket.socket]: - return self.sock - - def upgrade(self) -> None: - key = base64.b64encode(secrets.token_bytes(16)) - self.sock.send(build_websocket_handshake_request(key, url=self.path)) - response = HttpParser(httpParserTypes.RESPONSE_PARSER) - response.parse(self.sock.recv(DEFAULT_BUFFER_SIZE)) - accept = response.header(b'Sec-Websocket-Accept') - assert WebsocketFrame.key_to_accept(key) == accept - - def ping(self, data: Optional[bytes] = None) -> None: - pass - - def pong(self, data: Optional[bytes] = None) -> None: - pass - - def shutdown(self, _data: Optional[bytes] = None) -> None: - """Closes connection with the server.""" - super().close() - - def run_once(self) -> bool: - ev = selectors.EVENT_READ - if self.has_buffer(): - ev |= selectors.EVENT_WRITE - self.selector.register(self.sock.fileno(), ev) - events = self.selector.select(timeout=1) - self.selector.unregister(self.sock) - for _, mask in events: - if mask & selectors.EVENT_READ and self.on_message: - raw = self.recv() - if raw is None or raw.tobytes() == b'': - self.closed = True - logger.debug('Websocket connection closed by server') - return True - frame = WebsocketFrame() - # TODO(abhinavsingh): Remove .tobytes after parser is - # memoryview compliant - frame.parse(raw.tobytes()) - self.on_message(frame) - elif mask & selectors.EVENT_WRITE: - logger.debug(self.buffer) - self.flush() - return False - - def run(self) -> None: - logger.debug('running') - try: - while not self.closed: - teardown = self.run_once() - if teardown: - break - except KeyboardInterrupt: - pass - finally: - try: - self.selector.unregister(self.sock) - self.sock.shutdown(socket.SHUT_WR) - except Exception as e: - logging.exception( - 'Exception while shutdown of websocket client', exc_info=e) - self.sock.close() - logger.info('done') diff --git a/proxy/plugin/__init__.py b/proxy/plugin/__init__.py index 1ef0af91..8450e8f6 100644 --- a/proxy/plugin/__init__.py +++ b/proxy/plugin/__init__.py @@ -18,6 +18,9 @@ from .shortlink import ShortLinkPlugin from .web_server_route import WebServerPlugin from .reverse_proxy import ReverseProxyPlugin from .proxy_pool import ProxyPoolPlugin +from .filter_by_client_ip import FilterByClientIpPlugin +from .filter_by_url_regex import FilterByURLRegexPlugin +from .modify_chunk_response import ModifyChunkResponsePlugin __all__ = [ 'CacheResponsesPlugin', @@ -31,4 +34,7 @@ __all__ = [ 'WebServerPlugin', 'ReverseProxyPlugin', 'ProxyPoolPlugin', + 'FilterByClientIpPlugin', + 'ModifyChunkResponsePlugin', + 'FilterByURLRegexPlugin', ] diff --git a/proxy/plugin/cache/cache_responses.py b/proxy/plugin/cache/cache_responses.py index 91f29079..f6da087e 100644 --- a/proxy/plugin/cache/cache_responses.py +++ b/proxy/plugin/cache/cache_responses.py @@ -9,7 +9,6 @@ :license: BSD, see LICENSE for more details. """ import multiprocessing -import tempfile from typing import Any from .store.disk import OnDiskCacheStore @@ -25,5 +24,5 @@ class CacheResponsesPlugin(BaseCacheResponsesPlugin): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.disk_store = OnDiskCacheStore( - uid=self.uid, cache_dir=tempfile.gettempdir()) + uid=self.uid, cache_dir=self.flags.cache_dir) self.set_store(self.disk_store) diff --git a/proxy/plugin/cache/store/disk.py b/proxy/plugin/cache/store/disk.py index 91eb4f29..1f472a12 100644 --- a/proxy/plugin/cache/store/disk.py +++ b/proxy/plugin/cache/store/disk.py @@ -10,9 +10,11 @@ """ import logging import os +import tempfile from typing import Optional, BinaryIO from uuid import UUID +from ....common.flag import flags from ....common.utils import text_ from ....http.parser import HttpParser @@ -21,6 +23,14 @@ from .base import CacheStore logger = logging.getLogger(__name__) +flags.add_argument( + '--cache-dir', + type=str, + default=tempfile.gettempdir(), + help='Default: A temporary directory. Flag only applicable when cache plugin is used with on-disk storage.' +) + + class OnDiskCacheStore(CacheStore): def __init__(self, uid: UUID, cache_dir: str) -> None: diff --git a/proxy/plugin/filter_by_client_ip.py b/proxy/plugin/filter_by_client_ip.py new file mode 100644 index 00000000..95169b48 --- /dev/null +++ b/proxy/plugin/filter_by_client_ip.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from typing import Optional + +from ..common.flag import flags +from ..http.exception import HttpRequestRejected +from ..http.parser import HttpParser +from ..http.codes import httpStatusCodes +from ..http.proxy import HttpProxyBasePlugin + + +flags.add_argument( + '--filtered-client-ips', + type=str, + default='127.0.0.1,::1', + help='Default: 127.0.0.1,::1. Comma separated list of IPv4 and IPv6 addresses.' +) + + +class FilterByClientIpPlugin(HttpProxyBasePlugin): + """Drop traffic by inspecting incoming client IP address.""" + + def before_upstream_connection( + self, request: HttpParser) -> Optional[HttpParser]: + if self.client.addr[0] in self.flags.filtered_client_ips.split(','): + raise HttpRequestRejected( + status_code=httpStatusCodes.I_AM_A_TEAPOT, reason=b'I\'m a tea pot', + headers={ + b'Connection': b'close', + } + ) + return request + + def handle_client_request( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + return chunk + + def on_upstream_connection_close(self) -> None: + pass diff --git a/proxy/plugin/filter_by_url_regex.py b/proxy/plugin/filter_by_url_regex.py new file mode 100644 index 00000000..1e9d5efb --- /dev/null +++ b/proxy/plugin/filter_by_url_regex.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" + +import logging + +from typing import Optional, List, Dict, Any + +from ..http.exception import HttpRequestRejected +from ..http.parser import HttpParser +from ..http.codes import httpStatusCodes +from ..http.proxy import HttpProxyBasePlugin +from ..common.utils import text_ + +import re + +logger = logging.getLogger(__name__) + + +class FilterByURLRegexPlugin(HttpProxyBasePlugin): + """Drops traffic by inspecting request URL and checking + against a list of regular expressions. Example, default + filter list below can be used as a starting point for + filtering ads. + """ + + FILTER_LIST: List[Dict[str, Any]] = [ + { + 'regex': b'tpc.googlesyndication.com/simgad/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google image ads', + }, + { + 'regex': b'tpc.googlesyndication.com/sadbundle/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google animated ad bundles', + }, + { + 'regex': b'pagead\\d+.googlesyndication.com/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google tracking', + }, + { + 'regex': b'(www){0,1}.google-analytics.com/r/collect\\?.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google tracking', + }, + { + 'regex': b'(www){0,1}.facebook.com/tr/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Facebook tracking', + }, + { + 'regex': b'tpc.googlesyndication.com/daca_images/simgad/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google image ads', + }, + { + 'regex': b'.*.2mdn.net/videoplayback/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Twitch.tv video ads', + }, + { + 'regex': b'(www.){0,1}google.com(.*)/pagead/.*', + 'status_code': httpStatusCodes.NOT_FOUND, + 'notes': 'Google ads', + }, + ] + + def before_upstream_connection( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_client_request( + self, request: HttpParser) -> Optional[HttpParser]: + + # determine host + request_host = None + if request.host: + request_host = request.host + else: + if b'host' in request.headers: + request_host = request.header(b'host') + + if not request_host: + logger.error("Cannot determine host") + return request + + # build URL + url = b'%s%s' % ( + request_host, + request.path, + ) + + # check URL against list + rule_number = 1 + for blocked_entry in self.FILTER_LIST: + + # if regex matches on URL + if re.search(text_(blocked_entry['regex']), text_(url)): + + # log that the request has been filtered + logger.info("Blocked: %r with status_code '%r' by rule number '%r'" % ( + text_(url), + blocked_entry['status_code'], + rule_number, + )) + + # close the connection with the status code from the filter + # list + raise HttpRequestRejected( + status_code=blocked_entry['status_code'], + headers={b'Connection': b'close'}, + reason=b'Blocked', + ) + + # stop looping through filter list + break + + # increment rule number + rule_number += 1 + + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + return chunk + + def on_upstream_connection_close(self) -> None: + pass diff --git a/proxy/plugin/modify_chunk_response.py b/proxy/plugin/modify_chunk_response.py new file mode 100644 index 00000000..707da5de --- /dev/null +++ b/proxy/plugin/modify_chunk_response.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from typing import Optional, Any + +from ..http.parser import HttpParser, httpParserTypes, httpParserStates +from ..http.proxy import HttpProxyBasePlugin + + +class ModifyChunkResponsePlugin(HttpProxyBasePlugin): + """Accumulate & modify chunk responses as received from upstream.""" + + DEFAULT_CHUNKS = [ + b'modify', + b'chunk', + b'response', + b'plugin', + ] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # Create a new http protocol parser for response payloads + self.response = HttpParser(httpParserTypes.RESPONSE_PARSER) + + def before_upstream_connection( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_client_request( + self, request: HttpParser) -> Optional[HttpParser]: + return request + + def handle_upstream_chunk(self, chunk: memoryview) -> memoryview: + # Parse the response. + # Note that these chunks also include headers + self.response.parse(chunk.tobytes()) + # If response is complete, modify and dispatch to client + if self.response.state == httpParserStates.COMPLETE: + self.response.body = b'\n'.join(self.DEFAULT_CHUNKS) + b'\n' + self.client.queue(memoryview(self.response.build_response())) + return memoryview(b'') + + def on_upstream_connection_close(self) -> None: + pass diff --git a/proxy/proxy.py b/proxy/proxy.py index 54790326..023c43cb 100644 --- a/proxy/proxy.py +++ b/proxy/proxy.py @@ -8,27 +8,128 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. """ +import abc +import argparse +import base64 +import collections import contextlib +import ipaddress +import multiprocessing import os +import socket import sys import time import logging +import importlib +import inspect from types import TracebackType -from typing import List, Optional, Generator, Any, Type +from typing import Dict, List, Optional, Generator, Any, Tuple, Type, Union, cast -from .common.utils import bytes_ -from .common.flags import Flags +from .common.utils import bytes_, text_ +from .common.types import IpAddress +from .common.version import __version__ from .core.acceptor import AcceptorPool from .http.handler import HttpProtocolHandler +from .common.flag import flags +from .common.constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, PLUGIN_PROXY_AUTH +from .common.constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS +from .common.constants import DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_NUM_WORKERS +from .common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_ENABLE_DEVTOOLS +from .common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_WEB_SERVER +from .common.constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL +from .common.constants import DEFAULT_OPEN_FILE_LIMIT, DEFAULT_PID_FILE, DEFAULT_PLUGINS +from .common.constants import DEFAULT_VERSION, DOT, PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL +from .common.constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE +from .common.constants import PLUGIN_WEB_SERVER, PY2_DEPRECATION_MESSAGE + +if os.name != 'nt': + import resource logger = logging.getLogger(__name__) +flags.add_argument( + '--pid-file', + type=str, + default=DEFAULT_PID_FILE, + help='Default: None. Save parent process ID to a file.') +flags.add_argument( + '--version', + '-v', + action='store_true', + default=DEFAULT_VERSION, + help='Prints proxy.py version.') +flags.add_argument( + '--disable-http-proxy', + action='store_true', + default=DEFAULT_DISABLE_HTTP_PROXY, + help='Default: False. Whether to disable proxy.HttpProxyPlugin.') +flags.add_argument( + '--enable-dashboard', + action='store_true', + default=DEFAULT_ENABLE_DASHBOARD, + help='Default: False. Enables proxy.py dashboard.' +) +flags.add_argument( + '--enable-devtools', + action='store_true', + default=DEFAULT_ENABLE_DEVTOOLS, + help='Default: False. Enables integration with Chrome Devtool Frontend. Also see --devtools-ws-path.' +) +flags.add_argument( + '--enable-static-server', + action='store_true', + default=DEFAULT_ENABLE_STATIC_SERVER, + help='Default: False. Enable inbuilt static file server. ' + 'Optionally, also use --static-server-dir to serve static content ' + 'from custom directory. By default, static file server serves ' + 'out of installed proxy.py python module folder.' +) +flags.add_argument( + '--enable-web-server', + action='store_true', + default=DEFAULT_ENABLE_WEB_SERVER, + help='Default: False. Whether to enable proxy.HttpWebServerPlugin.') +flags.add_argument( + '--log-level', + type=str, + default=DEFAULT_LOG_LEVEL, + help='Valid options: DEBUG, INFO (default), WARNING, ERROR, CRITICAL. ' + 'Both upper and lowercase values are allowed. ' + 'You may also simply use the leading character e.g. --log-level d') +flags.add_argument( + '--log-file', + type=str, + default=DEFAULT_LOG_FILE, + help='Default: sys.stdout. Log file destination.') +flags.add_argument( + '--log-format', + type=str, + default=DEFAULT_LOG_FORMAT, + help='Log format for Python logger.') +flags.add_argument( + '--open-file-limit', + type=int, + default=DEFAULT_OPEN_FILE_LIMIT, + help='Default: 1024. Maximum number of files (TCP connections) ' + 'that proxy.py can open concurrently.') +flags.add_argument( + '--plugins', + type=str, + default=DEFAULT_PLUGINS, + help='Comma separated plugins') + + class Proxy: + """Context manager for controlling core AcceptorPool server lifecycle. + + By default this context manager starts AcceptorPool with HttpProtocolHandler + worker class. + """ def __init__(self, input_args: Optional[List[str]], **opts: Any) -> None: - self.flags = Flags.initialize(input_args, **opts) + self.flags = Proxy.initialize(input_args, **opts) self.acceptors: Optional[AcceptorPool] = None def write_pid_file(self) -> None: @@ -58,6 +159,264 @@ class Proxy: self.acceptors.shutdown() self.delete_pid_file() + @staticmethod + def initialize(input_args: Optional[List[str]] + = None, **opts: Any) -> argparse.Namespace: + if input_args is None: + input_args = [] + + if not Proxy.is_py3(): + print(PY2_DEPRECATION_MESSAGE) + sys.exit(1) + + # Discover flags from requested plugin. + # This also surface external plugin flags under --help + for i, f in enumerate(input_args): + if f == '--plugin': + Proxy.import_plugin(bytes_(input_args[i + 1])) + + # Parse flags + args = flags.parse_args(input_args) + + # Print version and exit + if args.version: + print(__version__) + sys.exit(0) + + # Setup logging module + Proxy.setup_logger(args.log_file, args.log_level, args.log_format) + + # Setup limits + Proxy.set_open_file_limit(args.open_file_limit) + + # Load plugins + default_plugins = Proxy.get_default_plugins(args) + + # Load default plugins along with user provided --plugins + plugins = Proxy.load_plugins( + [bytes_(p) for p in collections.OrderedDict(default_plugins).keys()] + + [p if isinstance(p, type) else bytes_(p) for p in opts.get( + 'plugins', args.plugins.split(text_(COMMA)))] + ) + + # proxy.py currently cannot serve over HTTPS and perform TLS interception + # at the same time. Check if user is trying to enable both feature + # at the same time. + if (args.cert_file and args.key_file) and \ + (args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file): + print('You can either enable end-to-end encryption OR TLS interception,' + 'not both together.') + sys.exit(1) + + # Generate auth_code required for basic authentication if enabled + auth_code = None + if args.basic_auth: + auth_code = base64.b64encode(bytes_(args.basic_auth)) + + args.plugins = plugins + args.auth_code = cast( + Optional[bytes], + opts.get( + 'auth_code', + auth_code)) + args.server_recvbuf_size = cast( + int, + opts.get( + 'server_recvbuf_size', + args.server_recvbuf_size)) + args.client_recvbuf_size = cast( + int, + opts.get( + 'client_recvbuf_size', + args.client_recvbuf_size)) + args.pac_file = cast( + Optional[str], opts.get( + 'pac_file', bytes_( + args.pac_file))) + args.pac_file_url_path = cast( + Optional[bytes], opts.get( + 'pac_file_url_path', bytes_( + args.pac_file_url_path))) + disabled_headers = cast(Optional[List[bytes]], opts.get('disable_headers', [ + header.lower() for header in bytes_( + args.disable_headers).split(COMMA) if header.strip() != b''])) + args.disable_headers = disabled_headers if disabled_headers is not None else DEFAULT_DISABLE_HEADERS + args.certfile = cast( + Optional[str], opts.get( + 'cert_file', args.cert_file)) + args.keyfile = cast(Optional[str], opts.get('key_file', args.key_file)) + args.ca_key_file = cast( + Optional[str], opts.get( + 'ca_key_file', args.ca_key_file)) + args.ca_cert_file = cast( + Optional[str], opts.get( + 'ca_cert_file', args.ca_cert_file)) + args.ca_signing_key_file = cast( + Optional[str], + opts.get( + 'ca_signing_key_file', + args.ca_signing_key_file)) + args.ca_file = cast( + Optional[str], + opts.get( + 'ca_file', + args.ca_file)) + args.hostname = cast(IpAddress, + opts.get('hostname', ipaddress.ip_address(args.hostname))) + args.family = socket.AF_INET6 if args.hostname.version == 6 else socket.AF_INET + args.port = cast(int, opts.get('port', args.port)) + args.backlog = cast(int, opts.get('backlog', args.backlog)) + num_workers = opts.get('num_workers', args.num_workers) + num_workers = num_workers if num_workers is not None else DEFAULT_NUM_WORKERS + args.num_workers = cast( + int, num_workers if num_workers > 0 else multiprocessing.cpu_count()) + args.static_server_dir = cast( + str, + opts.get( + 'static_server_dir', + args.static_server_dir)) + args.enable_static_server = cast( + bool, + opts.get( + 'enable_static_server', + args.enable_static_server)) + args.devtools_ws_path = cast( + bytes, + opts.get( + 'devtools_ws_path', + getattr(args, 'devtools_ws_path', DEFAULT_DEVTOOLS_WS_PATH))) + args.timeout = cast(int, opts.get('timeout', args.timeout)) + args.threadless = cast(bool, opts.get('threadless', args.threadless)) + args.enable_events = cast( + bool, + opts.get( + 'enable_events', + args.enable_events)) + args.pid_file = cast( + Optional[str], opts.get( + 'pid_file', args.pid_file)) + + args.proxy_py_data_dir = DEFAULT_DATA_DIRECTORY_PATH + os.makedirs(args.proxy_py_data_dir, exist_ok=True) + + ca_cert_dir = opts.get('ca_cert_dir', args.ca_cert_dir) + args.ca_cert_dir = cast(Optional[str], ca_cert_dir) + if args.ca_cert_dir is None: + args.ca_cert_dir = os.path.join( + args.proxy_py_data_dir, 'certificates') + os.makedirs(args.ca_cert_dir, exist_ok=True) + + return args + + @staticmethod + def load_plugins(plugins: List[Union[bytes, type]] + ) -> Dict[bytes, List[type]]: + """Accepts a comma separated list of Python modules and returns + a list of respective Python classes.""" + p: Dict[bytes, List[type]] = { + b'HttpProtocolHandlerPlugin': [], + b'HttpProxyBasePlugin': [], + b'HttpWebServerBasePlugin': [], + b'ProxyDashboardWebsocketPlugin': [] + } + for plugin_ in plugins: + klass, module_name = Proxy.import_plugin(plugin_) + if klass is None and module_name is None: + continue + mro = list(inspect.getmro(klass)) + mro.reverse() + iterator = iter(mro) + while next(iterator) is not abc.ABC: + pass + base_klass = next(iterator) + if klass not in p[bytes_(base_klass.__name__)]: + p[bytes_(base_klass.__name__)].append(klass) + logger.info('Loaded plugin %s.%s', module_name, klass.__name__) + return p + + @staticmethod + def import_plugin(plugin: Union[bytes, type]) -> Any: + if isinstance(plugin, type): + module_name = '__main__' + klass = plugin + else: + plugin_ = text_(plugin.strip()) + if plugin_ == '': + return (None, None) + module_name, klass_name = plugin_.rsplit(text_(DOT), 1) + klass = getattr( + importlib.import_module( + module_name.replace( + os.path.sep, text_(DOT))), + klass_name) + return (klass, module_name) + + @staticmethod + def get_default_plugins( + args: argparse.Namespace) -> List[Tuple[str, bool]]: + # Prepare list of plugins to load based upon + # --enable-*, --disable-* and --basic-auth flags. + default_plugins: List[Tuple[str, bool]] = [] + if args.basic_auth is not None: + default_plugins.append((PLUGIN_PROXY_AUTH, True)) + if args.enable_dashboard: + default_plugins.append((PLUGIN_WEB_SERVER, True)) + args.enable_static_server = True + default_plugins.append((PLUGIN_DASHBOARD, True)) + default_plugins.append((PLUGIN_INSPECT_TRAFFIC, True)) + args.enable_events = True + args.enable_devtools = True + if args.enable_devtools: + default_plugins.append((PLUGIN_DEVTOOLS_PROTOCOL, True)) + default_plugins.append((PLUGIN_WEB_SERVER, True)) + if not args.disable_http_proxy: + default_plugins.append((PLUGIN_HTTP_PROXY, True)) + if args.enable_web_server or \ + args.pac_file is not None or \ + args.enable_static_server: + default_plugins.append((PLUGIN_WEB_SERVER, True)) + if args.pac_file is not None: + default_plugins.append((PLUGIN_PAC_FILE, True)) + return default_plugins + + @staticmethod + def is_py3() -> bool: + """Exists only to avoid mocking sys.version_info in tests.""" + return sys.version_info[0] == 3 + + @staticmethod + def setup_logger( + log_file: Optional[str] = DEFAULT_LOG_FILE, + log_level: str = DEFAULT_LOG_LEVEL, + log_format: str = DEFAULT_LOG_FORMAT) -> None: + ll = getattr( + logging, + {'D': 'DEBUG', + 'I': 'INFO', + 'W': 'WARNING', + 'E': 'ERROR', + 'C': 'CRITICAL'}[log_level.upper()[0]]) + if log_file: + logging.basicConfig( + filename=log_file, + filemode='a', + level=ll, + format=log_format) + else: + logging.basicConfig(level=ll, format=log_format) + + @staticmethod + def set_open_file_limit(soft_limit: int) -> None: + """Configure open file description soft limit on supported OS.""" + if os.name != 'nt': # resource module not available on Windows OS + curr_soft_limit, curr_hard_limit = resource.getrlimit( + resource.RLIMIT_NOFILE) + if curr_soft_limit < soft_limit < curr_hard_limit: + resource.setrlimit( + resource.RLIMIT_NOFILE, (soft_limit, curr_hard_limit)) + logger.debug( + 'Open file soft limit set to %d', soft_limit) + @contextlib.contextmanager def start( @@ -78,7 +437,8 @@ def main( **opts: Any) -> None: try: with Proxy(input_args=input_args, **opts): - # TODO: Introduce cron feature instead of mindless sleep + # TODO: Introduce cron feature + # https://github.com/abhinavsingh/proxy.py/issues/392 while True: time.sleep(1) except KeyboardInterrupt: diff --git a/requirements-release.txt b/requirements-release.txt index 8c2c116d..0c899fc4 100644 --- a/requirements-release.txt +++ b/requirements-release.txt @@ -1,2 +1,2 @@ -twine==3.1.1 -wheel==0.34.2 +twine==3.2.0 +wheel==0.36.2 diff --git a/requirements-testing.txt b/requirements-testing.txt index 11e15509..0d41fcee 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,13 +1,13 @@ python-coveralls==2.9.3 -coverage==5.1 -flake8==3.8.3 -pytest==5.4.3 -pytest-cov==2.10.0 -autopep8==1.5.3 -mypy==0.780 +coverage==5.3 +flake8==3.8.4 +pytest==6.2.1 +pytest-cov==2.10.1 +autopep8==1.5.4 +mypy==0.790 py-spy==0.3.3 -codecov==2.1.7 -tox==3.15.2 +codecov==2.1.11 +tox==3.20.1 mccabe==0.6.1 -pylint==2.5.3 -rope==0.17.0 +pylint==2.6.0 +rope==0.18.0 diff --git a/requirements-tunnel.txt b/requirements-tunnel.txt index ada4a365..58f39bb2 100644 --- a/requirements-tunnel.txt +++ b/requirements-tunnel.txt @@ -1 +1 @@ -paramiko==2.7.1 +paramiko==2.7.2 diff --git a/requirements.txt b/requirements.txt index f0c0fb60..9ae66d02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -typing-extensions==3.7.4.2 +typing-extensions==3.7.4.3 diff --git a/setup.py b/setup.py index 0d2c6751..b09bc800 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ """ from setuptools import setup, find_packages -VERSION = (2, 2, 0) +VERSION = (2, 3, 0) __version__ = '.'.join(map(str, VERSION[0:3])) __description__ = '''⚡⚡⚡Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on Network monitoring, controls & Application development, testing, debugging.''' @@ -28,7 +28,8 @@ if __name__ == '__main__': author_email=__author_email__, url=__homepage__, description=__description__, - long_description=open('README.md', 'r', encoding='utf-8').read().strip(), + long_description=open( + 'README.md', 'r', encoding='utf-8').read().strip(), long_description_content_type='text/markdown', download_url=__download_url__, license=__license__, diff --git a/tests/common/test_flags.py b/tests/common/test_flags.py new file mode 100644 index 00000000..c36f8e63 --- /dev/null +++ b/tests/common/test_flags.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +""" + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on + Network monitoring, controls & Application development, testing, debugging. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +""" +from proxy.common.utils import bytes_ +from proxy.common.constants import PLUGIN_HTTP_PROXY +import unittest + +from typing import List, Dict + +from proxy.proxy import Proxy +from proxy.http.proxy import HttpProxyPlugin +from proxy.plugin import CacheResponsesPlugin +from proxy.plugin import FilterByUpstreamHostPlugin + + +class TestFlags(unittest.TestCase): + def assert_plugins(self, expected: Dict[str, List[type]]) -> None: + for k in expected: + self.assertIn(k.encode(), self.flags.plugins) + for p in expected[k]: + self.assertIn(p, self.flags.plugins[k.encode()]) + self.assertEqual( + len([o for o in self.flags.plugins[k.encode()] if o == p]), 1) + + def test_load_plugin_from_bytes(self) -> None: + self.flags = Proxy.initialize([], plugins=[ + b'proxy.plugin.CacheResponsesPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_bytes(self) -> None: + self.flags = Proxy.initialize([], plugins=[ + b'proxy.plugin.CacheResponsesPlugin', + b'proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugin_from_args(self) -> None: + self.flags = Proxy.initialize([ + '--plugins', 'proxy.plugin.CacheResponsesPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_args(self) -> None: + self.flags = Proxy.initialize([ + '--plugins', 'proxy.plugin.CacheResponsesPlugin,proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugin_from_class(self) -> None: + self.flags = Proxy.initialize([], plugins=[ + CacheResponsesPlugin, + ]) + self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]}) + + def test_load_plugins_from_class(self) -> None: + self.flags = Proxy.initialize([], plugins=[ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_load_plugins_from_bytes_and_class(self) -> None: + self.flags = Proxy.initialize([], plugins=[ + CacheResponsesPlugin, + b'proxy.plugin.FilterByUpstreamHostPlugin', + ]) + self.assert_plugins({'HttpProxyBasePlugin': [ + CacheResponsesPlugin, + FilterByUpstreamHostPlugin, + ]}) + + def test_unique_plugin_from_bytes(self) -> None: + self.flags = Proxy.initialize([], plugins=[ + bytes_(PLUGIN_HTTP_PROXY), + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + + def test_unique_plugin_from_args(self) -> None: + self.flags = Proxy.initialize([ + '--plugins', PLUGIN_HTTP_PROXY, + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + + def test_unique_plugin_from_class(self) -> None: + self.flags = Proxy.initialize([], plugins=[ + HttpProxyPlugin, + ]) + self.assert_plugins({'HttpProtocolHandlerPlugin': [ + HttpProxyPlugin, + ]}) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/common/test_pki.py b/tests/common/test_pki.py index ebeb767d..e55c0637 100644 --- a/tests/common/test_pki.py +++ b/tests/common/test_pki.py @@ -112,7 +112,9 @@ class TestPki(unittest.TestCase): def _gen_private_key(self) -> Tuple[str, str]: key_path = os.path.join(tempfile.gettempdir(), 'test_gen_private.key') - nopass_key_path = os.path.join(tempfile.gettempdir(), 'test_gen_private_nopass.key') + nopass_key_path = os.path.join( + tempfile.gettempdir(), + 'test_gen_private_nopass.key') pki.gen_private_key(key_path, 'password') pki.remove_passphrase(key_path, 'password', nopass_key_path) return (key_path, nopass_key_path) diff --git a/tests/core/test_acceptor.py b/tests/core/test_acceptor.py index 2b4dbd9b..aae3de5c 100644 --- a/tests/core/test_acceptor.py +++ b/tests/core/test_acceptor.py @@ -14,8 +14,8 @@ import selectors import multiprocessing from unittest import mock -from proxy.common.flags import Flags from proxy.core.acceptor import Acceptor +from proxy.proxy import Proxy class TestAcceptor(unittest.TestCase): @@ -24,7 +24,7 @@ class TestAcceptor(unittest.TestCase): self.acceptor_id = 1 self.mock_protocol_handler = mock.MagicMock() self.pipe = multiprocessing.Pipe() - self.flags = Flags() + self.flags = Proxy.initialize() self.acceptor = Acceptor( idd=self.acceptor_id, work_queue=self.pipe[1], diff --git a/tests/core/test_acceptor_pool.py b/tests/core/test_acceptor_pool.py index e8192495..3142e39b 100644 --- a/tests/core/test_acceptor_pool.py +++ b/tests/core/test_acceptor_pool.py @@ -12,7 +12,7 @@ import unittest import socket from unittest import mock -from proxy.common.flags import Flags +from proxy.proxy import Proxy from proxy.core.acceptor import AcceptorPool @@ -35,7 +35,7 @@ class TestAcceptorPool(unittest.TestCase): num_workers = 2 sock = mock_socket.return_value work_klass = mock.MagicMock() - flags = Flags(num_workers=2) + flags = Proxy.initialize(num_workers=2) pool = AcceptorPool(flags=flags, work_klass=work_klass) pool.setup() diff --git a/tests/http/test_http_parser.py b/tests/http/test_http_parser.py index c98e6381..253f2b7f 100644 --- a/tests/http/test_http_parser.py +++ b/tests/http/test_http_parser.py @@ -139,7 +139,8 @@ class TestHttpParser(unittest.TestCase): self.assertEqual(self.parser.url.port, None) self.assertEqual(self.parser.version, b'HTTP/1.1') self.assertEqual(self.parser.state, httpParserStates.COMPLETE) - self.assertEqual(self.parser.headers[b'host'], (b'Host', b'example.com')) + self.assertEqual( + self.parser.headers[b'host'], (b'Host', b'example.com')) self.parser.del_headers([b'host']) self.parser.add_headers([(b'Host', b'example.com')]) self.assertEqual( @@ -193,7 +194,10 @@ class TestHttpParser(unittest.TestCase): self.parser.parse(CRLF * 2) self.assertEqual(self.parser.total_size, len(pkt) + (3 * len(CRLF)) + len(host_hdr)) - self.assertEqual(self.parser.headers[b'host'], (b'Host', b'localhost:8080')) + self.assertEqual( + self.parser.headers[b'host'], + (b'Host', + b'localhost:8080')) self.assertEqual(self.parser.state, httpParserStates.COMPLETE) def test_get_partial_parse2(self) -> None: @@ -210,7 +214,10 @@ class TestHttpParser(unittest.TestCase): self.assertEqual(self.parser.state, httpParserStates.LINE_RCVD) self.parser.parse(b'localhost:8080' + CRLF) - self.assertEqual(self.parser.headers[b'host'], (b'Host', b'localhost:8080')) + self.assertEqual( + self.parser.headers[b'host'], + (b'Host', + b'localhost:8080')) self.assertEqual(self.parser.buffer, b'') self.assertEqual( self.parser.state, @@ -301,7 +308,7 @@ class TestHttpParser(unittest.TestCase): See https://github.com/abhinavsingh/py/issues/5 for details. """ self.parser.parse(b'CONNECT pypi.org:443 HTTP/1.0\r\n\r\n') - self.assertEqual(self.parser.method, b'CONNECT') + self.assertEqual(self.parser.method, httpMethods.CONNECT) self.assertEqual(self.parser.version, b'HTTP/1.0') self.assertEqual(self.parser.state, httpParserStates.COMPLETE) diff --git a/tests/http/test_http_proxy.py b/tests/http/test_http_proxy.py index 60024d3f..1be5af96 100644 --- a/tests/http/test_http_proxy.py +++ b/tests/http/test_http_proxy.py @@ -13,7 +13,7 @@ import selectors from unittest import mock from proxy.common.constants import DEFAULT_HTTP_PORT -from proxy.common.flags import Flags +from proxy.proxy import Proxy from proxy.core.connection import TcpClientConnection from proxy.http.proxy import HttpProxyPlugin from proxy.http.handler import HttpProtocolHandler @@ -33,7 +33,7 @@ class TestHttpProxyPlugin(unittest.TestCase): self.fileno = 10 self._addr = ('127.0.0.1', 54382) - self.flags = Flags() + self.flags = Proxy.initialize() self.plugin = mock.MagicMock() self.flags.plugins = { b'HttpProtocolHandlerPlugin': [HttpProxyPlugin], diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py index 7799c1d5..ddb79870 100644 --- a/tests/http/test_http_proxy_tls_interception.py +++ b/tests/http/test_http_proxy_tls_interception.py @@ -17,12 +17,12 @@ import selectors from typing import Any from unittest import mock -from proxy.core.connection import TcpClientConnection +from proxy.core.connection import TcpClientConnection, TcpServerConnection from proxy.http.handler import HttpProtocolHandler from proxy.http.proxy import HttpProxyPlugin from proxy.http.methods import httpMethods from proxy.common.utils import build_http_request, bytes_ -from proxy.common.flags import Flags +from proxy.proxy import Proxy class TestHttpProxyTlsInterception(unittest.TestCase): @@ -71,12 +71,17 @@ class TestHttpProxyTlsInterception(unittest.TestCase): return ssl_connection return plain_connection + # Do not mock the original wrap method + self.mock_server_conn.return_value.wrap.side_effect = \ + lambda x, y: TcpServerConnection.wrap( + self.mock_server_conn.return_value, x, y) + type(self.mock_server_conn.return_value).connection = \ mock.PropertyMock(side_effect=mock_connection) self.fileno = 10 self._addr = ('127.0.0.1', 54382) - self.flags = Flags( + self.flags = Proxy.initialize( ca_cert_file='ca-cert.pem', ca_key_file='ca-key.pem', ca_signing_key_file='ca-signing-key.pem' @@ -146,7 +151,8 @@ class TestHttpProxyTlsInterception(unittest.TestCase): self.mock_server_conn.return_value.connection.setblocking.assert_called_with( False) - self.mock_ssl_context.assert_called_with(ssl.Purpose.SERVER_AUTH, cafile=None) + self.mock_ssl_context.assert_called_with( + ssl.Purpose.SERVER_AUTH, cafile=None) # self.assertEqual(self.mock_ssl_context.return_value.options, # ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | # ssl.OP_NO_TLSv1_1) @@ -169,7 +175,7 @@ class TestHttpProxyTlsInterception(unittest.TestCase): keyfile=self.flags.ca_signing_key_file, certfile=HttpProxyPlugin.generated_cert_file_path( self.flags.ca_cert_dir, host), - ssl_version=ssl.PROTOCOL_TLSv1_2 + ssl_version=ssl.PROTOCOL_TLS ) self.assertEqual(self._conn.setblocking.call_count, 2) self.assertEqual( diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py index ca4dac03..a79e6b4a 100644 --- a/tests/http/test_protocol_handler.py +++ b/tests/http/test_protocol_handler.py @@ -15,10 +15,10 @@ import base64 from typing import cast from unittest import mock +from proxy.proxy import Proxy from proxy.common.version import __version__ -from proxy.common.flags import Flags from proxy.common.utils import bytes_ -from proxy.common.constants import CRLF +from proxy.common.constants import CRLF, PLUGIN_HTTP_PROXY, PLUGIN_PROXY_AUTH, PLUGIN_WEB_SERVER from proxy.core.connection import TcpClientConnection from proxy.http.parser import HttpParser from proxy.http.proxy import HttpProxyPlugin @@ -39,9 +39,11 @@ class TestHttpProtocolHandler(unittest.TestCase): self._conn = mock_fromfd.return_value self.http_server_port = 65535 - self.flags = Flags() - self.flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + self.flags = Proxy.initialize() + self.flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + ]) self.mock_selector = mock_selector self.protocol_handler = HttpProtocolHandler( @@ -170,11 +172,13 @@ class TestHttpProtocolHandler(unittest.TestCase): mock_selector: mock.Mock) -> None: self._conn = mock_fromfd.return_value self.mock_selector_for_client_read(mock_selector) - flags = Flags( - auth_code=b'Basic %s' % - base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags = Proxy.initialize( + auth_code=base64.b64encode(b'user:pass')) + flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + bytes_(PLUGIN_PROXY_AUTH), + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) self.protocol_handler.initialize() @@ -202,11 +206,12 @@ class TestHttpProtocolHandler(unittest.TestCase): server.connect.return_value = True server.buffer_size.return_value = 0 - flags = Flags( - auth_code=b'Basic %s' % - base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags = Proxy.initialize( + auth_code=base64.b64encode(b'user:pass')) + flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) @@ -250,11 +255,12 @@ class TestHttpProtocolHandler(unittest.TestCase): self.mock_selector_for_client_read_read_server_write( mock_selector, server) - flags = Flags( - auth_code=b'Basic %s' % - base64.b64encode(b'user:pass')) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags = Proxy.initialize( + auth_code=base64.b64encode(b'user:pass')) + flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER) + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py index 3a21fc9e..cdb35926 100644 --- a/tests/http/test_web_server.py +++ b/tests/http/test_web_server.py @@ -15,12 +15,12 @@ import unittest import selectors from unittest import mock -from proxy.common.flags import Flags +from proxy.proxy import Proxy from proxy.core.connection import TcpClientConnection from proxy.http.handler import HttpProtocolHandler from proxy.http.parser import httpParserStates from proxy.common.utils import build_http_response, build_http_request, bytes_, text_ -from proxy.common.constants import CRLF, PROXY_PY_DIR +from proxy.common.constants import CRLF, PLUGIN_HTTP_PROXY, PLUGIN_PAC_FILE, PLUGIN_WEB_SERVER, PROXY_PY_DIR from proxy.http.server import HttpWebServerPlugin @@ -33,9 +33,11 @@ class TestWebServerPlugin(unittest.TestCase): self._addr = ('127.0.0.1', 54382) self._conn = mock_fromfd.return_value self.mock_selector = mock_selector - self.flags = Flags() - self.flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + self.flags = Proxy.initialize() + self.flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=self.flags) @@ -94,9 +96,11 @@ class TestWebServerPlugin(unittest.TestCase): fd=self._conn.fileno, events=selectors.EVENT_READ, data=None), selectors.EVENT_READ), ] - flags = Flags() - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags = Proxy.initialize() + flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) @@ -143,11 +147,13 @@ class TestWebServerPlugin(unittest.TestCase): events=selectors.EVENT_WRITE, data=None), selectors.EVENT_WRITE)], ] - flags = Flags( + flags = Proxy.initialize( enable_static_server=True, static_server_dir=static_server_dir) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), @@ -193,9 +199,11 @@ class TestWebServerPlugin(unittest.TestCase): events=selectors.EVENT_WRITE, data=None), selectors.EVENT_WRITE)], ] - flags = Flags(enable_static_server=True) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin') + flags = Proxy.initialize(enable_static_server=True) + flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), @@ -213,7 +221,7 @@ class TestWebServerPlugin(unittest.TestCase): @mock.patch('socket.fromfd') def test_on_client_connection_called_on_teardown( self, mock_fromfd: mock.Mock) -> None: - flags = Flags() + flags = Proxy.initialize() plugin = mock.MagicMock() flags.plugins = {b'HttpProtocolHandlerPlugin': [plugin]} self._conn = mock_fromfd.return_value @@ -229,10 +237,12 @@ class TestWebServerPlugin(unittest.TestCase): plugin.return_value.on_client_connection_close.assert_called() def init_and_make_pac_file_request(self, pac_file: str) -> None: - flags = Flags(pac_file=pac_file) - flags.plugins = Flags.load_plugins( - b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin,' - b'proxy.http.server.HttpWebServerPacFilePlugin') + flags = Proxy.initialize(pac_file=pac_file) + flags.plugins = Proxy.load_plugins([ + bytes_(PLUGIN_HTTP_PROXY), + bytes_(PLUGIN_WEB_SERVER), + bytes_(PLUGIN_PAC_FILE), + ]) self.protocol_handler = HttpProtocolHandler( TcpClientConnection(self._conn, self._addr), flags=flags) diff --git a/tests/http/test_websocket_client.py b/tests/http/test_websocket_client.py index 060fba10..faf18b2a 100644 --- a/tests/http/test_websocket_client.py +++ b/tests/http/test_websocket_client.py @@ -13,21 +13,26 @@ from unittest import mock from proxy.common.utils import build_websocket_handshake_response, build_websocket_handshake_request from proxy.http.websocket import WebsocketClient, WebsocketFrame -from proxy.common.constants import DEFAULT_IPV4_HOSTNAME, DEFAULT_PORT +from proxy.common.constants import DEFAULT_PORT class TestWebsocketClient(unittest.TestCase): + @mock.patch('proxy.http.websocket.client.socket.gethostbyname') @mock.patch('base64.b64encode') - @mock.patch('proxy.http.websocket.new_socket_connection') + @mock.patch('proxy.http.websocket.client.new_socket_connection') def test_handshake(self, mock_connect: mock.Mock, - mock_b64encode: mock.Mock) -> None: + mock_b64encode: mock.Mock, + mock_gethostbyname: mock.Mock) -> None: key = b'MySecretKey' mock_b64encode.return_value = key + mock_gethostbyname.return_value = '127.0.0.1' mock_connect.return_value.recv.return_value = \ build_websocket_handshake_response( WebsocketFrame.key_to_accept(key)) - _ = WebsocketClient(DEFAULT_IPV4_HOSTNAME, DEFAULT_PORT) + client = WebsocketClient(b'localhost', DEFAULT_PORT) + mock_connect.return_value.send.assert_not_called() + client.handshake() mock_connect.return_value.send.assert_called_with( build_websocket_handshake_request(key) ) diff --git a/tests/plugin/test_http_proxy_plugins.py b/tests/plugin/test_http_proxy_plugins.py index 84ca5a69..28e85896 100644 --- a/tests/plugin/test_http_proxy_plugins.py +++ b/tests/plugin/test_http_proxy_plugins.py @@ -16,7 +16,7 @@ from urllib import parse as urlparse from unittest import mock from typing import cast -from proxy.common.flags import Flags +from proxy.proxy import Proxy from proxy.core.connection import TcpClientConnection from proxy.http.handler import HttpProtocolHandler from proxy.http.proxy import HttpProxyPlugin @@ -38,7 +38,7 @@ class TestHttpProxyPluginExamples(unittest.TestCase): mock_selector: mock.Mock) -> None: self.fileno = 10 self._addr = ('127.0.0.1', 54382) - self.flags = Flags() + self.flags = Proxy.initialize() self.plugin = mock.MagicMock() self.mock_fromfd = mock_fromfd @@ -254,3 +254,30 @@ class TestHttpProxyPluginExamples(unittest.TestCase): httpStatusCodes.OK, reason=b'OK', body=b'Hello from man in the middle') ) + + @mock.patch('proxy.http.proxy.server.TcpServerConnection') + def test_filter_by_url_regex_plugin( + self, mock_server_conn: mock.Mock) -> None: + request = build_http_request( + b'GET', b'http://www.facebook.com/tr/', + headers={ + b'Host': b'www.facebook.com', + } + ) + self._conn.recv.return_value = request + self.mock_selector.return_value.select.side_effect = [ + [(selectors.SelectorKey( + fileobj=self._conn, + fd=self._conn.fileno, + events=selectors.EVENT_READ, + data=None), selectors.EVENT_READ)], ] + self.protocol_handler.run_once() + + self.assertEqual( + self.protocol_handler.client.buffer[0].tobytes(), + build_http_response( + status_code=httpStatusCodes.NOT_FOUND, + reason=b'Blocked', + headers={b'Connection': b'close'}, + ) + ) diff --git a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py index 0c1ec0a4..39311b22 100644 --- a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py +++ b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py @@ -16,10 +16,10 @@ import ssl from unittest import mock from typing import Any, cast +from proxy.proxy import Proxy from proxy.common.utils import bytes_ -from proxy.common.flags import Flags from proxy.common.utils import build_http_request, build_http_response -from proxy.core.connection import TcpClientConnection +from proxy.core.connection import TcpClientConnection, TcpServerConnection from proxy.http.codes import httpStatusCodes from proxy.http.methods import httpMethods from proxy.http.handler import HttpProtocolHandler @@ -62,7 +62,7 @@ class TestHttpProxyPluginExamplesWithTlsInterception(unittest.TestCase): self.fileno = 10 self._addr = ('127.0.0.1', 54382) - self.flags = Flags( + self.flags = Proxy.initialize( ca_cert_file='ca-cert.pem', ca_key_file='ca-key.pem', ca_signing_key_file='ca-signing-key.pem',) @@ -98,6 +98,10 @@ class TestHttpProxyPluginExamplesWithTlsInterception(unittest.TestCase): return self.server_ssl_connection return self._conn + # Do not mock the original wrap method + self.server.wrap.side_effect = \ + lambda x, y: TcpServerConnection.wrap(self.server, x, y) + self.server.has_buffer.side_effect = has_buffer type(self.server).closed = mock.PropertyMock(side_effect=closed) type( diff --git a/tests/plugin/utils.py b/tests/plugin/utils.py index 09343679..3e58ff12 100644 --- a/tests/plugin/utils.py +++ b/tests/plugin/utils.py @@ -12,7 +12,7 @@ from typing import Type from proxy.http.proxy import HttpProxyBasePlugin from proxy.plugin import ModifyPostDataPlugin, ProposedRestApiPlugin, RedirectToCustomServerPlugin, \ - FilterByUpstreamHostPlugin, CacheResponsesPlugin, ManInTheMiddlePlugin + FilterByUpstreamHostPlugin, CacheResponsesPlugin, ManInTheMiddlePlugin, FilterByURLRegexPlugin def get_plugin_by_test_name(test_name: str) -> Type[HttpProxyBasePlugin]: @@ -29,4 +29,6 @@ def get_plugin_by_test_name(test_name: str) -> Type[HttpProxyBasePlugin]: plugin = CacheResponsesPlugin elif test_name == 'test_man_in_the_middle_plugin': plugin = ManInTheMiddlePlugin + elif test_name == 'test_filter_by_url_regex_plugin': + plugin = FilterByURLRegexPlugin return plugin diff --git a/tests/test_main.py b/tests/test_main.py index c927a2a1..0e171eb0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,15 +9,13 @@ :license: BSD, see LICENSE for more details. """ import unittest -import logging import tempfile import os from unittest import mock from typing import List -from proxy.proxy import main -from proxy.common.flags import Flags +from proxy.proxy import main, Proxy from proxy.common.utils import bytes_ from proxy.http.handler import HttpProtocolHandler @@ -28,7 +26,7 @@ from proxy.common.constants import DEFAULT_ENABLE_WEB_SERVER, DEFAULT_THREADLESS from proxy.common.constants import DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE from proxy.common.constants import DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT from proxy.common.constants import DEFAULT_NUM_WORKERS, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME -from proxy.common.constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE +from proxy.common.constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, PY2_DEPRECATION_MESSAGE from proxy.common.version import __version__ @@ -70,29 +68,21 @@ class TestMain(unittest.TestCase): mock_args.enable_events = DEFAULT_ENABLE_EVENTS @mock.patch('time.sleep') - @mock.patch('proxy.proxy.Flags') + @mock.patch('proxy.proxy.Proxy.initialize') @mock.patch('proxy.proxy.AcceptorPool') @mock.patch('logging.basicConfig') def test_init_with_no_arguments( self, mock_logging_config: mock.Mock, mock_acceptor_pool: mock.Mock, - mock_flags: mock.Mock, + mock_initialize: mock.Mock, mock_sleep: mock.Mock) -> None: mock_sleep.side_effect = KeyboardInterrupt() input_args: List[str] = [] - flags = Flags.initialize(input_args=input_args) - mock_flags.initialize = lambda *args, **kwargs: flags - - main() - - mock_logging_config.assert_called_with( - level=logging.INFO, - format=DEFAULT_LOG_FORMAT - ) + main(input_args) mock_acceptor_pool.assert_called_with( - flags=flags, + flags=mock_initialize.return_value, work_klass=HttpProtocolHandler, ) mock_acceptor_pool.return_value.setup.assert_called() @@ -103,23 +93,22 @@ class TestMain(unittest.TestCase): @mock.patch('os.remove') @mock.patch('os.path.exists') @mock.patch('builtins.open') - @mock.patch('proxy.proxy.Flags.init_parser') @mock.patch('proxy.proxy.AcceptorPool') + @mock.patch('proxy.common.flag.FlagParser.parse_args') def test_pid_file_is_written_and_removed( self, + mock_parse_args: mock.Mock, mock_acceptor_pool: mock.Mock, - mock_init_parser: mock.Mock, mock_open: mock.Mock, mock_exists: mock.Mock, mock_remove: mock.Mock, mock_sleep: mock.Mock) -> None: pid_file = get_temp_file('pid') mock_sleep.side_effect = KeyboardInterrupt() - mock_args = mock_init_parser.return_value.parse_args.return_value + mock_args = mock_parse_args.return_value self.mock_default_args(mock_args) mock_args.pid_file = pid_file main(['--pid-file', pid_file]) - mock_init_parser.assert_called() mock_acceptor_pool.assert_called() mock_acceptor_pool.return_value.setup.assert_called() mock_open.assert_called_with(pid_file, 'wb') @@ -129,54 +118,46 @@ class TestMain(unittest.TestCase): mock_remove.assert_called_with(pid_file) @mock.patch('time.sleep') - @mock.patch('proxy.proxy.Flags') @mock.patch('proxy.proxy.AcceptorPool') def test_basic_auth( self, mock_acceptor_pool: mock.Mock, - mock_flags: mock.Mock, mock_sleep: mock.Mock) -> None: mock_sleep.side_effect = KeyboardInterrupt() input_args = ['--basic-auth', 'user:pass'] - flags = Flags.initialize(input_args=input_args) - mock_flags.initialize = lambda *args, **kwargs: flags + flgs = Proxy.initialize(input_args) main(input_args=input_args) - mock_acceptor_pool.assert_called_with( - flags=flags, - work_klass=HttpProtocolHandler) + mock_acceptor_pool.assert_called_once() self.assertEqual( - flags.auth_code, - b'Basic dXNlcjpwYXNz') + flgs.auth_code, + b'dXNlcjpwYXNz') @mock.patch('time.sleep') @mock.patch('builtins.print') - @mock.patch('proxy.proxy.Flags') @mock.patch('proxy.proxy.AcceptorPool') - @mock.patch('proxy.proxy.Flags.is_py3') + @mock.patch('proxy.proxy.Proxy.is_py3') def test_main_py3_runs( self, mock_is_py3: mock.Mock, mock_acceptor_pool: mock.Mock, - mock_flags: mock.Mock, mock_print: mock.Mock, mock_sleep: mock.Mock) -> None: mock_sleep.side_effect = KeyboardInterrupt() input_args = ['--basic-auth', 'user:pass'] - flags = Flags.initialize(input_args=input_args) - mock_flags.initialize = lambda *args, **kwargs: flags - mock_is_py3.return_value = True - main(num_workers=1) + + main(input_args, num_workers=1) + mock_is_py3.assert_called() mock_print.assert_not_called() - mock_acceptor_pool.assert_called() + mock_acceptor_pool.assert_called_once() mock_acceptor_pool.return_value.setup.assert_called() @mock.patch('builtins.print') - @mock.patch('proxy.proxy.Flags.is_py3') + @mock.patch('proxy.proxy.Proxy.is_py3') def test_main_py2_exit( self, mock_is_py3: mock.Mock, @@ -184,15 +165,7 @@ class TestMain(unittest.TestCase): mock_is_py3.return_value = False with self.assertRaises(SystemExit) as e: main(num_workers=1) - mock_print.assert_called_with( - 'DEPRECATION: "develop" branch no longer supports Python 2.7. Kindly upgrade to Python 3+. ' - 'If for some reasons you cannot upgrade, consider using "master" branch or simply ' - '"pip install proxy.py==0.3".' - '\n\n' - 'DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. ' - 'Please upgrade your Python as Python 2.7 won\'t be maintained after that date. ' - 'A future version of pip will drop support for Python 2.7.' - ) + mock_print.assert_called_with(PY2_DEPRECATION_MESSAGE) self.assertEqual(e.exception.code, 1) mock_is_py3.assert_called() diff --git a/tests/test_set_open_file_limit.py b/tests/test_set_open_file_limit.py index 7eddcf0f..3bae38cf 100644 --- a/tests/test_set_open_file_limit.py +++ b/tests/test_set_open_file_limit.py @@ -12,7 +12,7 @@ import os import unittest from unittest import mock -from proxy.common.flags import Flags +from proxy.proxy import Proxy if os.name != 'nt': import resource @@ -29,7 +29,7 @@ class TestSetOpenFileLimit(unittest.TestCase): self, mock_set_rlimit: mock.Mock, mock_get_rlimit: mock.Mock) -> None: - Flags.set_open_file_limit(256) + Proxy.set_open_file_limit(256) mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE) mock_set_rlimit.assert_called_with(resource.RLIMIT_NOFILE, (256, 1024)) @@ -39,7 +39,7 @@ class TestSetOpenFileLimit(unittest.TestCase): self, mock_set_rlimit: mock.Mock, mock_get_rlimit: mock.Mock) -> None: - Flags.set_open_file_limit(256) + Proxy.set_open_file_limit(256) mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE) mock_set_rlimit.assert_not_called() @@ -49,6 +49,6 @@ class TestSetOpenFileLimit(unittest.TestCase): self, mock_set_rlimit: mock.Mock, mock_get_rlimit: mock.Mock) -> None: - Flags.set_open_file_limit(1024) + Proxy.set_open_file_limit(1024) mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE) mock_set_rlimit.assert_not_called() diff --git a/tests/testing/test_embed.py b/tests/testing/test_embed.py index 6a55af26..e69c5ffe 100644 --- a/tests/testing/test_embed.py +++ b/tests/testing/test_embed.py @@ -21,7 +21,8 @@ from proxy.http.codes import httpStatusCodes from proxy.http.methods import httpMethods -@unittest.skipIf(os.name == 'nt', 'Disabled for Windows due to weird permission issues.') +@unittest.skipIf( + os.name == 'nt', 'Disabled for Windows due to weird permission issues.') class TestProxyPyEmbedded(TestCase): """This test case is a demonstration of proxy.TestCase and also serves as integration test suite for proxy.py.""" diff --git a/version-check.py b/version-check.py index c6f88b52..e783a2a3 100644 --- a/version-check.py +++ b/version-check.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ proxy.py ~~~~~~~~ @@ -8,17 +9,30 @@ :license: BSD, see LICENSE for more details. """ import sys +import subprocess from proxy.common.version import __version__ as lib_version from setup import __version__ as pkg_version # This script ensures our versions never run out of sync. # -# 1. setup.py doesn't import proxy and hence they both use -# their own respective __version__ -# 2. TODO: Version is hardcoded in homebrew stable package +# 1. TODO: Version is hardcoded in homebrew stable package # installer file, but it only needs to match with lib # versions if current git branch is master -# 3. TODO: Version is also hardcoded in README.md flags -# section + +# setup.py doesn't import proxy and hence they both use +# their own respective __version__ if lib_version != pkg_version: + print('Version mismatch found. {0} (lib) vs {1} (pkg).'.format( + lib_version, pkg_version)) + sys.exit(1) + +# Version is also hardcoded in README.md flags section +readme_version_cmd = 'cat README.md | grep "proxy.py v" | tail -2 | head -1 | cut -d " " -f 2 | cut -c2-' +readme_version_output = subprocess.check_output( + ['bash', '-c', readme_version_cmd]) +readme_version = readme_version_output.decode().strip() + +if readme_version != lib_version: + print('Version mismatch found. {0} (readme) vs {1} (lib).'.format( + readme_version, lib_version)) sys.exit(1)