Repository: abhinavsingh/proxy.py
Pull request #725 (2021-11-11 18:34:14)

Allow `--plugins` flag to be used multiple times
Following are valid invocation:
1) `--plugins A`
2) `--plugins A,B`
3) `--plugins A --plugins B`
4) `--plugins A,B --plugins C`

Additionally:
1. Now static web server only performs compression if `size > 20 bytes`.
2. This value can be overridden using `--min-compression-limit` flag.

codecov[bot]: # [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) Report
> Merging [#725](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (bb5fd29) into [develop](https://codecov.io/gh/abhinavsingh/proxy.py/commit/e38f1a8feecf0f62669c29a490cff7a895866173?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (e38f1a8) will **decrease** coverage by `0.24%`.
> The diff coverage is `86.36%`.

> :exclamation: Current head bb5fd29 differs from pull request most recent head 023d191. Consider uploading reports for the commit 023d191 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/graphs/tree.svg?width=650&height=150&src=pr&token=Zh9J7b4la2&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)

```diff
@@             Coverage Diff             @@
##           develop     #725      +/-   ##
===========================================
- Coverage    86.83%   86.59%   -0.25%     
===========================================
  Files          114      114              
  Lines         5135     5147      +12     
  Branches       417      376      -41     
===========================================
- Hits          4459     4457       -2     
- Misses         594      598       +4     
- Partials        82       92      +10     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytest | `86.59% <86.36%> (-0.09%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | Coverage Δ | |
|---|---|---|
| [proxy/core/connection/connection.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9jb25uZWN0aW9uL2Nvbm5lY3Rpb24ucHk=) | `91.22% <ø> (ø)` | |
| [proxy/dashboard/dashboard.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvZGFzaGJvYXJkL2Rhc2hib2FyZC5weQ==) | `42.30% <ø> (ø)` | |
| [proxy/plugin/proxy\_pool.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcGx1Z2luL3Byb3h5X3Bvb2wucHk=) | `29.34% <0.00%> (ø)` | |
| [proxy/plugin/redirect\_to\_custom\_server.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcGx1Z2luL3JlZGlyZWN0X3RvX2N1c3RvbV9zZXJ2ZXIucHk=) | `80.00% <0.00%> (-0.96%)` | :arrow_down: |
| [proxy/proxy.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcHJveHkucHk=) | `100.00% <ø> (ø)` | |
| [proxy/common/plugins.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL3BsdWdpbnMucHk=) | `96.22% <75.00%> (-3.78%)` | :arrow_down: |
| [proxy/http/server/web.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvaHR0cC9zZXJ2ZXIvd2ViLnB5) | `64.90% <90.90%> (+1.14%)` | :arrow_up: |
| [proxy/common/constants.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL2NvbnN0YW50cy5weQ==) | `97.67% <100.00%> (-2.33%)` | :arrow_down: |
| [proxy/common/flag.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL2ZsYWcucHk=) | `92.00% <100.00%> (-1.60%)` | :arrow_down: |
| [proxy/http/parser.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvaHR0cC9wYXJzZXIucHk=) | `94.67% <100.00%> (+0.06%)` | :arrow_up: |
| ... and [10 more](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Last update [e38f1a8...023d191](https://codecov.io/gh/abhinavsingh/proxy.py/pull/725?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).


Patch:
diff --git a/README.md b/README.md
index 4825291..5fb7eb2 100644
--- a/README.md
+++ b/README.md
@@ -1941,24 +1941,24 @@ for list of tests.
 
 ```console
 ❯ proxy -h
-usage: -m [-h] [--enable-events] [--enable-conn-pool] [--threadless] [--threaded]
-          [--num-workers NUM_WORKERS] [--pid-file PID_FILE] [--backlog BACKLOG]
-          [--hostname HOSTNAME] [--port PORT] [--num-acceptors NUM_ACCEPTORS]
-          [--unix-socket-path UNIX_SOCKET_PATH]
-          [--client-recvbuf-size CLIENT_RECVBUF_SIZE] [--key-file KEY_FILE]
-          [--timeout TIMEOUT] [--version] [--log-level LOG_LEVEL]
+usage: proxy [-h] [--enable-events] [--enable-conn-pool] [--threadless] [--threaded]
+          [--num-workers NUM_WORKERS] [--backlog BACKLOG] [--hostname HOSTNAME]
+          [--port PORT] [--unix-socket-path UNIX_SOCKET_PATH]
+          [--num-acceptors NUM_ACCEPTORS] [--version] [--log-level LOG_LEVEL]
           [--log-file LOG_FILE] [--log-format LOG_FORMAT]
-          [--open-file-limit OPEN_FILE_LIMIT] [--plugins PLUGINS]
-          [--enable-dashboard] [--work-klass WORK_KLASS] [--disable-http-proxy]
-          [--ca-key-file CA_KEY_FILE] [--ca-cert-dir CA_CERT_DIR]
-          [--ca-cert-file CA_CERT_FILE] [--ca-file CA_FILE]
-          [--ca-signing-key-file CA_SIGNING_KEY_FILE] [--cert-file CERT_FILE]
-          [--disable-headers DISABLE_HEADERS]
+          [--open-file-limit OPEN_FILE_LIMIT] [--plugins PLUGINS [PLUGINS ...]]
+          [--enable-dashboard] [--work-klass WORK_KLASS] [--pid-file PID_FILE]
+          [--client-recvbuf-size CLIENT_RECVBUF_SIZE] [--key-file KEY_FILE]
+          [--timeout TIMEOUT] [--disable-http-proxy] [--ca-key-file CA_KEY_FILE]
+          [--ca-cert-dir CA_CERT_DIR] [--ca-cert-file CA_CERT_FILE]
+          [--ca-file CA_FILE] [--ca-signing-key-file CA_SIGNING_KEY_FILE]
+          [--cert-file CERT_FILE] [--disable-headers DISABLE_HEADERS]
           [--server-recvbuf-size SERVER_RECVBUF_SIZE] [--basic-auth BASIC_AUTH]
           [--cache-dir CACHE_DIR]
           [--filtered-upstream-hosts FILTERED_UPSTREAM_HOSTS] [--enable-web-server]
           [--enable-static-server] [--static-server-dir STATIC_SERVER_DIR]
-          [--pac-file PAC_FILE] [--pac-file-url-path PAC_FILE_URL_PATH]
+          [--min-compression-length MIN_COMPRESSION_LENGTH] [--pac-file PAC_FILE]
+          [--pac-file-url-path PAC_FILE_URL_PATH]
           [--filtered-client-ips FILTERED_CLIENT_IPS]
           [--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
           [--cloudflare-dns-mode CLOUDFLARE_DNS_MODE]
@@ -1971,50 +1971,51 @@ options:
                         Plugins can be used to subscribe for core events.
   --enable-conn-pool    Default: False. (WIP) Enable upstream connection pooling.
   --threadless          Default: True. Enabled by default on Python 3.8+ (mac,
-                        linux). When disabled a new thread is spawned to handle
-                        each client connection.
+                        linux). When disabled a new thread is spawned to handle each
+                        client connection.
   --threaded            Default: False. Disabled by default on Python < 3.8 and
-                        windows. When enabled a new thread is spawned to handle
-                        each client connection.
+                        windows. When enabled a new thread is spawned to handle each
+                        client connection.
   --num-workers NUM_WORKERS
                         Defaults to number of CPU cores.
-  --pid-file PID_FILE   Default: None. Save parent process ID to a file.
-  --backlog BACKLOG     Default: 100. Maximum number of pending connections to
-                        proxy server
+  --backlog BACKLOG     Default: 100. Maximum number of pending connections to proxy
+                        server
   --hostname HOSTNAME   Default: ::1. Server IP address.
   --port PORT           Default: 8899. Server port.
+  --unix-socket-path UNIX_SOCKET_PATH
+                        Default: None. Unix socket path to use. When provided --host
+                        and --port flags are ignored
   --num-acceptors NUM_ACCEPTORS
                         Defaults to number of CPU cores.
-  --unix-socket-path UNIX_SOCKET_PATH
-                        Default: None. Unix socket path to use. When provided
-                        --host and --port flags are ignored
-  --client-recvbuf-size CLIENT_RECVBUF_SIZE
-                        Default: 1 MB. Maximum amount of data received from the
-                        client in a single recv() operation. Bump this value for
-                        faster uploads at the expense of increased RAM.
-  --key-file KEY_FILE   Default: None. Server key file to enable end-to-end TLS
-                        encryption with clients. If used, must also pass --cert-
-                        file.
-  --timeout TIMEOUT     Default: 10.0. Number of seconds after which an inactive
-                        connection must be dropped. Inactivity is defined by no
-                        data sent or received by the client.
   --version, -v         Prints proxy.py version.
   --log-level LOG_LEVEL
                         Valid options: DEBUG, INFO (default), WARNING, ERROR,
                         CRITICAL. Both upper and lowercase values are allowed. You
-                        may also simply use the leading character e.g. --log-level
-                        d
+                        may also simply use the leading character e.g. --log-level d
   --log-file LOG_FILE   Default: sys.stdout. Log file destination.
   --log-format LOG_FORMAT
                         Log format for Python logger.
   --open-file-limit OPEN_FILE_LIMIT
                         Default: 1024. Maximum number of files (TCP connections)
                         that proxy.py can open concurrently.
-  --plugins PLUGINS     Comma separated plugins
+  --plugins PLUGINS [PLUGINS ...]
+                        Comma separated plugins. You may use --plugins flag multiple
+                        times.
   --enable-dashboard    Default: False. Enables proxy.py dashboard.
   --work-klass WORK_KLASS
-                        Default: proxy.http.handler.HttpProtocolHandler. Work klass
-                        to use for work execution.
+                        Default: proxy.http.HttpProtocolHandler. Work klass to use
+                        for work execution.
+  --pid-file PID_FILE   Default: None. Save "parent" process ID to a file.
+  --client-recvbuf-size CLIENT_RECVBUF_SIZE
+                        Default: 1 MB. Maximum amount of data received from the
+                        client in a single recv() operation. Bump this value for
+                        faster uploads at the expense of increased RAM.
+  --key-file KEY_FILE   Default: None. Server key file to enable end-to-end TLS
+                        encryption with clients. If used, must also pass --cert-
+                        file.
+  --timeout TIMEOUT     Default: 10.0. Number of seconds after which an inactive
+                        connection must be dropped. Inactivity is defined by no data
+                        sent or received by the client.
   --disable-http-proxy  Default: False. Whether to disable proxy.HttpProxyPlugin.
   --ca-key-file CA_KEY_FILE
                         Default: None. CA key to use for signing dynamically
@@ -2026,19 +2027,18 @@ options:
                         file and --ca-signing-key-file
   --ca-cert-file CA_CERT_FILE
                         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: /Users/abhinavsingh/Dev/proxy.py/venv310/lib/pytho
-                        n3.10/site-packages/certifi/cacert.pem. Provide path to
+                        dynamically generated HTTPS certificates. If used, must also
+                        pass --ca-key-file and --ca-signing-key-file
+  --ca-file CA_FILE     Default: /Users/abhinavsingh/Dev/proxy.py/venv310/lib/python
+                        3.10/site-packages/certifi/cacert.pem. Provide path to
                         custom CA bundle for peer certificate verification
   --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 pass --ca-key-
-                        file and --ca-cert-file
+                        of HTTPS certificates. If used, must also pass --ca-key-file
+                        and --ca-cert-file
   --cert-file CERT_FILE
                         Default: None. Server certificate to enable end-to-end TLS
-                        encryption with clients. If used, must also pass --key-
-                        file.
+                        encryption with clients. If used, must also pass --key-file.
   --disable-headers DISABLE_HEADERS
                         Default: None. Comma separated list of headers to remove
                         before dispatching client request to upstream server.
@@ -2055,8 +2055,7 @@ options:
   --filtered-upstream-hosts FILTERED_UPSTREAM_HOSTS
                         Default: Blocks Facebook. Comma separated list of IPv4 and
                         IPv6 addresses.
-  --enable-web-server   Default: False. Whether to enable
-                        proxy.HttpWebServerPlugin.
+  --enable-web-server   Default: False. Whether to enable proxy.HttpWebServerPlugin.
   --enable-static-server
                         Default: False. Enable inbuilt static file server.
                         Optionally, also use --static-server-dir to serve static
@@ -2065,11 +2064,14 @@ options:
                         folder.
   --static-server-dir STATIC_SERVER_DIR
                         Default: "public" folder in directory where proxy.py is
-                        placed. This option is only applicable when static server
-                        is also enabled. See --enable-static-server.
+                        placed. This option is only applicable when static server is
+                        also enabled. See --enable-static-server.
+  --min-compression-length MIN_COMPRESSION_LENGTH
+                        Default: 20 bytes. Sets the minimum length of a response
+                        that will be compressed (gzipped).
   --pac-file PAC_FILE   A file (Proxy Auto Configuration) or string to serve when
-                        the server receives a direct file request. Using this
-                        option enables proxy.HttpWebServerPlugin.
+                        the server receives a direct file request. Using this option
+                        enables proxy.HttpWebServerPlugin.
   --pac-file-url-path PAC_FILE_URL_PATH
                         Default: /. Web server path to serve the PAC file.
   --filtered-client-ips FILTERED_CLIENT_IPS
@@ -2083,8 +2085,7 @@ options:
                         protection) or "family" (for malware and adult content
                         protection)
 
-Proxy.py not working? Report at:
-https://github.com/abhinavsingh/proxy.py/issues/new
+Proxy.py not working? Report at: https://github.com/abhinavsingh/proxy.py/issues/new
 ```
 
 # Changelog
diff --git a/examples/https_connect_tunnel.py b/examples/https_connect_tunnel.py
index 5b246a5..5e8dbd2 100644
--- a/examples/https_connect_tunnel.py
+++ b/examples/https_connect_tunnel.py
@@ -16,7 +16,6 @@ from proxy import Proxy
 from proxy.common.utils import build_http_response
 from proxy.http.codes import httpStatusCodes
 from proxy.http.parser import httpParserStates
-from proxy.http.methods import httpMethods
 from proxy.core.base import BaseTcpTunnelHandler
 
 
@@ -51,7 +50,7 @@ class HttpsConnectTunnelHandler(BaseTcpTunnelHandler):
         self.request.parse(data)
 
         # Drop the request if not a CONNECT request
-        if self.request.method != httpMethods.CONNECT:
+        if not self.request.is_https_tunnel():
             self.work.queue(
                 HttpsConnectTunnelHandler.PROXY_TUNNEL_UNSUPPORTED_SCHEME,
             )
diff --git a/proxy/common/constants.py b/proxy/common/constants.py
index b5b8818..bff6c84 100644
--- a/proxy/common/constants.py
+++ b/proxy/common/constants.py
@@ -16,7 +16,7 @@ import pathlib
 import sysconfig
 import ipaddress
 
-from typing import List
+from typing import Any, List
 
 from .version import __version__
 
@@ -93,10 +93,11 @@ DEFAULT_OPEN_FILE_LIMIT = 1024
 DEFAULT_PAC_FILE = None
 DEFAULT_PAC_FILE_URL_PATH = b'/'
 DEFAULT_PID_FILE = None
-DEFAULT_PLUGINS = ''
+DEFAULT_PLUGINS: List[Any] = []
 DEFAULT_PORT = 8899
 DEFAULT_SERVER_RECVBUF_SIZE = DEFAULT_BUFFER_SIZE
 DEFAULT_STATIC_SERVER_DIR = os.path.join(PROXY_PY_DIR, "public")
+DEFAULT_MIN_COMPRESSION_LIMIT = 20  # In bytes
 DEFAULT_THREADLESS = _env_threadless_compliant()
 DEFAULT_TIMEOUT = 10.0
 DEFAULT_VERSION = False
diff --git a/proxy/common/flag.py b/proxy/common/flag.py
index ac6f251..7d8224e 100644
--- a/proxy/common/flag.py
+++ b/proxy/common/flag.py
@@ -21,10 +21,10 @@ from typing import Optional, List, Any, cast
 
 from .plugins import Plugins
 from .types import IpAddress
-from .utils import text_, bytes_, is_py2, set_open_file_limit
+from .utils import bytes_, is_py2, set_open_file_limit
 from .constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_NUM_ACCEPTORS, DEFAULT_NUM_WORKERS
 from .constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS, PY2_DEPRECATION_MESSAGE
-from .constants import PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL
+from .constants import PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL, DEFAULT_MIN_COMPRESSION_LIMIT
 from .constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE
 from .constants import PLUGIN_WEB_SERVER, PLUGIN_PROXY_AUTH
 from .logger import Logger
@@ -110,6 +110,9 @@ class FlagParser:
         # proxy.py currently cannot serve over HTTPS and also perform TLS interception
         # at the same time.  Check if user is trying to enable both feature
         # at the same time.
+        #
+        # TODO: Use parser.add_mutually_exclusive_group()
+        # and remove this logic from here.
         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(
@@ -140,18 +143,16 @@ class FlagParser:
             bytes_(p)
             for p in FlagParser.get_default_plugins(args)
         ]
-        extra_plugins = [
-            p if isinstance(p, type) else bytes_(p)
-            for p in opts.get('plugins', args.plugins.split(text_(COMMA)))
-            if not (isinstance(p, str) and len(p) == 0)
-        ]
-        plugins = Plugins.load(default_plugins + extra_plugins)
+        plugins = Plugins.load(
+            default_plugins + Plugins.resolve_plugin_flag(
+                args.plugins, opts.get('plugins', None),
+            ),
+        )
 
         # https://github.com/python/mypy/issues/5865
         #
         # def option(t: object, key: str, default: Any) -> Any:
         #     return cast(t, opts.get(key, default))
-
         args.work_klass = work_klass
         args.plugins = plugins
         args.auth_code = cast(
@@ -284,6 +285,16 @@ class FlagParser:
                 args.enable_static_server,
             ),
         )
+        args.min_compression_limit = cast(
+            bool,
+            opts.get(
+                'min_compression_limit',
+                getattr(
+                    args, 'min_compression_limit',
+                    DEFAULT_MIN_COMPRESSION_LIMIT,
+                ),
+            ),
+        )
         args.devtools_ws_path = cast(
             bytes,
             opts.get(
diff --git a/proxy/common/plugins.py b/proxy/common/plugins.py
index 2c04530..52d6a28 100644
--- a/proxy/common/plugins.py
+++ b/proxy/common/plugins.py
@@ -12,12 +12,13 @@ import os
 import abc
 import logging
 import inspect
+import itertools
 import importlib
 
 from typing import Any, List, Dict, Optional, Union
 
 from .utils import bytes_, text_
-from .constants import DOT, DEFAULT_ABC_PLUGINS
+from .constants import DOT, DEFAULT_ABC_PLUGINS, COMMA
 
 logger = logging.getLogger(__name__)
 
@@ -25,6 +26,24 @@ logger = logging.getLogger(__name__)
 class Plugins:
     """Common utilities for plugin discovery."""
 
+    @staticmethod
+    def resolve_plugin_flag(flag_plugins: Any, opt_plugins: Optional[Any] = None) -> List[Union[bytes, type]]:
+        if isinstance(flag_plugins, list):
+            requested_plugins = list(
+                itertools.chain.from_iterable([
+                    p.split(text_(COMMA)) for p in list(
+                        itertools.chain.from_iterable(flag_plugins),
+                    )
+                ]),
+            )
+        else:
+            requested_plugins = flag_plugins.split(text_(COMMA))
+        return [
+            p if isinstance(p, type) else bytes_(p)
+            for p in (opt_plugins if opt_plugins is not None else requested_plugins)
+            if not (isinstance(p, str) and len(p) == 0)
+        ]
+
     @staticmethod
     def discover(input_args: List[str]) -> None:
         """Search for plugin and plugins flag in command line arguments,
diff --git a/proxy/core/connection/connection.py b/proxy/core/connection/connection.py
index 2827185..69651b8 100644
--- a/proxy/core/connection/connection.py
+++ b/proxy/core/connection/connection.py
@@ -56,6 +56,7 @@ class TcpConnection(ABC):
 
     def send(self, data: bytes) -> int:
         """Users must handle BrokenPipeError exceptions"""
+        # logger.info(data)
         return self.connection.send(data)
 
     def recv(
diff --git a/proxy/dashboard/dashboard.py b/proxy/dashboard/dashboard.py
index 57fb5aa..d9ca1a0 100644
--- a/proxy/dashboard/dashboard.py
+++ b/proxy/dashboard/dashboard.py
@@ -67,6 +67,7 @@ class ProxyDashboard(HttpWebServerBasePlugin):
                         self.flags.static_server_dir,
                         'dashboard', 'proxy.html',
                     ),
+                    self.flags.min_compression_limit,
                 ),
             )
         elif request.path in (
diff --git a/proxy/http/parser.py b/proxy/http/parser.py
index f51ec8e..209ecee 100644
--- a/proxy/http/parser.py
+++ b/proxy/http/parser.py
@@ -138,11 +138,14 @@ class HttpParser:
         # For CONNECT requests, request line contains
         # upstream_host:upstream_port which is not complaint
         # with urlsplit, which expects a fully qualified url.
-        if self.method == httpMethods.CONNECT:
+        if self.is_https_tunnel():
             url = b'https://' + url
         self._url = urlparse.urlsplit(url)
         self._set_line_attributes()
 
+    def is_https_tunnel(self) -> bool:
+        return self.method == httpMethods.CONNECT
+
     def is_chunked_encoded(self) -> bool:
         return b'transfer-encoding' in self.headers and \
                self.headers[b'transfer-encoding'][1].lower() == b'chunked'
@@ -184,7 +187,7 @@ class HttpParser:
                 COLON +
                 str(self.port).encode() +
                 self.path
-            ) if self.method != httpMethods.CONNECT else (self.host + COLON + str(self.port).encode())
+            ) if not self.is_https_tunnel() else (self.host + COLON + str(self.port).encode())
         return build_http_request(
             self.method, path, self.version,
             headers={} if not self.headers else {
@@ -305,7 +308,7 @@ class HttpParser:
 
     def _set_line_attributes(self) -> None:
         if self.type == httpParserTypes.REQUEST_PARSER:
-            if self.method == httpMethods.CONNECT and self._url:
+            if self.is_https_tunnel() and self._url:
                 self.host = self._url.hostname
                 self.port = 443 if self._url.port is None else self._url.port
             elif self._url:
diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py
index b313542..158b890 100644
--- a/proxy/http/proxy/server.py
+++ b/proxy/http/proxy/server.py
@@ -298,7 +298,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
             # parse incoming response packet
             # only for non-https requests and when
             # tls interception is enabled
-            if self.request.method != httpMethods.CONNECT:
+            if not self.request.is_https_tunnel():
                 # See https://github.com/abhinavsingh/proxy.py/issues/127 for why
                 # currently response parsing is disabled when TLS interception is enabled.
                 #
@@ -385,7 +385,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
 
     def access_log(self, log_attrs: Dict[str, Any]) -> None:
         access_log_format = DEFAULT_HTTPS_ACCESS_LOG_FORMAT
-        if self.request.method != httpMethods.CONNECT:
+        if not self.request.is_https_tunnel():
             access_log_format = DEFAULT_HTTP_ACCESS_LOG_FORMAT
         logger.info(access_log_format.format_map(log_attrs))
 
@@ -395,7 +395,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
         # However, this must also be accompanied by resetting both request
         # and response objects.
         #
-        # if not self.request.method == httpMethods.CONNECT and \
+        # if not self.request.is_https_tunnel() and \
         #         self.response.state == httpParserStates.COMPLETE:
         #     self.access_log()
         return chunk
@@ -426,7 +426,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
             # We also handle pipeline scenario for https proxy
             # requests is TLS interception is enabled.
             if self.request.state == httpParserStates.COMPLETE and (
-                    self.request.method != httpMethods.CONNECT or
+                    not self.request.is_https_tunnel() or
                     self.tls_interception_enabled()
             ):
                 if self.pipeline_request is not None and \
@@ -506,7 +506,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
         # For https requests, respond back with tunnel established response.
         # Optionally, setup interceptor if TLS interception is enabled.
         if self.upstream:
-            if self.request.method == httpMethods.CONNECT:
+            if self.request.is_https_tunnel():
                 self.client.queue(
                     HttpProxyPlugin.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT,
                 )
@@ -868,7 +868,7 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
             event_name=eventNames.REQUEST_COMPLETE,
             event_payload={
                 'url': text_(self.request.path)
-                if self.request.method == httpMethods.CONNECT
+                if self.request.is_https_tunnel()
                 else 'http://%s:%d%s' % (text_(self.request.host), self.request.port, text_(self.request.path)),
                 'method': text_(self.request.method),
                 'headers': {text_(k): text_(v[1]) for k, v in self.request.headers.items()},
diff --git a/proxy/http/server/web.py b/proxy/http/server/web.py
index 09c50f1..588d593 100644
--- a/proxy/http/server/web.py
+++ b/proxy/http/server/web.py
@@ -8,28 +8,30 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
-import gzip
 import re
+import gzip
 import time
+import socket
 import logging
-import os
 import mimetypes
-import socket
+
 from typing import List, Tuple, Optional, Dict, Union, Any, Pattern
 
-from .plugin import HttpWebServerBasePlugin
-from .protocols import httpProtocolTypes
+from ...common.constants import DEFAULT_STATIC_SERVER_DIR, PROXY_AGENT_HEADER_VALUE
+from ...common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_WEB_SERVER
+from ...common.constants import DEFAULT_MIN_COMPRESSION_LIMIT
+from ...common.utils import bytes_, text_, build_http_response, build_websocket_handshake_response
+from ...common.types import Readables, Writables
+from ...common.flag import flags
+
 from ..exception import HttpProtocolException
 from ..websocket import WebsocketFrame, websocketOpcodes
 from ..codes import httpStatusCodes
 from ..parser import HttpParser, httpParserStates, httpParserTypes
 from ..plugin import HttpProtocolHandlerPlugin
 
-from ...common.utils import bytes_, text_, build_http_response, build_websocket_handshake_response
-from ...common.constants import DEFAULT_STATIC_SERVER_DIR, PROXY_AGENT_HEADER_VALUE
-from ...common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_WEB_SERVER
-from ...common.types import Readables, Writables
-from ...common.flag import flags
+from .plugin import HttpWebServerBasePlugin
+from .protocols import httpProtocolTypes
 
 logger = logging.getLogger(__name__)
 
@@ -60,6 +62,14 @@ flags.add_argument(
     'See --enable-static-server.',
 )
 
+flags.add_argument(
+    '--min-compression-length',
+    type=int,
+    default=DEFAULT_MIN_COMPRESSION_LIMIT,
+    help='Default: ' + str(DEFAULT_MIN_COMPRESSION_LIMIT) + ' bytes.  ' +
+    'Sets the minimum length of a response that will be compressed (gzipped).',
+)
+
 
 class HttpWebServerPlugin(HttpProtocolHandlerPlugin):
     """HttpProtocolHandler plugin which handles incoming requests to local web server."""
@@ -119,40 +129,31 @@ class HttpWebServerPlugin(HttpProtocolHandlerPlugin):
             self.flags.certfile is not None
 
     @staticmethod
-    def read_and_build_static_file_response(path: str) -> memoryview:
+    def read_and_build_static_file_response(path: str, min_compression_limit: int) -> memoryview:
         with open(path, 'rb') as f:
             content = f.read()
         content_type = mimetypes.guess_type(path)[0]
         if content_type is None:
             content_type = 'text/plain'
+        headers = {
+            b'Content-Type': bytes_(content_type),
+            b'Cache-Control': b'max-age=86400',
+            b'Connection': b'close',
+        }
+        do_compress = len(content) > min_compression_limit
+        if do_compress:
+            headers.update({
+                b'Content-Encoding': b'gzip',
+            })
         return memoryview(
             build_http_response(
                 httpStatusCodes.OK,
                 reason=b'OK',
-                headers={
-                    b'Content-Type': bytes_(content_type),
-                    b'Cache-Control': b'max-age=86400',
-                    b'Content-Encoding': b'gzip',
-                    b'Connection': b'close',
-                },
-                body=gzip.compress(content),
+                headers=headers,
+                body=gzip.compress(content) if do_compress else content,
             ),
         )
 
-    def serve_file_or_404(self, path: str) -> bool:
-        """Read and serves a file from disk.
-
-        Queues 404 Not Found for IOError.
-        Shouldn't this be server error?
-        """
-        try:
-            self.client.queue(
-                self.read_and_build_static_file_response(path),
-            )
-        except IOError:
-            self.client.queue(self.DEFAULT_404_RESPONSE)
-        return True
-
     def try_upgrade(self) -> bool:
         if self.request.has_header(b'connection') and \
                 self.request.header(b'connection').lower() == b'upgrade':
@@ -207,18 +208,24 @@ class HttpWebServerPlugin(HttpProtocolHandlerPlugin):
             if match:
                 self.route = self.routes[protocol][route]
                 self.route.handle_request(self.request)
-                if self.request.has_header(b'connection') \
-                        and self.request.header(b'connection').lower() == b'close':
+                if self.request.has_header(b'connection') and \
+                        self.request.header(b'connection').lower() == b'close':
                     return True
                 return False
 
         # No-route found, try static serving if enabled
         if self.flags.enable_static_server:
             path = text_(self.request.path).split('?')[0]
-            if os.path.isfile(self.flags.static_server_dir + path):
-                return self.serve_file_or_404(
-                    self.flags.static_server_dir + path,
+            try:
+                self.client.queue(
+                    self.read_and_build_static_file_response(
+                        self.flags.static_server_dir + path,
+                        self.flags.min_compression_limit,
+                    ),
                 )
+                return True
+            except FileNotFoundError:
+                pass
 
         # Catch all unhandled web server requests, return 404
         self.client.queue(self.DEFAULT_404_RESPONSE)
diff --git a/proxy/plugin/proxy_pool.py b/proxy/plugin/proxy_pool.py
index 1787748..6117046 100644
--- a/proxy/plugin/proxy_pool.py
+++ b/proxy/plugin/proxy_pool.py
@@ -115,11 +115,15 @@ class ProxyPoolPlugin(HttpProxyBasePlugin):
             # using a datastructure without having to spawn separate thread/process for health
             # check.
             logger.info(
-                'Connection refused by upstream proxy {0}:{1}'.format(*endpoint),
+                'Connection refused by upstream proxy {0}:{1}'.format(
+                    *endpoint,
+                ),
             )
             raise HttpProtocolException()
         logger.debug(
-            'Established connection to upstream proxy {0}:{1}'.format(*endpoint),
+            'Established connection to upstream proxy {0}:{1}'.format(
+                *endpoint,
+            ),
         )
         return None
 
@@ -140,7 +144,7 @@ class ProxyPoolPlugin(HttpProxyBasePlugin):
             else:
                 assert len(parts) == 1
                 host = parts[0]
-                port = '443' if request.method == httpMethods.CONNECT else '80'
+                port = '443' if request.is_https_tunnel() else '80'
         path = None if not request.path else request.path.decode()
         self.request_host_port_path_method = [
             host, port, path, request.method,
diff --git a/proxy/plugin/redirect_to_custom_server.py b/proxy/plugin/redirect_to_custom_server.py
index 75d3835..bb500d3 100644
--- a/proxy/plugin/redirect_to_custom_server.py
+++ b/proxy/plugin/redirect_to_custom_server.py
@@ -13,7 +13,6 @@ from typing import Optional
 
 from ..http.proxy import HttpProxyBasePlugin
 from ..http.parser import HttpParser
-from ..http.methods import httpMethods
 
 
 class RedirectToCustomServerPlugin(HttpProxyBasePlugin):
@@ -25,7 +24,7 @@ class RedirectToCustomServerPlugin(HttpProxyBasePlugin):
             self, request: HttpParser,
     ) -> Optional[HttpParser]:
         # Redirect all non-https requests to inbuilt WebServer.
-        if request.method != httpMethods.CONNECT:
+        if not request.is_https_tunnel():
             request.set_url(self.UPSTREAM_SERVER)
             # Update Host header too, otherwise upstream can reject our request
             if request.has_header(b'Host'):
diff --git a/proxy/proxy.py b/proxy/proxy.py
index 9e4c116..706f864 100644
--- a/proxy/proxy.py
+++ b/proxy/proxy.py
@@ -35,6 +35,10 @@ flags.add_argument(
     help='Prints proxy.py version.',
 )
 
+# TODO: Convert me into 1-letter choices
+# TODO: Add --verbose option which also
+# starts to log traffic flowing between
+# clients and upstream servers.
 flags.add_argument(
     '--log-level',
     type=str,
@@ -68,9 +72,11 @@ flags.add_argument(
 
 flags.add_argument(
     '--plugins',
-    type=str,
+    action='append',
+    nargs='+',
     default=DEFAULT_PLUGINS,
-    help='Comma separated plugins',
+    help='Comma separated plugins.  ' +
+    'You may use --plugins flag multiple times.',
 )
 
 # TODO: Ideally all `--enable-*` flags must be at the top-level.


Test patch:
diff --git a/tests/http/test_http_parser.py b/tests/http/test_http_parser.py
index b5ede79..3d3ba8f 100644
--- a/tests/http/test_http_parser.py
+++ b/tests/http/test_http_parser.py
@@ -24,6 +24,7 @@ class TestHttpParser(unittest.TestCase):
 
     def test_urlparse(self) -> None:
         self.parser.parse(b'CONNECT httpbin.org:443 HTTP/1.1\r\n')
+        self.assertTrue(self.parser.is_https_tunnel())
         self.assertEqual(self.parser.host, b'httpbin.org')
         self.assertEqual(self.parser.port, 443)