Repository: witchard/grole

Issue:
HTTPS connections result in exceptions
To reproduce simply run grole.py and connect to https://127.0.0.1:1234/

ERROR:asyncio:Task exception was never retrieved
future: <Task finished coro=<Grole._handle() done, defined at ./grole.py:336> exception=UnicodeDecodeError('utf-8', b'[Long hex string that I redacted]\n', 4, 5, 'invalid start byte')>
Traceback (most recent call last):
  File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
    result = coro.send(None)
  File "./grole.py", line 349, in _handle
    await req._read(reader)
  File "./grole.py", line 45, in _read
    self.method, self.location, self.version = start_line.decode().split()
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xae in position 4: invalid start byte

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/14246503/badge)](https://coveralls.io/builds/14246503)

Coverage decreased (-0.8%) to 89.64% when pulling **b79589e9daec23c08537795327876a336be505df on errors** into **d47d0ec83f7d76912d5b6b00fd130d79f892939c on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14246507/badge)](https://coveralls.io/builds/14246507)

Coverage increased (+0.1%) to 90.583% when pulling **b79589e9daec23c08537795327876a336be505df on errors** into **d47d0ec83f7d76912d5b6b00fd130d79f892939c on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14246523/badge)](https://coveralls.io/builds/14246523)

Coverage increased (+5.6%) to 96.035% when pulling **ed28971b3184dcc7a9a4275edd8fc66ac7d1bd61 on errors** into **d47d0ec83f7d76912d5b6b00fd130d79f892939c on master**.


Patch:
diff --git a/grole.py b/grole.py
index 72eba4a..bd9ed83 100755
--- a/grole.py
+++ b/grole.py
@@ -376,6 +376,9 @@ class Grole:
                 self._logger.info('{}: {} -> {}'.format(peer, req.path,  res.code))
         except EOFError:
             self._logger.debug('Connection closed from {}'.format(peer))
+        except Exception as e:
+            self._logger.error('Connection error ({}) from {}'.format(e, peer))
+            writer.close()
 
     def run(self, host='localhost', port=1234):
         """
@@ -389,7 +392,11 @@ class Grole:
         # Setup loop
         loop = asyncio.get_event_loop()
         coro = asyncio.start_server(self._handle, host, port, loop=loop)
-        server = loop.run_until_complete(coro)
+        try:
+            server = loop.run_until_complete(coro)
+        except Exception as e:
+            self._logger.error('Could not launch server: {}'.format(e))
+            return
 
         # Run the server
         self._logger.info('Serving on {}'.format(server.sockets[0].getsockname()))


Test patch:
diff --git a/test/helpers.py b/test/helpers.py
index 51c1394..85f307b 100644
--- a/test/helpers.py
+++ b/test/helpers.py
@@ -32,3 +32,19 @@ class FakeWriter():
 
     def get_extra_info(self, arg):
         return 'fake'
+
+class ErrorWriter():
+    def __init__(self):
+        self.closed = False
+
+    async def drain(self):
+        return
+
+    def write(self, data):
+        raise Exception('Broken')
+
+    def get_extra_info(self, arg):
+        return 'fake'
+
+    def close(self):
+        self.closed = True
diff --git a/test/test_grole.py b/test/test_grole.py
index 26eb5e7..02a7a6d 100644
--- a/test/test_grole.py
+++ b/test/test_grole.py
@@ -1,6 +1,6 @@
 import unittest
 import pathlib
-from helpers import FakeReader, FakeWriter, a_wait
+from helpers import *
 
 import grole
 
@@ -43,6 +43,18 @@ class TestGrole(unittest.TestCase):
         data = wr.data.split(b'\r\n')[0]
         self.assertEqual(b'HTTP/1.1 500 Internal Server Error', data)
 
+    def test_close_big_error(self):
+        @self.app.route('/')
+        def error(env, req):
+            a = []
+            return a[1]
+
+        rd = FakeReader(data=b'GET / HTTP/1.1\r\n\r\n')
+        wr = ErrorWriter()
+        a_wait(self.app._handle(rd, wr))
+        self.assertTrue(wr.closed)
+
+
     def test_404(self):
         rd = FakeReader(data=b'GET / HTTP/1.1\r\n\r\n')
         wr = FakeWriter()
diff --git a/test/test_main.py b/test/test_main.py
new file mode 100644
index 0000000..4df7d18
--- /dev/null
+++ b/test/test_main.py
@@ -0,0 +1,17 @@
+import unittest
+
+import grole
+
+class TestMain(unittest.TestCase):
+
+    def test_launch(self):
+        # Success is that it doesn't do anything
+        grole.main(['-p', '80'])
+
+    def test_launch2(self):
+        # Success is that it doesn't do anything
+        grole.main(['-p', '80', '-q'])
+
+    def test_launch3(self):
+        # Success is that it doesn't do anything
+        grole.main(['-p', '80', '-v'])
diff --git a/test/test_serve.py b/test/test_serve.py
index 1e6955c..45a725e 100644
--- a/test/test_serve.py
+++ b/test/test_serve.py
@@ -36,4 +36,11 @@ class TestServe(unittest.TestCase):
                self.assertEqual(html, b'foo\n')
         p.terminate()
 
+    def test_https(self):
+        p = multiprocessing.Process(target=simple_server)
+        p.start()
+        time.sleep(0.1)
+        self.assertRaises(urllib.error.URLError)
+        p.terminate()
+