Repository: aio-libs/aiosmtpd

Issue:
Exception when parsing binary data
aiosmtpd will try to ASCII decode binary data as SMTP commands, raising a `UnicodeDecodeError`.  A simple test case that triggers this is shown at the end of this report.  This can be caught by a generic exception handler, however it would be better to do this inside the library.

This can be trivially fixed with a `try`/`except` handler.  PR incoming.

``` python
from base64 import b64decode
import socket


TCP_IP = '127.0.0.1'
TCP_PORT = 8025
BUFFER_SIZE = 1024

RAW = "gUMBAwMBGgAAACAAwDAAwCwAwCgAwCQAwBQAwAoAwCIAwCEAAKMAAJ8AAGsAAGoAADkAADgAAIgAAIcAwBkAwCAAwDIAwC4AwCoAwCYAwA8AwAUAAJ0AAD0AADUAAIQAwBIAwAgAwBwAwBsAABYAABMAwBcAwBoAwA0AwAMAAAoHAMAAwC8AwCsAwCcAwCMAwBMAwAkAwB8AwB4AAKIAAJ4AAGcAAEAAADMAADIAAJoAAJkAAEUAAEQAwBgAwB0AwDEAwC0AwCkAwCUAwA4AwAQAAJwAADwAAC8AAJYAAEEAAAcAwBEAwAcAwBYAwAwAwAIAAAUAAAQFAIADAIABAIAAABUAABIAAAkGAEAAABQAABEAAAgAAAYAAAMEAIACAIAAAP8cXF6WB1DBTAUUZfksmYhwy/mtOvciiLZb+ZNMaF/tYg=="
MESSAGE = b64decode(RAW)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TCP_IP, TCP_PORT))
data = s.recv(BUFFER_SIZE)
print("Received: {}".format(data))
s.send(MESSAGE)
data = s.recv(BUFFER_SIZE)
print("Received: {}".format(data))
s.close()
```

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 6f1e947..8fb569e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,3 +12,5 @@ matrix:
 script:
   - tox -e $INTERP-nocov,$INTERP-cov,qa,docs
   - 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then tox -e $INTERP-diffcov; fi'
+before_script:
+  - echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6
diff --git a/aiosmtpd/smtp.py b/aiosmtpd/smtp.py
index 681732a..24661a6 100644
--- a/aiosmtpd/smtp.py
+++ b/aiosmtpd/smtp.py
@@ -234,10 +234,20 @@ class SMTP(asyncio.StreamReaderProtocol):
                 # re-encoded back to the original bytes when the SMTP command
                 # is handled.
                 if i < 0:
-                    command = line.upper().decode(encoding='ascii')
+                    try:
+                        command = line.upper().decode(encoding='ascii')
+                    except UnicodeDecodeError:
+                        await self.push('500 Error: bad syntax')
+                        continue
+
                     arg = None
                 else:
-                    command = line[:i].upper().decode(encoding='ascii')
+                    try:
+                        command = line[:i].upper().decode(encoding='ascii')
+                    except UnicodeDecodeError:
+                        await self.push('500 Error: bad syntax')
+                        continue
+
                     arg = line[i+1:].strip()
                     # Remote SMTP servers can send us UTF-8 content despite
                     # whether they've declared to do so or not.  Some old


Test patch:
diff --git a/aiosmtpd/tests/test_smtp.py b/aiosmtpd/tests/test_smtp.py
index be1da96..975f252 100644
--- a/aiosmtpd/tests/test_smtp.py
+++ b/aiosmtpd/tests/test_smtp.py
@@ -197,6 +197,20 @@ class TestSMTP(unittest.TestCase):
         self.addCleanup(controller.stop)
         self.address = (controller.hostname, controller.port)
 
+    def test_binary(self):
+        with SMTP(*self.address) as client:
+            client.sock.send(b"\x80FAIL\r\n")
+            code, response = client.getreply()
+            self.assertEqual(code, 500)
+            self.assertEqual(response, b'Error: bad syntax')
+
+    def test_binary_space(self):
+        with SMTP(*self.address) as client:
+            client.sock.send(b"\x80 FAIL\r\n")
+            code, response = client.getreply()
+            self.assertEqual(code, 500)
+            self.assertEqual(response, b'Error: bad syntax')
+
     def test_helo(self):
         with SMTP(*self.address) as client:
             code, response = client.helo('example.com')