Repository: pypa/pip

Issue:
Which is the recommended value of trusted host when the URL contains port
**Environment**

* pip version:19.0.3
* Python version:3.7
* OS:macOS

<!-- Feel free to add more information about your environment here -->

**Description**
When the index URL contains port, the behavior of trusted is inconsistent depending on it's HTTP or HTTPS.

For HTTP indexes, Pip complains that trusted host should be sans-port. However, using port doesn't work for HTTPS indexes, throwing an SSL error.

**Expected behavior**
There should only one way to specify trusted host.

**How to Reproduce**
See the following steps. Note for the index URLs and trusted host values.

**Output**

**HTTP**
```bash
$ pip install -i http://localtest.me:5000 urllib3 --trusted-host localtest.me:5000
Looking in indexes: http://localtest.me:5000
Collecting urllib3
  The repository located at localtest.me is not a trusted or secure host and is being ignored. If this repository is available via HTTPS we recommend you use HTTPS instead, otherwise you may silence this warning and allow it anyway with '--trusted-host localtest.me'.
  Could not find a version that satisfies the requirement urllib3 (from versions: )
No matching distribution found for urllib3
```
**HTTPS**
```bash
$ pip install -i https://localtest.me:8088 urllib3 --trusted-host localtest.me
Looking in indexes: https://localtest.me:8088
Collecting urllib3
  Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1076)'))': /urllib3/
```



Discussion:
Can you show the output of the other two of the four cases?

```
$ pip install -i http://localtest.me:5000 urllib3 --trusted-host localtest.me
$ pip install -i https://localtest.me:8088 urllib3 --trusted-host localtest.me:5000
```

They both succeeded so i didn' show in my desc. 
Maybe one approach for this can be--

1. Get `--trusted-host` with no port working for both,
2. Deprecate passing a port to `--trusted-host` (can be done at the same time as (1)), and then
3. Error out if a port is passed to `--trusted-host` after the deprecation period


@cjerdonek I am curious about the reason why w/o port is preferred over the other.

From a rough digging into the code, I found that an insecure host is bypassed using the requests' `mount` method: https://github.com/pypa/pip/blob/34621bf0087c1fa8e2b72807f6704e589c20821c/src/pip/_internal/download.py#L598-L600

At that point, we don't know the exact port of the host, if no port is provided through `trusted-host`. Though there is an easy fix to remove the trailing slash in the URL(the matching uses `startswith` check), however, it will introduce extra risk to match a different host, e.g. `https://example.com` v.s `https://example.com.cn`, which is not expected.

Therefore, it is more straightforward to always use host **with port**. But I am also OK with the decisions made by maintainers if you insist. I am glad to send a PR then.
> I am curious about the reason why w/o port is preferred over the other.

I wasn't expressing a preference but rather just making a suggestion to start talking about possible solutions. There are a couple factors that seem important: (1) Is the port looked at when using `--trusted-host`, and if not, should it be? If the answer to both of these questions is no, then I think it would be better not to accept a port. Otherwise, it gives the user the false impression that the port matters. (2) The option is currently documented as being a "hostname" and doesn't mention a port:

> `--trusted-host <hostname>`: Mark this host as trusted, even though it does not have valid or any HTTPS.
@cjerdonek Makes sense now. `--trusted-host` does expect a host only. PR is on the way.

Resolving patch:
diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py
--- a/src/pip/_internal/download.py
+++ b/src/pip/_internal/download.py
@@ -43,10 +43,12 @@
     ask_password,
     ask_path_exists,
     backup_dir,
+    build_url_from_netloc,
     consume,
     display_path,
     format_size,
     get_installed_version,
+    netloc_has_port,
     path_to_url,
     remove_auth_from_url,
     rmtree,
@@ -608,7 +610,13 @@ def __init__(self, *args, **kwargs):
 
     def add_insecure_host(self, host):
         # type: (str) -> None
-        self.mount('https://{}/'.format(host), self._insecure_adapter)
+        self.mount(build_url_from_netloc(host) + '/', self._insecure_adapter)
+        if not netloc_has_port(host):
+            # Mount wildcard ports for the same host.
+            self.mount(
+                build_url_from_netloc(host) + ':',
+                self._insecure_adapter
+            )
 
     def request(self, method, url, *args, **kwargs):
         # Allow setting a default timeout on a session
diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py
--- a/src/pip/_internal/utils/misc.py
+++ b/src/pip/_internal/utils/misc.py
@@ -1081,6 +1081,27 @@ def path_to_url(path):
     return url
 
 
+def build_url_from_netloc(netloc, scheme='https'):
+    # type: (str, str) -> str
+    """
+    Build a full URL from a netloc.
+    """
+    if netloc.count(':') >= 2 and '@' not in netloc and '[' not in netloc:
+        # It must be a bare IPv6 address, so wrap it with brackets.
+        netloc = '[{}]'.format(netloc)
+    return '{}://{}'.format(scheme, netloc)
+
+
+def netloc_has_port(netloc):
+    # type: (str) -> bool
+    """
+    Return whether the netloc has a port part.
+    """
+    url = build_url_from_netloc(netloc)
+    parsed = urllib_parse.urlparse(url)
+    return bool(parsed.port)
+
+
 def split_auth_from_netloc(netloc):
     """
     Parse out and remove the auth information from a netloc.


Test patch:
diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py
--- a/tests/unit/test_download.py
+++ b/tests/unit/test_download.py
@@ -527,12 +527,16 @@ def test_http_cache_is_not_enabled(self, tmpdir):
 
         assert not hasattr(session.adapters["http://"], "cache")
 
-    def test_insecure_host_cache_is_not_enabled(self, tmpdir):
+    def test_insecure_host_adapter(self, tmpdir):
         session = PipSession(
             cache=tmpdir.joinpath("test-cache"),
             insecure_hosts=["example.com"],
         )
 
+        assert "https://example.com/" in session.adapters
+        # Check that the "port wildcard" is present.
+        assert "https://example.com:" in session.adapters
+        # Check that the cache isn't enabled.
         assert not hasattr(session.adapters["https://example.com/"], "cache")
 
 
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -37,6 +37,7 @@
 )
 from pip._internal.utils.hashes import Hashes, MissingHashes
 from pip._internal.utils.misc import (
+    build_url_from_netloc,
     call_subprocess,
     egg_link_path,
     ensure_dir,
@@ -44,6 +45,7 @@
     get_installed_distributions,
     get_prog,
     make_subprocess_output_error,
+    netloc_has_port,
     normalize_path,
     normalize_version_info,
     path_to_display,
@@ -1223,6 +1225,31 @@ def test_path_to_url_win():
     assert path_to_url('file') == 'file:' + urllib_request.pathname2url(path)
 
 
+@pytest.mark.parametrize('netloc, expected_url, expected_has_port', [
+    # Test domain name.
+    ('example.com', 'https://example.com', False),
+    ('example.com:5000', 'https://example.com:5000', True),
+    # Test IPv4 address.
+    ('127.0.0.1', 'https://127.0.0.1', False),
+    ('127.0.0.1:5000', 'https://127.0.0.1:5000', True),
+    # Test bare IPv6 address.
+    ('2001:DB6::1', 'https://[2001:DB6::1]', False),
+    # Test IPv6 with port.
+    ('[2001:DB6::1]:5000', 'https://[2001:DB6::1]:5000', True),
+    # Test netloc with auth.
+    (
+        'user:password@localhost:5000',
+        'https://user:password@localhost:5000',
+        True
+    )
+])
+def test_build_url_from_netloc_and_netloc_has_port(
+    netloc, expected_url, expected_has_port,
+):
+    assert build_url_from_netloc(netloc) == expected_url
+    assert netloc_has_port(netloc) is expected_has_port
+
+
 @pytest.mark.parametrize('netloc, expected', [
     # Test a basic case.
     ('example.com', ('example.com', (None, None))),