Repository: encode/uvicorn

Issue:
scope["server"] and scope["client"] must be a two-item iterable
According to the [asgi spec](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope)
> both scope["server"] and scope["client"]  must be either a two-item iterable of [host, port] or None (the default).

The problems is that in certain configurations, where the socket is a unix domain socket (ex. with gunicorn), transport.get_extra_info("sockname") and "peername" will return a string with the unix domain socket.
https://github.com/encode/uvicorn/blob/master/uvicorn/protocols/http/httptools_impl.py#L143-L144

This will break any apps correctly assuming scope["server"] and scope["client"] are two items.

Comments:

Patch:
diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py
index 0628a17..4ae0932 100644
--- a/uvicorn/protocols/http/h11_impl.py
+++ b/uvicorn/protocols/http/h11_impl.py
@@ -5,6 +5,7 @@ import logging
 import time
 import traceback
 from urllib.parse import unquote
+from uvicorn.protocols.utils import get_local_addr, get_remote_addr, is_ssl
 
 import h11
 
@@ -136,9 +137,9 @@ class H11Protocol(asyncio.Protocol):
 
         self.transport = transport
         self.flow = FlowControl(transport)
-        self.server = transport.get_extra_info("sockname")
-        self.client = transport.get_extra_info("peername")
-        self.scheme = "https" if transport.get_extra_info("sslcontext") else "http"
+        self.server = get_local_addr(transport)
+        self.client = get_remote_addr(transport)
+        self.scheme = "https" if is_ssl(transport) else "http"
 
         if self.logger.level <= logging.DEBUG:
             self.logger.debug("%s - Connected", self.client)
diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py
index 29dd27a..14a20f1 100644
--- a/uvicorn/protocols/http/httptools_impl.py
+++ b/uvicorn/protocols/http/httptools_impl.py
@@ -5,6 +5,7 @@ import logging
 import time
 import traceback
 from urllib.parse import unquote
+from uvicorn.protocols.utils import get_local_addr, get_remote_addr, is_ssl
 
 import httptools
 
@@ -140,9 +141,9 @@ class HttpToolsProtocol(asyncio.Protocol):
 
         self.transport = transport
         self.flow = FlowControl(transport)
-        self.server = transport.get_extra_info("sockname")
-        self.client = transport.get_extra_info("peername")
-        self.scheme = "https" if transport.get_extra_info("sslcontext") else "http"
+        self.server = get_local_addr(transport)
+        self.client = get_remote_addr(transport)
+        self.scheme = "https" if is_ssl(transport) else "http"
 
         if self.logger.level <= logging.DEBUG:
             self.logger.debug("%s - Connected", self.client)
diff --git a/uvicorn/protocols/utils.py b/uvicorn/protocols/utils.py
new file mode 100644
index 0000000..1792365
--- /dev/null
+++ b/uvicorn/protocols/utils.py
@@ -0,0 +1,16 @@
+def get_remote_addr(transport):
+    info = transport.get_extra_info("peername")
+    if info is not None and isinstance(info, (list, tuple)) and len(info) == 2:
+        return (str(info[0]), int(info[1]))
+    return None
+
+
+def get_local_addr(transport):
+    info = transport.get_extra_info("sockname")
+    if info is not None and isinstance(info, (list, tuple)) and len(info) == 2:
+        return (str(info[0]), int(info[1]))
+    return None
+
+
+def is_ssl(transport):
+    return bool(transport.get_extra_info("sslcontext"))
diff --git a/uvicorn/protocols/websockets/websockets_impl.py b/uvicorn/protocols/websockets/websockets_impl.py
index 37dd2e1..e8a1688 100644
--- a/uvicorn/protocols/websockets/websockets_impl.py
+++ b/uvicorn/protocols/websockets/websockets_impl.py
@@ -1,4 +1,5 @@
 from urllib.parse import unquote
+from uvicorn.protocols.utils import get_local_addr, get_remote_addr, is_ssl
 import asyncio
 import http
 import logging
@@ -47,9 +48,9 @@ class WebSocketProtocol(websockets.WebSocketServerProtocol):
     def connection_made(self, transport):
         self.connections.add(self)
         self.transport = transport
-        self.server = transport.get_extra_info("sockname")
-        self.client = transport.get_extra_info("peername")
-        self.scheme = "wss" if transport.get_extra_info("sslcontext") else "ws"
+        self.server = get_local_addr(transport)
+        self.client = get_remote_addr(transport)
+        self.scheme = "wss" if is_ssl(transport) else "ws"
         super().connection_made(transport)
 
     def connection_lost(self, exc):
diff --git a/uvicorn/protocols/websockets/wsproto_impl.py b/uvicorn/protocols/websockets/wsproto_impl.py
index 141490b..e4bd9c9 100644
--- a/uvicorn/protocols/websockets/wsproto_impl.py
+++ b/uvicorn/protocols/websockets/wsproto_impl.py
@@ -1,4 +1,5 @@
 from urllib.parse import unquote
+from uvicorn.protocols.utils import get_local_addr, get_remote_addr, is_ssl
 import asyncio
 import h11
 import logging
@@ -47,9 +48,9 @@ class WSProtocol(asyncio.Protocol):
     def connection_made(self, transport):
         self.connections.add(self)
         self.transport = transport
-        self.server = transport.get_extra_info("sockname")
-        self.client = transport.get_extra_info("peername")
-        self.scheme = "wss" if transport.get_extra_info("sslcontext") else "ws"
+        self.server = get_local_addr(transport)
+        self.client = get_remote_addr(transport)
+        self.scheme = "wss" if is_ssl(transport) else "ws"
 
     def connection_lost(self, exc):
         self.connections.remove(self)


Test patch:
diff --git a/tests/protocols/test_utils.py b/tests/protocols/test_utils.py
new file mode 100644
index 0000000..2b5ec63
--- /dev/null
+++ b/tests/protocols/test_utils.py
@@ -0,0 +1,25 @@
+from uvicorn.protocols.utils import get_local_addr, get_remote_addr
+
+
+class MockTransport:
+    def __init__(self, info):
+        self.info = info
+
+    def get_extra_info(self, info_type):
+        return self.info[info_type]
+
+
+def test_get_local_addr():
+    transport = MockTransport({"sockname": "path/to/unix-domain-socket"})
+    assert get_local_addr(transport) == None
+
+    transport = MockTransport({"sockname": ['123.45.6.7', 123]})
+    assert get_local_addr(transport) == ('123.45.6.7', 123)
+
+
+def test_get_remote_addr():
+    transport = MockTransport({"peername": None})
+    assert get_remote_addr(transport) == None
+
+    transport = MockTransport({"peername": ['123.45.6.7', 123]})
+    assert get_remote_addr(transport) == ('123.45.6.7', 123)