Repository: abhinavsingh/proxy.py
Pull request #377 (2020-06-17 13:13:17)

v2.2.0


Patch:
diff --git a/Dockerfile b/Dockerfile
index 60f8880..fb079fc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,6 +20,9 @@ LABEL com.abhinavsingh.name="abhinavsingh/proxy.py" \
 
 COPY --from=builder /deps /usr/local
 
+# Install openssl to enable TLS interception within container
+RUN apk update && apk add openssl
+
 EXPOSE 8899/tcp
 ENTRYPOINT [ "proxy" ]
 CMD [ "--hostname=0.0.0.0" ]
diff --git a/README.md b/README.md
index d093a77..ed6f8b0 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
 [![Contributions Welcome](https://img.shields.io/static/v1?label=contributions&message=welcome%20%F0%9F%91%8D&color=green)](https://github.com/abhinavsingh/proxy.py/issues)
 [![Gitter](https://badges.gitter.im/proxy-py/community.svg)](https://gitter.im/proxy-py/community)
 
-[![Python 3.5](https://img.shields.io/static/v1?label=Python&message=3.5%20%7C%203.6%20%7C%203.7%20%7C%203.8&color=blue)](https://www.python.org/)
+[![Python 3.x](https://img.shields.io/static/v1?label=Python&message=3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9&color=blue)](https://www.python.org/)
 [![Checked with mypy](https://img.shields.io/static/v1?label=MyPy&message=checked&color=blue)](http://mypy-lang.org/)
 
 [![Become a Backer](https://opencollective.com/proxypy/tiers/backer.svg?avatarHeight=72)](https://opencollective.com/proxypy)
@@ -63,6 +63,7 @@ Table of Contents
     * [Plugin Ordering](#plugin-ordering)
 * [End-to-End Encryption](#end-to-end-encryption)
 * [TLS Interception](#tls-interception)
+    * [TLS Interception With Docker](#tls-interception-with-docker)
 * [Proxy Over SSH Tunnel](#proxy-over-ssh-tunnel)
     * [Proxy Remote Requests Locally](#proxy-remote-requests-locally)
     * [Proxy Local Requests Remotely](#proxy-local-requests-remotely)
@@ -677,7 +678,7 @@ Extend in-built Web Server to add Reverse Proxy capabilities.
 Start `proxy.py` as:
 
 ```bash
-❯ proxy \
+❯ proxy --enable-web-server \
     --plugins proxy.plugin.ReverseProxyPlugin
 ```
 
@@ -712,7 +713,7 @@ Demonstrates inbuilt web server routing using plugin.
 Start `proxy.py` as:
 
 ```bash
-❯ proxy \
+❯ proxy --enable-web-server \
     --plugins proxy.plugin.WebServerPlugin
 ```
 
@@ -777,14 +778,14 @@ TLS Interception
 =================
 
 By default, `proxy.py` will not decrypt `https` traffic between client and server.
-To enable TLS interception first generate CA certificates:
+To enable TLS interception first generate root CA certificates:
 
-```
-make ca-certificates
+```bash
+❯ make ca-certificates
 ```
 
 Lets also enable `CacheResponsePlugin` so that we can verify decrypted
-response from the server. Start `proxy.py` as:
+response from the server.  Start `proxy.py` as:
 
 ```bash
 ❯ proxy \
@@ -794,7 +795,15 @@ response from the server. Start `proxy.py` as:
     --ca-signing-key-file ca-signing-key.pem
 ```
 
-Verify using `curl -v -x localhost:8899 --cacert ca-cert.pem https://httpbin.org/get`
+
+[![NOTE](https://img.shields.io/static/v1?label=MacOS&message=note&color=yellow)](https://github.com/abhinavsingh/proxy.py#flags) Also provide explicit CA bundle path needed for validation of peer certificates. See `--ca-file` flag.
+
+
+Verify TLS interception using `curl`
+
+```bash
+❯ curl -v -x localhost:8899 --cacert ca-cert.pem https://httpbin.org/get
+```
 
 ```bash
 *  issuer: C=US; ST=CA; L=SanFrancisco; O=proxy.py; OU=CA; CN=Proxy PY CA; emailAddress=proxyca@mailserver.com
@@ -854,9 +863,94 @@ cached file instead of plain text.
 Now use CA flags with other
 [plugin examples](#plugin-examples) to see them work with `https` traffic.
 
+## TLS Interception With Docker
+
+Important notes about TLS Interception with Docker container:
+
+- Since `v2.2.0`, `proxy.py` docker container also ships with `openssl`.  This allows `proxy.py`
+to generate certificates on the fly for TLS Interception.
+
+- For security reasons, `proxy.py` docker container doesn't ship with CA certificates.
+
+Here is how to start a `proxy.py` docker container
+with TLS Interception:
+
+1. Generate CA certificates on host computer
+
+    ```bash
+    ❯ make ca-certificates
+    ```
+
+2. Copy all generated certificates into a separate directory.  We'll later mount this directory into our docker container
+
+    ```bash
+    ❯ mkdir /tmp/ca-certificates
+    ❯ cp ca-cert.pem ca-key.pem ca-signing-key.pem /tmp/ca-certificates
+    ```
+
+3. Start docker container
+
+    ```bash
+    ❯ docker run -it --rm \
+        -v /tmp/ca-certificates:/tmp/ca-certificates \
+        -p 8899:8899 \
+        abhinavsingh/proxy.py:latest \
+        --hostname 0.0.0.0 \
+        --plugins proxy.plugin.CacheResponsesPlugin \
+        --ca-key-file /tmp/ca-certificates/ca-key.pem \
+        --ca-cert-file /tmp/ca-certificates/ca-cert.pem \
+        --ca-signing-key /tmp/ca-certificates/ca-signing-key.pem
+    ```
+
+    - `-v /tmp/ca-certificates:/tmp/ca-certificates` flag mounts our CA certificate directory in container environment
+    - `--plugins proxy.plugin.CacheResponsesPlugin` enables `CacheResponsesPlugin` so that we can inspect intercepted traffic
+    - `--ca-*` flags enable TLS Interception.
+
+4. From another terminal, try TLS Interception using `curl`. You can omit `--cacert` flag if CA certificate is already trusted by the system.
+
+    ```bash
+    ❯ curl -v \
+        --cacert ca-cert.pem \
+        -x 127.0.0.1:8899 \
+        https://httpbin.org/get
+    ```
+
+5. Verify `issuer` field from response headers.
+
+    ```bash
+    * Server certificate:
+    *  subject: CN=httpbin.org; C=NA; ST=Unavailable; L=Unavailable; O=Unavailable; OU=Unavailable
+    *  start date: Jun 17 09:26:57 2020 GMT
+    *  expire date: Jun 17 09:26:57 2022 GMT
+    *  subjectAltName: host "httpbin.org" matched cert's "httpbin.org"
+    *  issuer: CN=example.com
+    *  SSL certificate verify ok.
+    ```
+
+6. Back on docker terminal, copy response dump path logs.
+
+    ```bash
+    ...[redacted]... [I] access_log:338 - 172.17.0.1:56498 - CONNECT httpbin.org:443 - 1031 bytes - 1216.70 ms
+    ...[redacted]... [I] close:49 - Cached response at /tmp/httpbin.org-ae1a927d064e4ab386ea319eb38fe251.txt
+    ```
+
+7. In another terminal, `cat` the response dump:
+
+    ```bash
+    ❯ docker exec -it $(docker ps | grep proxy.py | awk '{ print $1 }') cat /tmp/httpbin.org-ae1a927d064e4ab386ea319eb38fe251.txt
+    HTTP/1.1 200 OK
+    ...[redacted]...
+    {
+      ...[redacted]...,
+      "url": "http://httpbin.org/get"
+    }
+    ```
+
 Proxy Over SSH Tunnel
 =====================
 
+**This is a WIP and may not work as documented**
+
 Requires `paramiko` to work. See [requirements-tunnel.txt](https://github.com/abhinavsingh/proxy.py/blob/develop/requirements-tunnel.txt)
 
 ## Proxy Remote Requests Locally
@@ -1226,20 +1320,82 @@ b'GET / HTTP/1.1\r\nConnection: close\r\n\r\n'
 
 ### build_http_response
 
-TODO
+```python
+build_http_response(
+    status_code: int,
+    protocol_version: bytes = HTTP_1_1,
+    reason: Optional[bytes] = None,
+    headers: Optional[Dict[bytes, bytes]] = None,
+    body: Optional[bytes] = None) -> bytes
+```
 
 ## PKI
 
 ### API Usage
 
 #### gen_private_key
+
+```python
+gen_private_key(
+    key_path: str,
+    password: str,
+    bits: int = 2048,
+    timeout: int = 10) -> bool
+```
+
 #### gen_public_key
+
+```python
+gen_public_key(
+    public_key_path: str,
+    private_key_path: str,
+    private_key_password: str,
+    subject: str,
+    alt_subj_names: Optional[List[str]] = None,
+    extended_key_usage: Optional[str] = None,
+    validity_in_days: int = 365,
+    timeout: int = 10) -> bool
+```
+
 #### remove_passphrase
+
+```python
+remove_passphrase(
+    key_in_path: str,
+    password: str,
+    key_out_path: str,
+    timeout: int = 10) -> bool
+```
+
 #### gen_csr
+
+```python
+gen_csr(
+    csr_path: str,
+    key_path: str,
+    password: str,
+    crt_path: str,
+    timeout: int = 10) -> bool
+```
+
 #### sign_csr
 
-See [pki.py](https://github.com/abhinavsingh/proxy.py/blob/develop/proxy/common/pki.py) for
-method parameters and [test_pki.py](https://github.com/abhinavsingh/proxy.py/blob/develop/tests/common/test_pki.py)
+```python
+sign_csr(
+    csr_path: str,
+    crt_path: str,
+    ca_key_path: str,
+    ca_key_password: str,
+    ca_crt_path: str,
+    serial: str,
+    alt_subj_names: Optional[List[str]] = None,
+    extended_key_usage: Optional[str] = None,
+    validity_in_days: int = 365,
+    timeout: int = 10) -> bool
+```
+
+See [pki.py](https://github.com/abhinavsingh/proxy.py/blob/develop/proxy/common/pki.py) and
+[test_pki.py](https://github.com/abhinavsingh/proxy.py/blob/develop/tests/common/test_pki.py)
 for usage examples.
 
 ### CLI Usage
@@ -1256,7 +1412,7 @@ usage: pki.py [-h] [--password PASSWORD] [--private-key-path PRIVATE_KEY_PATH]
               [--public-key-path PUBLIC_KEY_PATH] [--subject SUBJECT]
               action
 
-proxy.py v2.1.0 : PKI Utility
+proxy.py v2.2.0 : PKI Utility
 
 positional arguments:
   action                Valid actions: remove_passphrase, gen_private_key,
@@ -1310,10 +1466,31 @@ start `proxy.py` with `--threadless` flag.
 
 ## SyntaxError: invalid syntax
 
-Make sure you are using `Python 3`. Verify the version before running `proxy.py`:
+`proxy.py` is strictly typed and uses Python `typing` annotations. Example:
+
+```python
+>>> my_strings : List[str] = []
+>>> #############^^^^^^^^^#####
+```
+
+Hence a Python version that understands typing annotations is required.
+Make sure you are using `Python 3.6+`.
+
+Verify the version before running `proxy.py`:
 
 `❯ python --version`
 
+All `typing` annotations can be replaced with `comment-only` annotations. Example:
+
+```python
+>>> my_strings = [] # List[str]
+>>> ################^^^^^^^^^^^
+```
+
+It will enable `proxy.py` to run on Python `pre-3.6`, even on `2.7`.
+However, as all future versions of Python will support `typing` annotations,
+this has not been considered.
+
 ## Unable to load plugins
 
 Make sure plugin modules are discoverable by adding them to `PYTHONPATH`.  Example:
@@ -1426,7 +1603,7 @@ usage: proxy [-h] [--backlog BACKLOG] [--basic-auth BASIC_AUTH]
              [--static-server-dir STATIC_SERVER_DIR] [--threadless]
              [--timeout TIMEOUT] [--version]
 
-proxy.py v2.1.0
+proxy.py v2.2.0
 
 optional arguments:
   -h, --help            show this help message and exit
@@ -1447,6 +1624,8 @@ optional arguments:
                         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
+  --ca-file CA_FILE     Default: None. Provide path to custom CA file for peer
+                        certificate validation. Specially useful on MacOS.
   --ca-signing-key-file CA_SIGNING_KEY_FILE
                         Default: None. CA signing key to use for dynamic
                         generation of HTTPS certificates. If used, must also
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index 2bd8e08..a28f327 100644
--- a/dashboard/package-lock.json
+++ b/dashboard/package-lock.json
@@ -24,15 +24,6 @@
         "js-tokens": "^4.0.0"
       }
     },
-    "@babel/runtime": {
-      "version": "7.0.0-rc.1",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-rc.1.tgz",
-      "integrity": "sha512-Nifv2kwP/nwR39cAOasNxzjYfpeuf/ZbZNtQz5eYxWTC9yHARU9wItFnAwz1GTZ62MU+AtSjzZPMbLK5Q9hmbg==",
-      "dev": true,
-      "requires": {
-        "regenerator-runtime": "^0.12.0"
-      }
-    },
     "@nodelib/fs.scandir": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
@@ -59,6 +50,12 @@
         "fastq": "^1.6.0"
       }
     },
+    "@types/color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
+      "dev": true
+    },
     "@types/eslint-visitor-keys": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
@@ -142,6 +139,12 @@
       "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
       "dev": true
     },
+    "@types/validator": {
+      "version": "10.11.3",
+      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-10.11.3.tgz",
+      "integrity": "sha512-GKF2VnEkMmEeEGvoo03ocrP9ySMuX1ypKazIYMlsjfslfBMhOAtC5dmEWKdJioW4lJN7MZRS88kalTsVClyQ9w==",
+      "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",
@@ -215,9 +218,9 @@
       "dev": true
     },
     "acorn": {
-      "version": "5.7.3",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
-      "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
+      "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
       "dev": true
     },
     "acorn-globals": {
@@ -228,23 +231,6 @@
       "requires": {
         "acorn": "^6.0.1",
         "acorn-walk": "^6.0.1"
-      },
-      "dependencies": {
-        "acorn": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
-          "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
-          "dev": true
-        }
-      }
-    },
-    "acorn-jsx": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-4.1.1.tgz",
-      "integrity": "sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw==",
-      "dev": true,
-      "requires": {
-        "acorn": "^5.0.3"
       }
     },
     "acorn-walk": {
@@ -302,12 +288,6 @@
         "sprintf-js": "~1.0.2"
       }
     },
-    "array-differ": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz",
-      "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=",
-      "dev": true
-    },
     "array-equal": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
@@ -330,12 +310,6 @@
       "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
       "dev": true
     },
-    "array-uniq": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
-      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
-      "dev": true
-    },
     "arrify": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
@@ -439,6 +413,12 @@
       "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
       "dev": true
     },
+    "basic-auth": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz",
+      "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=",
+      "dev": true
+    },
     "bcrypt-pbkdf": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -508,12 +488,6 @@
         "supports-color": "^5.3.0"
       }
     },
-    "chance": {
-      "version": "1.0.16",
-      "resolved": "https://registry.npmjs.org/chance/-/chance-1.0.16.tgz",
-      "integrity": "sha512-2bgDHH5bVfAXH05SPtjqrsASzZ7h90yCuYT2z4mkYpxxYvJXiIydBFzVieVHZx7wLH1Ag2Azaaej2/zA1XUrNQ==",
-      "dev": true
-    },
     "chardet": {
       "version": "0.4.2",
       "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
@@ -532,16 +506,6 @@
       "integrity": "sha512-2zOpA/bTouVt9/hhLQbaNMPVHHJH+dW1OkR/PFagCqYp2+mrPgXr1vi44BdzMOEw+qIhj7x5wg2mZFK02/30MQ==",
       "dev": true
     },
-    "class-validator": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.9.1.tgz",
-      "integrity": "sha512-3wApflrd3ywVZyx4jaasGoFt8pmo4aGLPPAEKCKCsTRWVGPilahD88q3jQjRQwja50rl9a7rsP5LAxJYwGK8/Q==",
-      "dev": true,
-      "requires": {
-        "google-libphonenumber": "^3.1.6",
-        "validator": "10.4.0"
-      }
-    },
     "cli-cursor": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
@@ -609,9 +573,9 @@
       "dev": true
     },
     "colors": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
-      "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=",
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+      "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
       "dev": true
     },
     "combined-stream": {
@@ -623,12 +587,6 @@
         "delayed-stream": "~1.0.0"
       }
     },
-    "commander": {
-      "version": "2.17.1",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
-      "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
-      "dev": true
-    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -877,37 +835,6 @@
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
       "dev": true
     },
-    "escodegen-wallaby": {
-      "version": "1.6.19",
-      "resolved": "https://registry.npmjs.org/escodegen-wallaby/-/escodegen-wallaby-1.6.19.tgz",
-      "integrity": "sha512-q+JGvR5+NR+EJBLnGAevCk5PIiIhPkUFCvcm6w9MWYtm8sv4FdGUsgzWsY6At/YHkgMyA366sjphA/JTNC8CeQ==",
-      "dev": true,
-      "requires": {
-        "esprima": "^2.7.1",
-        "estraverse": "^1.9.1",
-        "esutils": "^2.0.2",
-        "optionator": "^0.8.1",
-        "source-map": "~0.2.0"
-      },
-      "dependencies": {
-        "estraverse": {
-          "version": "1.9.3",
-          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
-          "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=",
-          "dev": true
-        },
-        "source-map": {
-          "version": "0.2.0",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
-          "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "amdefine": ">=0.0.4"
-          }
-        }
-      }
-    },
     "eslint": {
       "version": "6.5.1",
       "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.5.1.tgz",
@@ -954,9 +881,9 @@
       },
       "dependencies": {
         "acorn": {
-          "version": "7.1.0",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
-          "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
+          "version": "7.1.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+          "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
           "dev": true
         },
         "acorn-jsx": {
@@ -1275,16 +1202,6 @@
       "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
       "dev": true
     },
-    "espree": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-4.0.0.tgz",
-      "integrity": "sha512-kapdTCt1bjmspxStVKX6huolXVV5ZfyZguY1lcfhVVZstce3bqxH9mcLzNn3/mlgW6wQ732+0fuG9v7h0ZQoKg==",
-      "dev": true,
-      "requires": {
-        "acorn": "^5.6.0",
-        "acorn-jsx": "^4.1.1"
-      }
-    },
     "esprima": {
       "version": "2.7.3",
       "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
@@ -1465,9 +1382,9 @@
       "dev": true
     },
     "follow-redirects": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz",
-      "integrity": "sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A==",
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz",
+      "integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==",
       "dev": true,
       "requires": {
         "debug": "^3.0.0"
@@ -1665,9 +1582,9 @@
       }
     },
     "http-proxy": {
-      "version": "1.18.0",
-      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz",
-      "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==",
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+      "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
       "dev": true,
       "requires": {
         "eventemitter3": "^4.0.0",
@@ -1676,19 +1593,21 @@
       }
     },
     "http-server": {
-      "version": "0.11.1",
-      "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.11.1.tgz",
-      "integrity": "sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w==",
+      "version": "0.12.3",
+      "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.12.3.tgz",
+      "integrity": "sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA==",
       "dev": true,
       "requires": {
-        "colors": "1.0.3",
-        "corser": "~2.0.0",
-        "ecstatic": "^3.0.0",
-        "http-proxy": "^1.8.1",
-        "opener": "~1.4.0",
-        "optimist": "0.6.x",
-        "portfinder": "^1.0.13",
-        "union": "~0.4.3"
+        "basic-auth": "^1.0.3",
+        "colors": "^1.4.0",
+        "corser": "^2.0.1",
+        "ecstatic": "^3.3.2",
+        "http-proxy": "^1.18.0",
+        "minimist": "^1.2.5",
+        "opener": "^1.5.1",
+        "portfinder": "^1.0.25",
+        "secure-compare": "3.0.1",
+        "union": "~0.5.0"
       }
     },
     "http-signature": {
@@ -1797,12 +1716,6 @@
         }
       }
     },
-    "inversify": {
-      "version": "4.13.0",
-      "resolved": "https://registry.npmjs.org/inversify/-/inversify-4.13.0.tgz",
-      "integrity": "sha512-O5d8y7gKtyRwrvTLZzYET3kdFjqUy58sGpBYMARF13mzqDobpfBXVOPLH7HmnD2VR6Q+1HzZtslGvsdQfeb0SA==",
-      "dev": true
-    },
     "invert-kv": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
@@ -1964,47 +1877,268 @@
       }
     },
     "javascript-obfuscator": {
-      "version": "0.18.1",
-      "resolved": "https://registry.npmjs.org/javascript-obfuscator/-/javascript-obfuscator-0.18.1.tgz",
-      "integrity": "sha512-pQ2DyRV4j0neaWdII1S7iJftCyks9H7afVkQRSE4gslkqpeqyM1DE0eapsZKHR0BnYvw3tPU+Ky+j4yhzcxRZA==",
-      "dev": true,
-      "requires": {
-        "@babel/runtime": "7.0.0-rc.1",
-        "chalk": "2.4.1",
-        "chance": "1.0.16",
-        "class-validator": "0.9.1",
-        "commander": "2.17.1",
-        "escodegen-wallaby": "1.6.19",
-        "espree": "4.0.0",
-        "estraverse": "4.2.0",
-        "inversify": "4.13.0",
+      "version": "0.18.8",
+      "resolved": "https://registry.npmjs.org/javascript-obfuscator/-/javascript-obfuscator-0.18.8.tgz",
+      "integrity": "sha512-lh/PT3dwLgv2vP9ymsmNZqjpbkDq1SQqpxLYtAerLzmYgHlQOzVpUe3NbFb8tUFNylMeg6ez1iEpA7oOQw/7Tw==",
+      "dev": true,
+      "requires": {
+        "@babel/runtime": "7.7.4",
+        "chalk": "3.0.0",
+        "chance": "1.1.3",
+        "class-validator": "0.11.0",
+        "commander": "4.0.1",
+        "escodegen-wallaby": "1.6.27",
+        "espree": "^6.1.2",
+        "estraverse": "4.3.0",
+        "eventemitter3": "4.0.0",
+        "inversify": "5.0.1",
         "js-string-escape": "1.0.1",
         "md5": "2.2.1",
         "mkdirp": "0.5.1",
-        "multimatch": "2.1.0",
+        "multimatch": "4.0.0",
         "opencollective": "1.0.3",
-        "reflect-metadata": "0.1.12",
-        "source-map-support": "0.5.8",
+        "reflect-metadata": "0.1.13",
+        "source-map-support": "0.5.16",
         "string-template": "1.0.0",
-        "tslib": "1.9.3"
+        "tslib": "1.10.0"
       },
       "dependencies": {
+        "@babel/runtime": {
+          "version": "7.7.4",
+          "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz",
+          "integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==",
+          "dev": true,
+          "requires": {
+            "regenerator-runtime": "^0.13.2"
+          }
+        },
+        "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.2.0",
+          "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
+          "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+          "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"
+          }
+        },
+        "array-differ": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz",
+          "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==",
+          "dev": true
+        },
+        "arrify": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+          "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+          "dev": true
+        },
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "chance": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.3.tgz",
+          "integrity": "sha512-XeJsdoVAzDb1WRPRuMBesRSiWpW1uNTo5Fd7mYxPJsAfgX71+jfuCOHOdbyBz2uAUZ8TwKcXgWk3DMedFfJkbg==",
+          "dev": true
+        },
+        "class-validator": {
+          "version": "0.11.0",
+          "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.11.0.tgz",
+          "integrity": "sha512-niAmmSPFku9xsnpYYrddy8NZRrCX3yyoZ/rgPKOilE5BG0Ma1eVCIxpR4X0LasL/6BzbYzsutG+mSbAXlh4zNw==",
+          "dev": true,
+          "requires": {
+            "@types/validator": "10.11.3",
+            "google-libphonenumber": "^3.1.6",
+            "validator": "12.0.0"
+          }
+        },
+        "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
+        },
+        "commander": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.1.tgz",
+          "integrity": "sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==",
+          "dev": true
+        },
+        "escodegen-wallaby": {
+          "version": "1.6.27",
+          "resolved": "https://registry.npmjs.org/escodegen-wallaby/-/escodegen-wallaby-1.6.27.tgz",
+          "integrity": "sha512-DFXbfLTNa/wiQRgqSseKhmzyDhwWJ4c/6IwsdAgA6qYL0ntsmSuuTMovEbA/8oCanSGkUR1qaLsDgKiWPllnjg==",
+          "dev": true,
+          "requires": {
+            "esprima": "^2.7.1",
+            "estraverse": "^1.9.1",
+            "esutils": "^2.0.2",
+            "optionator": "^0.8.1",
+            "source-map": "~0.2.0"
+          },
+          "dependencies": {
+            "estraverse": {
+              "version": "1.9.3",
+              "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
+              "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=",
+              "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"
+          }
+        },
+        "estraverse": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+          "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+          "dev": true
+        },
+        "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
+        },
+        "inversify": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.1.tgz",
+          "integrity": "sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ==",
+          "dev": true
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+          "dev": true
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+          "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+          "dev": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "multimatch": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz",
+          "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==",
+          "dev": true,
+          "requires": {
+            "@types/minimatch": "^3.0.3",
+            "array-differ": "^3.0.0",
+            "array-union": "^2.1.0",
+            "arrify": "^2.0.1",
+            "minimatch": "^3.0.4"
+          }
+        },
+        "reflect-metadata": {
+          "version": "0.1.13",
+          "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
+          "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==",
+          "dev": true
+        },
+        "regenerator-runtime": {
+          "version": "0.13.5",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
+          "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
+          "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        },
         "source-map-support": {
-          "version": "0.5.8",
-          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.8.tgz",
-          "integrity": "sha512-WqAEWPdb78u25RfKzOF0swBpY0dKrNdjc4GvLwm7ScX/o9bj8Eh/YL8mcMhBHYDGl87UkkSXDOFnW4G7GhWhGg==",
+          "version": "0.5.16",
+          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
+          "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
           "dev": true,
           "requires": {
             "buffer-from": "^1.0.0",
             "source-map": "^0.6.0"
+          },
+          "dependencies": {
+            "source-map": {
+              "version": "0.6.1",
+              "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+              "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+              "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"
+          }
+        },
+        "tslib": {
+          "version": "1.10.0",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
+          "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
+          "dev": true
+        },
+        "validator": {
+          "version": "12.0.0",
+          "resolved": "https://registry.npmjs.org/validator/-/validator-12.0.0.tgz",
+          "integrity": "sha512-r5zA1cQBEOgYlesRmSEwc9LkbfNLTtji+vWyaHzRZUxCTHdsX3bd+sdHfs5tGZ2W6ILGGsxWxCNwT/h3IY/3ng==",
+          "dev": true
         }
       }
     },
     "jquery": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
-      "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==",
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz",
+      "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==",
       "dev": true
     },
     "js-cookie": {
@@ -2084,9 +2218,9 @@
       },
       "dependencies": {
         "acorn": {
-          "version": "7.1.0",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
-          "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
+          "version": "7.1.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+          "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
           "dev": true
         },
         "escodegen": {
@@ -2303,26 +2437,18 @@
       }
     },
     "minimist": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
-      "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
       "dev": true
     },
     "mkdirp": {
-      "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
       "dev": true,
       "requires": {
-        "minimist": "0.0.8"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "0.0.8",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
-          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
-          "dev": true
-        }
+        "minimist": "^1.2.5"
       }
     },
     "ms": {
@@ -2331,29 +2457,6 @@
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
       "dev": true
     },
-    "multimatch": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz",
-      "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=",
-      "dev": true,
-      "requires": {
-        "array-differ": "^1.0.0",
-        "array-union": "^1.0.1",
-        "arrify": "^1.0.0",
-        "minimatch": "^3.0.0"
-      },
-      "dependencies": {
-        "array-union": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
-          "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
-          "dev": true,
-          "requires": {
-            "array-uniq": "^1.0.1"
-          }
-        }
-      }
-    },
     "mute-stream": {
       "version": "0.0.7",
       "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
@@ -2508,6 +2611,12 @@
             "supports-color": "^2.0.0"
           }
         },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
         "supports-color": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
@@ -2517,9 +2626,9 @@
       }
     },
     "opener": {
-      "version": "1.4.3",
-      "resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
-      "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=",
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
+      "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
       "dev": true
     },
     "opn": {
@@ -2532,30 +2641,6 @@
         "pinkie-promise": "^2.0.0"
       }
     },
-    "optimist": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
-      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
-      "dev": true,
-      "requires": {
-        "minimist": "~0.0.1",
-        "wordwrap": "~0.0.2"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "0.0.10",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
-          "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
-          "dev": true
-        },
-        "wordwrap": {
-          "version": "0.0.3",
-          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
-          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
-          "dev": true
-        }
-      }
-    },
     "optionator": {
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
@@ -2723,9 +2808,9 @@
       "dev": true
     },
     "portfinder": {
-      "version": "1.0.25",
-      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",
-      "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==",
+      "version": "1.0.26",
+      "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz",
+      "integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==",
       "dev": true,
       "requires": {
         "async": "^2.6.2",
@@ -2764,9 +2849,9 @@
       "dev": true
     },
     "qs": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
-      "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=",
+      "version": "6.9.4",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
+      "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==",
       "dev": true
     },
     "read-pkg": {
@@ -2790,18 +2875,6 @@
         "read-pkg": "^2.0.0"
       }
     },
-    "reflect-metadata": {
-      "version": "0.1.12",
-      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz",
-      "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==",
-      "dev": true
-    },
-    "regenerator-runtime": {
-      "version": "0.12.1",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
-      "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==",
-      "dev": true
-    },
     "regexpp": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
@@ -2962,9 +3035,9 @@
       },
       "dependencies": {
         "acorn": {
-          "version": "7.1.0",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
-          "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
+          "version": "7.1.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+          "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
           "dev": true
         }
       }
@@ -3061,6 +3134,12 @@
         "xmlchars": "^2.1.1"
       }
     },
+    "secure-compare": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
+      "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=",
+      "dev": true
+    },
     "semver": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -3455,12 +3534,12 @@
       "dev": true
     },
     "union": {
-      "version": "0.4.6",
-      "resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
-      "integrity": "sha1-GY+9rrolTniLDvy2MLwR8kopWeA=",
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
+      "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
       "dev": true,
       "requires": {
-        "qs": "~2.3.3"
+        "qs": "^6.4.0"
       }
     },
     "universalify": {
@@ -3506,12 +3585,6 @@
         "spdx-expression-parse": "^3.0.0"
       }
     },
-    "validator": {
-      "version": "10.4.0",
-      "resolved": "https://registry.npmjs.org/validator/-/validator-10.4.0.tgz",
-      "integrity": "sha512-Q/wBy3LB1uOyssgNlXSRmaf22NxjvDNZM2MtIQ4jaEOAB61xsh1TQxsq1CgzUMBV1lDrVMogIh8GjG1DYW0zLg==",
-      "dev": true
-    },
     "verror": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
diff --git a/dashboard/package.json b/dashboard/package.json
index 48ddf26..0f63c92 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -37,10 +37,10 @@
     "eslint-plugin-node": "^10.0.0",
     "eslint-plugin-promise": "^4.2.1",
     "eslint-plugin-standard": "^4.0.1",
-    "http-server": "^0.11.1",
+    "http-server": "^0.12.3",
     "jasmine": "^3.5.0",
     "jasmine-ts": "^0.3.0",
-    "jquery": "^3.4.1",
+    "jquery": "^3.5.0",
     "js-cookie": "^2.2.1",
     "jsdom": "^15.2.0",
     "ncp": "^2.0.0",
diff --git a/helper/chrome_with_proxy.sh b/helper/chrome_with_proxy.sh
index fb4760b..2e04b60 100755
--- a/helper/chrome_with_proxy.sh
+++ b/helper/chrome_with_proxy.sh
@@ -20,6 +20,6 @@ fi
   --no-first-run \
   --no-default-browser-check \
   --user-data-dir="$(mktemp -d -t 'chrome-remote_data_dir')" \
-  --proxy-server=${PROXY_PY_ADDR} \
-  --ignore-urlfetcher-cert-requests \
-  --ignore-certificate-errors
+  --proxy-server=${PROXY_PY_ADDR} #\
+#  --ignore-urlfetcher-cert-requests \
+#  --ignore-certificate-errors
diff --git a/helper/homebrew/develop/proxy.rb b/helper/homebrew/develop/proxy.rb
index 36d9bb2..d695e53 100644
--- a/helper/homebrew/develop/proxy.rb
+++ b/helper/homebrew/develop/proxy.rb
@@ -10,8 +10,8 @@ class Proxy < Formula
   depends_on "python"
 
   resource "typing-extensions" do
-    url "https://files.pythonhosted.org/packages/e7/dd/f1713bc6638cc3a6a23735eff6ee09393b44b96176d3296693ada272a80b/typing_extensions-3.7.4.1.tar.gz"
-    sha256 "091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2"
+    url "https://files.pythonhosted.org/packages/6a/28/d32852f2af6b5ead85d396249d5bdf450833f3a69896d76eb480d9c5e406/typing_extensions-3.7.4.2.tar.gz"
+    sha256 "79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"
   end
 
   def install
diff --git a/helper/homebrew/stable/proxy.rb b/helper/homebrew/stable/proxy.rb
index c13d221..83b38cb 100644
--- a/helper/homebrew/stable/proxy.rb
+++ b/helper/homebrew/stable/proxy.rb
@@ -5,13 +5,13 @@ class Proxy < Formula
     Network monitoring, controls & Application development, testing, debugging."
   homepage "https://github.com/abhinavsingh/proxy.py"
   url "https://github.com/abhinavsingh/proxy.py/archive/master.zip"
-  version "2.0.0"
+  version "2.2.0"
 
   depends_on "python"
 
   resource "typing-extensions" do
-    url "https://files.pythonhosted.org/packages/e7/dd/f1713bc6638cc3a6a23735eff6ee09393b44b96176d3296693ada272a80b/typing_extensions-3.7.4.1.tar.gz"
-    sha256 "091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2"
+    url "https://files.pythonhosted.org/packages/6a/28/d32852f2af6b5ead85d396249d5bdf450833f3a69896d76eb480d9c5e406/typing_extensions-3.7.4.2.tar.gz"
+    sha256 "79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"
   end
 
   def install
diff --git a/proxy/benchmark/__init__.py b/proxy/benchmark/__init__.py
deleted file mode 100644
index 8095cf9..0000000
--- a/proxy/benchmark/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-# -*- 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.
-"""
diff --git a/proxy/benchmark/benchmark.py b/proxy/benchmark/benchmark.py
deleted file mode 100644
index c12f74b..0000000
--- a/proxy/benchmark/benchmark.py
+++ /dev/null
@@ -1,128 +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 asyncio
-import time
-from typing import List, Tuple
-
-from ..common.constants import DEFAULT_BUFFER_SIZE
-from ..common.utils import build_http_request
-from ..http.methods import httpMethods
-from ..http.parser import httpParserStates, httpParserTypes, HttpParser
-
-__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
-
-DEFAULT_N = 1
-
-
-class Benchmark:
-
-    def __init__(self, n: int = DEFAULT_N) -> None:
-        self.n = n
-        self.clients: List[Tuple[asyncio.StreamReader,
-                                 asyncio.StreamWriter]] = []
-
-    async def open_connections(self) -> None:
-        for _ in range(self.n):
-            self.clients.append(await asyncio.open_connection('::', 8899))
-        print('Opened ' + str(self.n) + ' connections')
-
-    @staticmethod
-    async def send(writer: asyncio.StreamWriter) -> None:
-        try:
-            while True:
-                writer.write(build_http_request(
-                    httpMethods.GET, b'/'
-                ))
-                await asyncio.sleep(0.01)
-        except KeyboardInterrupt:
-            pass
-
-    @staticmethod
-    def parse_pipeline_response(response: HttpParser, raw: bytes, counter: int = 0) -> \
-            Tuple[HttpParser, int]:
-        response.parse(raw)
-        if response.state != httpParserStates.COMPLETE:
-            # Need more data
-            return response, counter
-
-        if response.buffer == b'':
-            # No more buffer left to parse
-            return response, counter + 1
-
-        # For pipelined requests we may have pending buffer, try parse them as
-        # responses
-        pipelined_response = HttpParser(httpParserTypes.RESPONSE_PARSER)
-        return Benchmark.parse_pipeline_response(
-            pipelined_response, response.buffer, counter + 1)
-
-    @staticmethod
-    async def recv(idd: int, reader: asyncio.StreamReader) -> None:
-        print_every = 1000
-        last_print = time.time()
-        num_completed_requests: int = 0
-        response = HttpParser(httpParserTypes.RESPONSE_PARSER)
-        try:
-            while True:
-                raw = await reader.read(DEFAULT_BUFFER_SIZE)
-                response, total_parsed = Benchmark.parse_pipeline_response(
-                    response, raw)
-                if response.state == httpParserStates.COMPLETE:
-                    response = HttpParser(httpParserTypes.RESPONSE_PARSER)
-                if total_parsed > 0:
-                    num_completed_requests += total_parsed
-                    # print('total parsed %d' % total_parsed)
-                    if num_completed_requests % print_every == 0:
-                        now = time.time()
-                        print('[%d] Completed last %d requests in %.2f secs' %
-                              (idd, print_every, now - last_print))
-                        last_print = now
-        except KeyboardInterrupt:
-            pass
-
-    async def close_connections(self) -> None:
-        for reader, writer in self.clients:
-            writer.close()
-            await writer.wait_closed()
-        print('Closed ' + str(self.n) + ' connections')
-
-    async def run(self) -> None:
-        try:
-            await self.open_connections()
-            print('Exchanging request / response packets')
-            readers = []
-            writers = []
-            idd = 0
-            for reader, writer in self.clients:
-                readers.append(
-                    asyncio.create_task(
-                        self.recv(idd, reader)
-                    )
-                )
-                writers.append(
-                    asyncio.create_task(
-                        self.send(writer)
-                    )
-                )
-                idd += 1
-            await asyncio.gather(*(readers + writers))
-        finally:
-            try:
-                await self.close_connections()
-            except RuntimeError:
-                pass
-
-
-def main() -> None:
-    benchmark = Benchmark(n=DEFAULT_N)
-    try:
-        asyncio.run(benchmark.run())
-    except KeyboardInterrupt:
-        pass
diff --git a/proxy/common/constants.py b/proxy/common/constants.py
index 72efbce..1479523 100644
--- a/proxy/common/constants.py
+++ b/proxy/common/constants.py
@@ -10,6 +10,7 @@
 """
 import os
 import time
+import pathlib
 import ipaddress
 
 from typing import List
@@ -44,6 +45,7 @@ DEFAULT_CA_CERT_FILE = None
 DEFAULT_CA_KEY_FILE = None
 DEFAULT_CA_SIGNING_KEY_FILE = None
 DEFAULT_CERT_FILE = None
+DEFAULT_CA_FILE = None
 DEFAULT_CLIENT_RECVBUF_SIZE = DEFAULT_BUFFER_SIZE
 DEFAULT_DEVTOOLS_WS_PATH = b'/devtools'
 DEFAULT_DISABLE_HEADERS: List[bytes] = []
@@ -73,3 +75,14 @@ DEFAULT_THREADLESS = False
 DEFAULT_TIMEOUT = 10
 DEFAULT_VERSION = False
 DEFAULT_HTTP_PORT = 80
+DEFAULT_MAX_SEND_SIZE = 16 * 1024
+
+DEFAULT_DATA_DIRECTORY_PATH = os.path.join(str(pathlib.Path.home()), '.proxy')
+
+# Cor plugins enabled by default or via flags
+PLUGIN_HTTP_PROXY = 'proxy.http.proxy.HttpProxyPlugin'
+PLUGIN_WEB_SERVER = 'proxy.http.server.HttpWebServerPlugin'
+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'
diff --git a/proxy/common/flags.py b/proxy/common/flags.py
index 30aaf6f..182c2c3 100644
--- a/proxy/common/flags.py
+++ b/proxy/common/flags.py
@@ -20,7 +20,6 @@ import socket
 import multiprocessing
 import sys
 import inspect
-import pathlib
 
 from typing import Optional, Union, Dict, List, TypeVar, Type, cast, Any, Tuple
 
@@ -28,12 +27,14 @@ 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
+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, COMMA, DOT
+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'
@@ -49,9 +50,6 @@ T = TypeVar('T', bound='Flags')
 class Flags:
     """Contains all input flags and inferred input parameters."""
 
-    ROOT_DATA_DIR_NAME = '.proxy.py'
-    GENERATED_CERTS_DIR_NAME = 'certificates'
-
     def __init__(
             self,
             auth_code: Optional[bytes] = DEFAULT_BASIC_AUTH,
@@ -67,6 +65,7 @@ class Flags:
             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,
@@ -98,6 +97,7 @@ class Flags:
         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
@@ -110,16 +110,25 @@ class Flags:
         self.devtools_ws_path: bytes = devtools_ws_path
         self.enable_events: bool = enable_events
 
-        self.proxy_py_data_dir = os.path.join(
-            str(pathlib.Path.home()), self.ROOT_DATA_DIR_NAME)
+        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, self.GENERATED_CERTS_DIR_NAME)
+                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],
@@ -136,51 +145,59 @@ class Flags:
                 'A future version of pip will drop support for Python 2.7.')
             sys.exit(1)
 
-        args = Flags.init_parser().parse_args(input_args)
+        parser = Flags.init_parser()
+        args = parser.parse_args(input_args)
 
+        # Print version and exit
         if args.version:
             print(__version__)
             sys.exit(0)
 
-        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)
-
-        auth_code = None
-        if args.basic_auth:
-            auth_code = b'Basic %s' % base64.b64encode(bytes_(args.basic_auth))
-
+        # Setup logging module
         Flags.setup_logger(args.log_file, args.log_level, args.log_format)
-        Flags.set_open_file_limit(args.open_file_limit)
 
-        http_proxy_plugin = 'proxy.http.proxy.HttpProxyPlugin'
-        web_server_plugin = 'proxy.http.server.HttpWebServerPlugin'
-        pac_file_plugin = 'proxy.http.server.HttpWebServerPacFilePlugin'
-        devtools_protocol_plugin = 'proxy.http.inspector.DevtoolsProtocolPlugin'
-        dashboard_plugin = 'proxy.dashboard.dashboard.ProxyDashboard'
-        inspect_traffic_plugin = 'proxy.dashboard.inspect_traffic.InspectTrafficPlugin'
+        # Setup limits
+        Flags.set_open_file_limit(args.open_file_limit)
 
         default_plugins: List[Tuple[str, bool]] = []
         if args.enable_dashboard:
-            default_plugins.append((web_server_plugin, True))
+            default_plugins.append((PLUGIN_WEB_SERVER, True))
             args.enable_static_server = True
-            default_plugins.append((dashboard_plugin, True))
-            default_plugins.append((inspect_traffic_plugin, 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((devtools_protocol_plugin, True))
-            default_plugins.append((web_server_plugin, True))
+            default_plugins.append((PLUGIN_DEVTOOLS_PROTOCOL, True))
+            default_plugins.append((PLUGIN_WEB_SERVER, True))
         if not args.disable_http_proxy:
-            default_plugins.append((http_proxy_plugin, True))
+            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((web_server_plugin, True))
+            default_plugins.append((PLUGIN_WEB_SERVER, True))
         if args.pac_file is not None:
-            default_plugins.append((pac_file_plugin, True))
+            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)),
@@ -223,6 +240,11 @@ class Flags:
                 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))),
@@ -251,23 +273,9 @@ class Flags:
                 opts.get(
                     'enable_events',
                     args.enable_events)),
-            plugins=Flags.load_plugins(
-                bytes_(
-                    '%s,%s' %
-                    (text_(COMMA).join(collections.OrderedDict(default_plugins).keys()),
-                     opts.get('plugins', args.plugins)))),
+            plugins=plugins,
             pid_file=cast(Optional[str], opts.get('pid_file', args.pid_file)))
 
-    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
-
     @staticmethod
     def init_parser() -> argparse.ArgumentParser:
         """Initializes and returns argument parser."""
@@ -308,6 +316,13 @@ class Flags:
             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,
diff --git a/proxy/common/pki.py b/proxy/common/pki.py
index 0f1030b..6e6e512 100644
--- a/proxy/common/pki.py
+++ b/proxy/common/pki.py
@@ -39,7 +39,7 @@ countryName_min			= 2
 countryName_max			= 2
 stateOrProvinceName		= State or Province Name (full name)
 localityName			= Locality Name (eg, city)
-0.organizationName		= Organization Name (eg, company)
+organizationName		= Organization Name (eg, company)
 organizationalUnitName	= Organizational Unit Name (eg, section)
 commonName			    = Common Name (eg, fully qualified host name)
 commonName_max			= 64
diff --git a/proxy/common/version.py b/proxy/common/version.py
index 913d804..e8af269 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, 1, 2)
+VERSION = (2, 2, 0)
 __version__ = '.'.join(map(str, VERSION[0:3]))
diff --git a/proxy/core/connection/connection.py b/proxy/core/connection/connection.py
index 29c15c1..3aa72ee 100644
--- a/proxy/core/connection/connection.py
+++ b/proxy/core/connection/connection.py
@@ -14,7 +14,7 @@ import logging
 from abc import ABC, abstractmethod
 from typing import NamedTuple, Optional, Union, List
 
-from ...common.constants import DEFAULT_BUFFER_SIZE
+from ...common.constants import DEFAULT_BUFFER_SIZE, DEFAULT_MAX_SEND_SIZE
 
 logger = logging.getLogger(__name__)
 
@@ -82,11 +82,11 @@ class TcpConnection(ABC):
         """Users must handle BrokenPipeError exceptions"""
         if not self.has_buffer():
             return 0
-        mv = self.buffer[0]
-        sent: int = self.send(mv.tobytes())
+        mv = self.buffer[0].tobytes()
+        sent: int = self.send(mv[:DEFAULT_MAX_SEND_SIZE])
         if sent == len(mv):
             self.buffer.pop(0)
         else:
-            self.buffer[0] = memoryview(mv.tobytes()[sent:])
+            self.buffer[0] = memoryview(mv[sent:])
         logger.debug('flushed %d bytes to %s' % (sent, self.tag))
         return sent
diff --git a/proxy/core/threadless.py b/proxy/core/threadless.py
index d13f973..87be7e5 100644
--- a/proxy/core/threadless.py
+++ b/proxy/core/threadless.py
@@ -9,7 +9,6 @@
     :license: BSD, see LICENSE for more details.
 """
 import os
-import uuid
 import socket
 import logging
 import asyncio
@@ -21,6 +20,7 @@ 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 .connection import TcpClientConnection
 from .event import EventQueue, eventNames
@@ -41,11 +41,11 @@ class ThreadlessWork(ABC):
             client: TcpClientConnection,
             flags: Optional[Flags],
             event_queue: Optional[EventQueue] = None,
-            uid: Optional[str] = None) -> None:
+            uid: Optional[UUID] = None) -> None:
         self.client = client
         self.flags = flags if flags else Flags()
         self.event_queue = event_queue
-        self.uid: str = uid if uid is not None else uuid.uuid4().hex
+        self.uid: UUID = uid if uid is not None else uuid4()
 
     @abstractmethod
     def initialize(self) -> None:
@@ -80,7 +80,7 @@ class ThreadlessWork(ABC):
             return
         assert self.event_queue
         self.event_queue.publish(
-            self.uid,
+            self.uid.hex,
             event_name,
             event_payload,
             publisher_id
diff --git a/proxy/http/handler.py b/proxy/http/handler.py
index a19a2b6..9c7dd90 100644
--- a/proxy/http/handler.py
+++ b/proxy/http/handler.py
@@ -17,7 +17,7 @@ import errno
 import logging
 from abc import ABC, abstractmethod
 from typing import Tuple, List, Union, Optional, Generator, Dict
-
+from uuid import UUID
 from .parser import HttpParser, httpParserStates, httpParserTypes
 from .exception import HttpProtocolException
 
@@ -54,12 +54,12 @@ class HttpProtocolHandlerPlugin(ABC):
 
     def __init__(
             self,
-            uid: str,
+            uid: UUID,
             flags: Flags,
             client: TcpClientConnection,
             request: HttpParser,
             event_queue: EventQueue):
-        self.uid: str = uid
+        self.uid: UUID = uid
         self.flags: Flags = flags
         self.client: TcpClientConnection = client
         self.request: HttpParser = request
@@ -116,7 +116,7 @@ class HttpProtocolHandler(ThreadlessWork):
     def __init__(self, client: TcpClientConnection,
                  flags: Optional[Flags] = None,
                  event_queue: Optional[EventQueue] = None,
-                 uid: Optional[str] = None):
+                 uid: Optional[UUID] = None):
         super().__init__(client, flags, event_queue, uid)
 
         self.start_time: float = time.time()
@@ -288,13 +288,13 @@ class HttpProtocolHandler(ThreadlessWork):
 
             try:
                 self.client.flush()
-            except OSError:
-                logger.error('OSError when flushing buffer to client')
-                return True
             except BrokenPipeError:
                 logger.error(
                     'BrokenPipeError when flushing buffer for client')
                 return True
+            except OSError:
+                logger.error('OSError when flushing buffer to client')
+                return True
         return False
 
     def handle_readables(self, readables: List[Union[int, HasFileno]]) -> bool:
@@ -359,7 +359,7 @@ class HttpProtocolHandler(ThreadlessWork):
             except HttpProtocolException as e:
                 logger.debug(
                     'HttpProtocolException type raised')
-                response = e.response(self.request)
+                response: Optional[memoryview] = e.response(self.request)
                 if response:
                     self.client.queue(response)
                 return True
diff --git a/proxy/http/parser.py b/proxy/http/parser.py
index bdd2683..63c62b8 100644
--- a/proxy/http/parser.py
+++ b/proxy/http/parser.py
@@ -105,19 +105,21 @@ class HttpParser:
             self.del_header(key.lower())
 
     def set_url(self, url: bytes) -> None:
+        # Work around with urlsplit semantics.
+        #
+        # 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':
+            url = b'https://' + url
         self.url = urlparse.urlsplit(url)
         self.set_line_attributes()
 
     def set_line_attributes(self) -> None:
         if self.type == httpParserTypes.REQUEST_PARSER:
             if self.method == httpMethods.CONNECT and self.url:
-                if self.url.scheme == b'':
-                    u = urlparse.urlsplit(b'//' + self.url.path)
-                    self.host, self.port = u.hostname, u.port
-                else:
-                    self.host = self.url.scheme
-                    self.port = 443 if self.url.path == b'' else \
-                        int(self.url.path)
+                self.host = self.url.hostname
+                self.port = 443 if self.url.port is None else self.url.port
             elif self.url:
                 self.host, self.port = self.url.hostname, self.url.port \
                     if self.url.port else DEFAULT_HTTP_PORT
@@ -125,7 +127,7 @@ class HttpParser:
                 raise KeyError(
                     'Invalid request. Method: %r, Url: %r' %
                     (self.method, self.url))
-            self.path = self.build_url()
+            self.path = self.build_path()
 
     def is_chunked_encoded(self) -> bool:
         return b'transfer-encoding' in self.headers and \
@@ -222,7 +224,7 @@ class HttpParser:
         value = COLON.join(parts[1:]).strip()
         self.add_headers([(key, value)])
 
-    def build_url(self) -> bytes:
+    def build_path(self) -> bytes:
         if not self.url:
             return b'/None'
         url = self.url.path
@@ -256,3 +258,8 @@ class HttpParser:
         return self.version == HTTP_1_1 and \
             (not self.has_header(b'Connection') or
              self.header(b'Connection').lower() == b'keep-alive')
+
+    def is_connection_upgrade(self) -> bool:
+        return self.version == HTTP_1_1 and \
+            self.has_header(b'Connection') and \
+            self.has_header(b'Upgrade')
diff --git a/proxy/http/proxy/plugin.py b/proxy/http/proxy/plugin.py
index 63fda08..44129ed 100644
--- a/proxy/http/proxy/plugin.py
+++ b/proxy/http/proxy/plugin.py
@@ -10,7 +10,7 @@
 """
 from abc import ABC, abstractmethod
 from typing import Optional
-
+from uuid import UUID
 from ..parser import HttpParser
 
 from ...common.flags import Flags
@@ -26,7 +26,7 @@ class HttpProxyBasePlugin(ABC):
 
     def __init__(
             self,
-            uid: str,
+            uid: UUID,
             flags: Flags,
             client: TcpClientConnection,
             event_queue: EventQueue) -> None:
diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py
index 229187c..5ce2948 100644
--- a/proxy/http/proxy/server.py
+++ b/proxy/http/proxy/server.py
@@ -8,14 +8,13 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
+import logging
 import threading
-import subprocess
 import os
 import ssl
 import socket
 import time
 import errno
-import logging
 from typing import Optional, List, Union, Dict, cast, Any, Tuple
 
 from .plugin import HttpProxyBasePlugin
@@ -28,6 +27,7 @@ from ..methods import httpMethods
 from ...common.types import HasFileno
 from ...common.constants import PROXY_AGENT_HEADER_VALUE
 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
@@ -89,13 +89,16 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
             logger.debug('Server is write ready, flushing buffer')
             try:
                 self.server.flush()
-            except OSError:
-                logger.error('OSError when flushing buffer to server')
-                return True
+            except ssl.SSLWantWriteError:
+                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)
+                return True
         return False
 
     def read_from_descriptors(self, r: List[Union[int, HasFileno]]) -> bool:
@@ -207,9 +210,18 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
             if self.request.state == httpParserStates.COMPLETE and (
                     self.request.method != httpMethods.CONNECT or
                     self.flags.tls_interception_enabled()):
+                if self.pipeline_request is not None and \
+                        self.pipeline_request.is_connection_upgrade():
+                    # Previous pipelined request was a WebSocket
+                    # upgrade request. Incoming client data now
+                    # must be treated as WebSocket protocol packets.
+                    self.server.queue(raw)
+                    return None
+
                 if self.pipeline_request is None:
                     self.pipeline_request = HttpParser(
                         httpParserTypes.REQUEST_PARSER)
+
                 # TODO(abhinavsingh): Remove .tobytes after parser is
                 # memoryview compliant
                 self.pipeline_request.parse(raw.tobytes())
@@ -226,7 +238,8 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
                     self.server.queue(
                         memoryview(
                             self.pipeline_request.build()))
-                    self.pipeline_request = None
+                    if not self.pipeline_request.is_connection_upgrade():
+                        self.pipeline_request = None
             else:
                 self.server.queue(raw)
             return None
@@ -274,13 +287,14 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
                     # wrap_client also flushes client data before wrapping
                     # sending to client can raise, handle expected exceptions
                     self.wrap_client()
-                except OSError:
-                    logger.error('OSError when wrapping client')
-                    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
                 for plugin in self.plugins.values():
                     plugin.client._conn = self.client.connection
@@ -342,12 +356,71 @@ 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:
+        '''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
+        CA key and secret.  Again this process only happen if signed
+        certificate doesn't already exist.
+
+        returns signed certificate path.'''
+        assert(self.request.host and self.flags.ca_cert_dir and self.flags.ca_signing_key_file and
+               self.flags.ca_key_file and self.flags.ca_cert_file)
+
+        upstream_subject = {s[0][0]: s[0][1] for s in certificate['subject']}
+        public_key_path = os.path.join(self.flags.ca_cert_dir,
+                                       '{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'))
+        alt_subj_names = [text_(self.request.host), ]
+        validity_in_days = 365 * 2
+        timeout = 10
+
+        # Generate a public key for the common name
+        if not os.path.isfile(public_key_path):
+            logger.debug('Generating public key %s', public_key_path)
+            resp = gen_public_key(public_key_path=public_key_path, private_key_path=private_key_path,
+                                  private_key_password=private_key_password, subject=subject, alt_subj_names=alt_subj_names,
+                                  validity_in_days=validity_in_days, timeout=timeout)
+            assert(resp is True)
+
+        csr_path = os.path.join(self.flags.ca_cert_dir,
+                                '{0}.{1}'.format(text_(self.request.host), 'csr'))
+
+        # Generate a CSR request for this common name
+        if not os.path.isfile(csr_path):
+            logger.debug('Generating CSR %s', csr_path)
+            resp = gen_csr(csr_path=csr_path, key_path=private_key_path, password=private_key_password,
+                           crt_path=public_key_path, timeout=timeout)
+            assert(resp is True)
+
+        ca_key_path = self.flags.ca_key_file
+        ca_key_password = ''
+        ca_crt_path = self.flags.ca_cert_file
+        serial = self.uid.int
+
+        # Sign generated CSR
+        if not os.path.isfile(cert_file_path):
+            logger.debug('Signing CSR %s', cert_file_path)
+            resp = sign_csr(csr_path=csr_path, crt_path=cert_file_path, ca_key_path=ca_key_path,
+                            ca_key_password=ca_key_password, ca_crt_path=ca_crt_path,
+                            serial=str(serial), alt_subj_names=alt_subj_names,
+                            validity_in_days=validity_in_days, timeout=timeout)
+            assert(resp is True)
+
     @staticmethod
     def generated_cert_file_path(ca_cert_dir: str, host: str) -> str:
         return os.path.join(ca_cert_dir, '%s.pem' % host)
 
     def generate_upstream_certificate(
-            self, _certificate: Optional[Dict[str, Any]]) -> str:
+            self, certificate: Dict[str, Any]) -> str:
         if not (self.flags.ca_cert_dir and self.flags.ca_signing_key_file and
                 self.flags.ca_cert_file and self.flags.ca_key_file):
             raise HttpProtocolException(
@@ -359,29 +432,16 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
             self.flags.ca_cert_dir, text_(self.request.host))
         with self.lock:
             if not os.path.isfile(cert_file_path):
-                logger.debug('Generating certificates %s', cert_file_path)
-                # TODO: Parse subject from certificate
-                # Currently we only set CN= field for generated certificates.
-                gen_cert = subprocess.Popen(
-                    ['openssl', 'req', '-new', '-key', self.flags.ca_signing_key_file, '-subj',
-                     f'/C=/ST=/L=/O=/OU=/CN={ text_(self.request.host) }'],
-                    stdout=subprocess.PIPE,
-                    stderr=subprocess.PIPE)
-                sign_cert = subprocess.Popen(
-                    ['openssl', 'x509', '-req', '-days', '365', '-CA', self.flags.ca_cert_file, '-CAkey',
-                     self.flags.ca_key_file, '-set_serial', str(int(time.time())), '-out', cert_file_path],
-                    stdin=gen_cert.stdout,
-                    stderr=subprocess.PIPE)
-                # TODO: Ensure sign_cert success.
-                sign_cert.communicate(timeout=10)
+                self.gen_ca_signed_certificate(cert_file_path, certificate)
         return cert_file_path
 
     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)
-        ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+            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,
@@ -398,8 +458,9 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
         self.client._conn = ssl.wrap_socket(
             self.client.connection,
             server_side=True,
+            certfile=generated_cert,
             keyfile=self.flags.ca_signing_key_file,
-            certfile=generated_cert)
+            ssl_version=ssl.PROTOCOL_TLSv1_2)
         self.client.connection.setblocking(False)
         logger.debug(
             'TLS interception using %s', generated_cert)
@@ -437,7 +498,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
         assert self.request.path
         assert self.request.port
         self.event_queue.publish(
-            request_id=self.uid,
+            request_id=self.uid.hex,
             event_name=eventNames.REQUEST_COMPLETE,
             event_payload={
                 'url': text_(self.request.path)
diff --git a/proxy/http/server/plugin.py b/proxy/http/server/plugin.py
index 64491a3..a1e17c1 100644
--- a/proxy/http/server/plugin.py
+++ b/proxy/http/server/plugin.py
@@ -10,7 +10,7 @@
 """
 from abc import ABC, abstractmethod
 from typing import List, Tuple
-
+from uuid import UUID
 from ..websocket import WebsocketFrame
 from ..parser import HttpParser
 
@@ -24,7 +24,7 @@ class HttpWebServerBasePlugin(ABC):
 
     def __init__(
             self,
-            uid: str,
+            uid: UUID,
             flags: Flags,
             client: TcpClientConnection,
             event_queue: EventQueue):
diff --git a/proxy/http/websocket.py b/proxy/http/websocket.py
index 6829411..a6eb5a3 100644
--- a/proxy/http/websocket.py
+++ b/proxy/http/websocket.py
@@ -231,7 +231,7 @@ class WebsocketClient(TcpConnection):
         self.selector.register(self.sock.fileno(), ev)
         events = self.selector.select(timeout=1)
         self.selector.unregister(self.sock)
-        for key, mask in events:
+        for _, mask in events:
             if mask & selectors.EVENT_READ and self.on_message:
                 raw = self.recv()
                 if raw is None or raw.tobytes() == b'':
diff --git a/proxy/plugin/cache/store/base.py b/proxy/plugin/cache/store/base.py
index d15c5a6..eafeaa3 100644
--- a/proxy/plugin/cache/store/base.py
+++ b/proxy/plugin/cache/store/base.py
@@ -10,13 +10,13 @@
 """
 from abc import ABC, abstractmethod
 from typing import Optional
-
+from uuid import UUID
 from ....http.parser import HttpParser
 
 
 class CacheStore(ABC):
 
-    def __init__(self, uid: str) -> None:
+    def __init__(self, uid: UUID) -> None:
         self.uid = uid
 
     @abstractmethod
diff --git a/proxy/plugin/cache/store/disk.py b/proxy/plugin/cache/store/disk.py
index cf108c6..91eb4f2 100644
--- a/proxy/plugin/cache/store/disk.py
+++ b/proxy/plugin/cache/store/disk.py
@@ -11,6 +11,7 @@
 import logging
 import os
 from typing import Optional, BinaryIO
+from uuid import UUID
 
 from ....common.utils import text_
 from ....http.parser import HttpParser
@@ -22,7 +23,7 @@ logger = logging.getLogger(__name__)
 
 class OnDiskCacheStore(CacheStore):
 
-    def __init__(self, uid: str, cache_dir: str) -> None:
+    def __init__(self, uid: UUID, cache_dir: str) -> None:
         super().__init__(uid)
         self.cache_dir = cache_dir
         self.cache_file_path: Optional[str] = None
@@ -31,7 +32,7 @@ class OnDiskCacheStore(CacheStore):
     def open(self, request: HttpParser) -> None:
         self.cache_file_path = os.path.join(
             self.cache_dir,
-            '%s-%s.txt' % (text_(request.host), self.uid))
+            '%s-%s.txt' % (text_(request.host), self.uid.hex))
         self.cache_file = open(self.cache_file_path, "wb")
 
     def cache_request(self, request: HttpParser) -> Optional[HttpParser]:
diff --git a/proxy/plugin/proxy_pool.py b/proxy/plugin/proxy_pool.py
index e701fcb..3cb664c 100644
--- a/proxy/plugin/proxy_pool.py
+++ b/proxy/plugin/proxy_pool.py
@@ -79,6 +79,6 @@ class ProxyPoolPlugin(HttpProxyBasePlugin):
             COLON + SLASH + SLASH +
             request.host +
             COLON +
-            bytes(request.port) +
+            str(request.port).encode() +
             request.path
         )
diff --git a/requirements-release.txt b/requirements-release.txt
index 9560969..8c2c116 100644
--- a/requirements-release.txt
+++ b/requirements-release.txt
@@ -1,3 +1,2 @@
 twine==3.1.1
 wheel==0.34.2
-setuptools==45.1.0
diff --git a/requirements.txt b/requirements.txt
index ad2ed38..f0c0fb6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,1 +1,1 @@
-typing-extensions==3.7.4.1
+typing-extensions==3.7.4.2
diff --git a/setup.py b/setup.py
index 6c73e03..0d2c675 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,7 @@
 """
 from setuptools import setup, find_packages
 
-VERSION = (2, 1, 2)
+VERSION = (2, 2, 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.'''


Test patch:
diff --git a/.github/workflows/test-brew.yml b/.github/workflows/test-brew.yml
index ff9b992..e6afe49 100644
--- a/.github/workflows/test-brew.yml
+++ b/.github/workflows/test-brew.yml
@@ -17,7 +17,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v1
         with:
-          python-version: ${{ matrix.python }}-dev
+          python-version: ${{ matrix.python }}
       - name: Brew
         run: |
           brew install ./helper/homebrew/develop/proxy.rb
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 7e1d433..e623186 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -17,7 +17,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v1
         with:
-          python-version: ${{ matrix.python }}-dev
+          python-version: ${{ matrix.python }}
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
diff --git a/.github/workflows/test-library.yml b/.github/workflows/test-library.yml
index 65059ec..9068485 100644
--- a/.github/workflows/test-library.yml
+++ b/.github/workflows/test-library.yml
@@ -9,7 +9,7 @@ jobs:
     strategy:
       matrix:
         os: [macOS, ubuntu, windows]
-        python: [3.5, 3.6, 3.7, 3.8]
+        python: [3.6, 3.7, 3.8]
       max-parallel: 4
       fail-fast: false
     steps:
@@ -17,7 +17,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v1
         with:
-          python-version: ${{ matrix.python }}-dev
+          python-version: ${{ matrix.python }}
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
diff --git a/proxy/testing/test_case.py b/proxy/testing/test_case.py
index b1cd9ce..55f4b95 100644
--- a/proxy/testing/test_case.py
+++ b/proxy/testing/test_case.py
@@ -38,6 +38,8 @@ class TestCase(unittest.TestCase):
         cls.INPUT_ARGS = getattr(cls, 'PROXY_PY_STARTUP_FLAGS') \
             if hasattr(cls, 'PROXY_PY_STARTUP_FLAGS') \
             else cls.DEFAULT_PROXY_PY_STARTUP_FLAGS
+        cls.INPUT_ARGS.append('--hostname')
+        cls.INPUT_ARGS.append('0.0.0.0')
         cls.INPUT_ARGS.append('--port')
         cls.INPUT_ARGS.append(str(cls.PROXY_PORT))
 
diff --git a/requirements-testing.txt b/requirements-testing.txt
index 2f63ff8..11e1550 100644
--- a/requirements-testing.txt
+++ b/requirements-testing.txt
@@ -1,11 +1,13 @@
 python-coveralls==2.9.3
-coverage==5.0.3
-flake8==3.7.9
-pytest==5.3.5
-pytest-cov==2.8.1
-autopep8==1.5
-mypy==0.761
-py-spy==0.3.0
-codecov==2.0.15
-tox==3.14.3
+coverage==5.1
+flake8==3.8.3
+pytest==5.4.3
+pytest-cov==2.10.0
+autopep8==1.5.3
+mypy==0.780
+py-spy==0.3.3
+codecov==2.1.7
+tox==3.15.2
 mccabe==0.6.1
+pylint==2.5.3
+rope==0.17.0
diff --git a/tests/codecov.yml b/tests/codecov.yml
new file mode 100644
index 0000000..f393de9
--- /dev/null
+++ b/tests/codecov.yml
@@ -0,0 +1,12 @@
+codecov:
+  require_ci_to_pass: yes
+  notify:
+    wait_for_ci: yes
+coverage:
+  status:
+    project:
+      default:
+        threshold: 1%
+    patch:
+      default:
+        threshold: 1%
diff --git a/tests/http/test_http_parser.py b/tests/http/test_http_parser.py
index 896b778..c98e638 100644
--- a/tests/http/test_http_parser.py
+++ b/tests/http/test_http_parser.py
@@ -132,7 +132,7 @@ class TestHttpParser(unittest.TestCase):
                      b'example.com')
         self.parser.parse(pkt)
         self.assertEqual(self.parser.total_size, len(pkt))
-        self.assertEqual(self.parser.build_url(), b'/path/dir/?a=b&c=d#p=q')
+        self.assertEqual(self.parser.build_path(), b'/path/dir/?a=b&c=d#p=q')
         self.assertEqual(self.parser.method, b'GET')
         assert self.parser.url
         self.assertEqual(self.parser.url.hostname, b'example.com')
@@ -149,7 +149,7 @@ class TestHttpParser(unittest.TestCase):
             self.parser.build())
 
     def test_build_url_none(self) -> None:
-        self.assertEqual(self.parser.build_url(), b'/None')
+        self.assertEqual(self.parser.build_path(), b'/None')
 
     def test_line_rcvd_to_rcving_headers_state_change(self) -> None:
         pkt = b'GET http://localhost HTTP/1.1'
diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py
index ff01a7c..7799c1d 100644
--- a/tests/http/test_http_proxy_tls_interception.py
+++ b/tests/http/test_http_proxy_tls_interception.py
@@ -30,14 +30,18 @@ class TestHttpProxyTlsInterception(unittest.TestCase):
     @mock.patch('ssl.wrap_socket')
     @mock.patch('ssl.create_default_context')
     @mock.patch('proxy.http.proxy.server.TcpServerConnection')
-    @mock.patch('subprocess.Popen')
+    @mock.patch('proxy.http.proxy.server.gen_public_key')
+    @mock.patch('proxy.http.proxy.server.gen_csr')
+    @mock.patch('proxy.http.proxy.server.sign_csr')
     @mock.patch('selectors.DefaultSelector')
     @mock.patch('socket.fromfd')
     def test_e2e(
             self,
             mock_fromfd: mock.Mock,
             mock_selector: mock.Mock,
-            mock_popen: mock.Mock,
+            mock_sign_csr: mock.Mock,
+            mock_gen_csr: mock.Mock,
+            mock_gen_public_key: mock.Mock,
             mock_server_conn: mock.Mock,
             mock_ssl_context: mock.Mock,
             mock_ssl_wrap: mock.Mock) -> None:
@@ -46,11 +50,17 @@ class TestHttpProxyTlsInterception(unittest.TestCase):
 
         self.mock_fromfd = mock_fromfd
         self.mock_selector = mock_selector
-        self.mock_popen = mock_popen
+        self.mock_sign_csr = mock_sign_csr
+        self.mock_gen_csr = mock_gen_csr
+        self.mock_gen_public_key = mock_gen_public_key
         self.mock_server_conn = mock_server_conn
         self.mock_ssl_context = mock_ssl_context
         self.mock_ssl_wrap = mock_ssl_wrap
 
+        self.mock_sign_csr.return_value = True
+        self.mock_gen_csr.return_value = True
+        self.mock_gen_public_key.return_value = True
+
         ssl_connection = mock.MagicMock(spec=ssl.SSLSocket)
         self.mock_ssl_context.return_value.wrap_socket.return_value = ssl_connection
         self.mock_ssl_wrap.return_value = mock.MagicMock(spec=ssl.SSLSocket)
@@ -69,7 +79,7 @@ class TestHttpProxyTlsInterception(unittest.TestCase):
         self.flags = Flags(
             ca_cert_file='ca-cert.pem',
             ca_key_file='ca-key.pem',
-            ca_signing_key_file='ca-signing-key.pem',
+            ca_signing_key_file='ca-signing-key.pem'
         )
         self.plugin = mock.MagicMock()
         self.proxy_plugin = mock.MagicMock()
@@ -118,6 +128,7 @@ class TestHttpProxyTlsInterception(unittest.TestCase):
                 fd=self._conn.fileno,
                 events=selectors.EVENT_READ,
                 data=None), selectors.EVENT_READ)], ]
+
         self.protocol_handler.run_once()
 
         # Assert our mocked plugins invocations
@@ -135,15 +146,16 @@ 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)
+        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)
         self.assertEqual(plain_connection.setblocking.call_count, 2)
         self.mock_ssl_context.return_value.wrap_socket.assert_called_with(
             plain_connection, server_hostname=host)
-        # TODO: Assert Popen arguments, piping, success condition
-        self.assertEqual(self.mock_popen.call_count, 2)
+        self.assertEqual(self.mock_sign_csr.call_count, 1)
+        self.assertEqual(self.mock_gen_csr.call_count, 1)
+        self.assertEqual(self.mock_gen_public_key.call_count, 1)
         self.assertEqual(ssl_connection.setblocking.call_count, 1)
         self.assertEqual(
             self.mock_server_conn.return_value._conn,
@@ -156,7 +168,8 @@ class TestHttpProxyTlsInterception(unittest.TestCase):
             server_side=True,
             keyfile=self.flags.ca_signing_key_file,
             certfile=HttpProxyPlugin.generated_cert_file_path(
-                self.flags.ca_cert_dir, host)
+                self.flags.ca_cert_dir, host),
+            ssl_version=ssl.PROTOCOL_TLSv1_2
         )
         self.assertEqual(self._conn.setblocking.call_count, 2)
         self.assertEqual(
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 2976869..0c1ec0a 100644
--- a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py
+++ b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py
@@ -33,23 +33,33 @@ class TestHttpProxyPluginExamplesWithTlsInterception(unittest.TestCase):
     @mock.patch('ssl.wrap_socket')
     @mock.patch('ssl.create_default_context')
     @mock.patch('proxy.http.proxy.server.TcpServerConnection')
-    @mock.patch('subprocess.Popen')
+    @mock.patch('proxy.http.proxy.server.gen_public_key')
+    @mock.patch('proxy.http.proxy.server.gen_csr')
+    @mock.patch('proxy.http.proxy.server.sign_csr')
     @mock.patch('selectors.DefaultSelector')
     @mock.patch('socket.fromfd')
     def setUp(self,
               mock_fromfd: mock.Mock,
               mock_selector: mock.Mock,
-              mock_popen: mock.Mock,
+              mock_sign_csr: mock.Mock,
+              mock_gen_csr: mock.Mock,
+              mock_gen_public_key: mock.Mock,
               mock_server_conn: mock.Mock,
               mock_ssl_context: mock.Mock,
               mock_ssl_wrap: mock.Mock) -> None:
         self.mock_fromfd = mock_fromfd
         self.mock_selector = mock_selector
-        self.mock_popen = mock_popen
+        self.mock_sign_csr = mock_sign_csr
+        self.mock_gen_csr = mock_gen_csr
+        self.mock_gen_public_key = mock_gen_public_key
         self.mock_server_conn = mock_server_conn
         self.mock_ssl_context = mock_ssl_context
         self.mock_ssl_wrap = mock_ssl_wrap
 
+        self.mock_sign_csr.return_value = True
+        self.mock_gen_csr.return_value = True
+        self.mock_gen_public_key.return_value = True
+
         self.fileno = 10
         self._addr = ('127.0.0.1', 54382)
         self.flags = Flags(
@@ -126,7 +136,10 @@ class TestHttpProxyPluginExamplesWithTlsInterception(unittest.TestCase):
         )
         self.protocol_handler.run_once()
 
-        self.mock_popen.assert_called()
+        self.assertEqual(self.mock_sign_csr.call_count, 1)
+        self.assertEqual(self.mock_gen_csr.call_count, 1)
+        self.assertEqual(self.mock_gen_public_key.call_count, 1)
+
         self.mock_server_conn.assert_called_once_with('uni.corn', 443)
         self.server.connect.assert_called()
         self.assertEqual(
diff --git a/tests/testing/test_embed.py b/tests/testing/test_embed.py
index d7032c2..6a55af2 100644
--- a/tests/testing/test_embed.py
+++ b/tests/testing/test_embed.py
@@ -21,8 +21,7 @@ from proxy.http.codes import httpStatusCodes
 from proxy.http.methods import httpMethods
 
 
-@unittest.skipIf(os.environ.get('GITHUB_ACTIONS', False),
-                 'Disabled on GitHub actions because proxy.py setup hangs')
+@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."""