Skip to content

Commit 3f4555d

Browse files
committed
Add implicit_tls connect arg to support non-standard implicit TLS connections, such as Google Cloud SQL
fixes #757
1 parent a09398f commit 3f4555d

File tree

9 files changed

+221
-23
lines changed

9 files changed

+221
-23
lines changed

.github/workflows/ci-cd.yml

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,11 @@ jobs:
449449
options: '--name=mysqld'
450450
env:
451451
MYSQL_ROOT_PASSWORD: rootpw
452+
haproxy:
453+
image: haproxytech/haproxy-alpine:2.6
454+
ports:
455+
- 13306:13306
456+
options: '--name=haproxy'
452457

453458
steps:
454459
- name: Setup Python ${{ matrix.py }}
@@ -569,6 +574,14 @@ jobs:
569574
# unfortunately we need this hacky workaround as GitHub Actions service containers can't reference data from our repo.
570575
- name: Prepare mysql
571576
run: |
577+
# inject HAproxy configuration
578+
docker container stop haproxy
579+
580+
docker container cp "${{ github.workspace }}/tests/ssl_resources/haproxy.cfg" haproxy:/usr/local/etc/haproxy/haproxy.cfg
581+
docker container cp "${{ github.workspace }}/tests/ssl_resources/ssl/server-combined.pem" haproxy:/usr/local/etc/haproxy/haproxy.pem
582+
583+
docker container start haproxy
584+
572585
# ensure server is started up
573586
while :
574587
do
@@ -598,11 +611,30 @@ jobs:
598611
599612
mysql -h127.0.0.1 -uroot "-p$MYSQL_ROOT_PASSWORD" -e "SET GLOBAL local_infile=on"
600613
614+
# ensure we can login as root from any ip, as haproxy will not connect from localhost.
615+
# at least on MySQL containers our account will be restricted to localhost by default.
616+
mysql -h127.0.0.1 -uroot "-p$MYSQL_ROOT_PASSWORD" -e "DROP USER 'root'@'%'"
617+
mysql -h127.0.0.1 -uroot "-p$MYSQL_ROOT_PASSWORD" -e "RENAME USER 'root'@'localhost' TO 'root'@'%'"
618+
mysql -h127.0.0.1 -uroot "-p$MYSQL_ROOT_PASSWORD" -e "FLUSH PRIVILEGES"
619+
601620
- name: Run tests
602-
run: |
603-
# timeout ensures a more or less clean stop by sending a KeyboardInterrupt which will still provide useful logs
604-
timeout --preserve-status --signal=INT --verbose 570s \
605-
pytest --capture=no --verbosity 2 --cov-report term --cov-report xml --cov aiomysql --cov tests ./tests --mysql-unix-socket "unix-${{ join(matrix.db, '') }}=/tmp/run-${{ join(matrix.db, '-') }}/mysql.sock" --mysql-address "tcp-${{ join(matrix.db, '') }}=127.0.0.1:3306"
621+
# timeout ensures a more or less clean stop by sending a KeyboardInterrupt which will still provide useful logs
622+
run: >-
623+
timeout
624+
--preserve-status
625+
--signal=INT
626+
--verbose 570s
627+
pytest
628+
--capture=no
629+
--verbosity 2
630+
--cov-report term
631+
--cov-report xml
632+
--cov aiomysql
633+
--cov tests
634+
./tests
635+
--mysql-unix-socket "unix-${{ join(matrix.db, '') }}=/tmp/run-${{ join(matrix.db, '-') }}/mysql.sock"
636+
--mysql-address "tcp-${{ join(matrix.db, '') }}=127.0.0.1:3306"
637+
--mysql-address-tls "tls-${{ join(matrix.db, '') }}=127.0.0.1:13306"
606638
env:
607639
PYTHONUNBUFFERED: 1
608640
timeout-minutes: 10

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ next (unreleased)
88

99
* Remove deprecated Pool.get #706
1010

11+
* Add `implicit_tls` connect arg to support non-standard implicit TLS connections, such as Google Cloud SQL #757
12+
1113
0.1.1 (2022-05-08)
1214
^^^^^^^^^^^^^^^^^^
1315

aiomysql/connection.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def connect(host="localhost", user=None, password="",
5353
connect_timeout=None, read_default_group=None,
5454
autocommit=False, echo=False,
5555
local_infile=False, loop=None, ssl=None, auth_plugin='',
56-
program_name='', server_public_key=None):
56+
program_name='', server_public_key=None, implicit_tls=False):
5757
"""See connections.Connection.__init__() for information about
5858
defaults."""
5959
coro = _connect(host=host, user=user, password=password, db=db,
@@ -66,7 +66,8 @@ def connect(host="localhost", user=None, password="",
6666
read_default_group=read_default_group,
6767
autocommit=autocommit, echo=echo,
6868
local_infile=local_infile, loop=loop, ssl=ssl,
69-
auth_plugin=auth_plugin, program_name=program_name)
69+
auth_plugin=auth_plugin, program_name=program_name,
70+
implicit_tls=implicit_tls)
7071
return _ConnectionContextManager(coro)
7172

7273

@@ -142,7 +143,7 @@ def __init__(self, host="localhost", user=None, password="",
142143
connect_timeout=None, read_default_group=None,
143144
autocommit=False, echo=False,
144145
local_infile=False, loop=None, ssl=None, auth_plugin='',
145-
program_name='', server_public_key=None):
146+
program_name='', server_public_key=None, implicit_tls=False):
146147
"""
147148
Establish a connection to the MySQL database. Accepts several
148149
arguments:
@@ -184,6 +185,9 @@ def __init__(self, host="localhost", user=None, password="",
184185
handshaking with MySQL. (omitted by default)
185186
:param server_public_key: SHA256 authentication plugin public
186187
key value.
188+
:param implicit_tls: Establish TLS immediately, skipping non-TLS
189+
preamble before upgrading to TLS.
190+
(default: False)
187191
:param loop: asyncio loop
188192
"""
189193
self._loop = loop or asyncio.get_event_loop()
@@ -218,6 +222,7 @@ def __init__(self, host="localhost", user=None, password="",
218222
self._auth_plugin_used = ""
219223
self._secure = False
220224
self.server_public_key = server_public_key
225+
self._implicit_tls = implicit_tls
221226
self.salt = None
222227

223228
from . import __version__
@@ -241,7 +246,7 @@ def __init__(self, host="localhost", user=None, password="",
241246
self.use_unicode = use_unicode
242247

243248
self._ssl_context = ssl
244-
if ssl:
249+
if ssl and not implicit_tls:
245250
client_flag |= CLIENT.SSL
246251

247252
self._encoding = charset_by_name(self._charset).encoding
@@ -536,7 +541,8 @@ async def _connect(self):
536541

537542
self._next_seq_id = 0
538543

539-
await self._get_server_information()
544+
if not self._implicit_tls:
545+
await self._get_server_information()
540546
await self._request_authentication()
541547

542548
self.connected_time = self._loop.time()
@@ -727,7 +733,8 @@ async def _execute_command(self, command, sql):
727733

728734
async def _request_authentication(self):
729735
# https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse
730-
if int(self.server_version.split('.', 1)[0]) >= 5:
736+
# FIXME: change this before merge
737+
if self._implicit_tls or int(self.server_version.split('.', 1)[0]) >= 5:
731738
self.client_flag |= CLIENT.MULTI_RESULTS
732739

733740
if self.user is None:
@@ -737,8 +744,10 @@ async def _request_authentication(self):
737744
data_init = struct.pack('<iIB23s', self.client_flag, MAX_PACKET_LEN,
738745
charset_id, b'')
739746

740-
if self._ssl_context and self.server_capabilities & CLIENT.SSL:
741-
self.write_packet(data_init)
747+
if self._ssl_context and \
748+
(self._implicit_tls or self.server_capabilities & CLIENT.SSL):
749+
if not self._implicit_tls:
750+
self.write_packet(data_init)
742751

743752
# Stop sending events to data_received
744753
self._writer.transport.pause_reading()
@@ -760,6 +769,9 @@ async def _request_authentication(self):
760769
server_hostname=self._host
761770
)
762771

772+
if self._implicit_tls:
773+
await self._get_server_information()
774+
763775
self._secure = True
764776

765777
if isinstance(self.user, str):

docs/connection.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Example::
4747
connect_timeout=None, read_default_group=None,
4848
autocommit=False, echo=False
4949
ssl=None, auth_plugin='', program_name='',
50-
server_public_key=None, loop=None)
50+
server_public_key=None, loop=None, implicit_tls=False)
5151

5252
A :ref:`coroutine <coroutine>` that connects to MySQL.
5353

@@ -93,6 +93,11 @@ Example::
9393
``sys.argv[0]`` is no longer passed by default
9494
:param server_public_key: SHA256 authenticaiton plugin public key value.
9595
:param loop: asyncio event loop instance or ``None`` for default one.
96+
:param implicit_tls: Establish TLS immediately, skipping non-TLS
97+
preamble before upgrading to TLS.
98+
(default: False)
99+
100+
.. versionadded:: 0.2
96101
:returns: :class:`Connection` instance.
97102

98103

tests/conftest.py

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import gc
33
import os
44
import re
5+
import socket
56
import ssl
67
import sys
78

@@ -63,13 +64,26 @@ def pytest_generate_tests(metafunc):
6364

6465
if ":" in addr:
6566
addr = addr.split(":", 1)
66-
mysql_addresses.append((addr[0], int(addr[1])))
67+
mysql_addresses.append((addr[0], int(addr[1]), False))
6768
else:
68-
mysql_addresses.append((addr, 3306))
69+
mysql_addresses.append((addr, 3306, False))
70+
71+
opt_mysql_address_tls =\
72+
list(metafunc.config.getoption("mysql_address_tls"))
73+
for i in range(len(opt_mysql_address_tls)):
74+
if "=" in opt_mysql_address_tls[i]:
75+
label, addr = opt_mysql_address_tls[i].split("=", 1)
76+
ids.append(label)
77+
else:
78+
addr = opt_mysql_address_tls[i]
79+
ids.append("tls{}".format(i))
80+
81+
addr = addr.split(":", 1)
82+
mysql_addresses.append((addr[0], int(addr[1]), True))
6983

7084
# default to connecting to localhost
7185
if len(mysql_addresses) == 0:
72-
mysql_addresses = [("127.0.0.1", 3306)]
86+
mysql_addresses = [("127.0.0.1", 3306, False)]
7387
ids = ["tcp-local"]
7488

7589
assert len(mysql_addresses) == len(set(mysql_addresses)), \
@@ -153,6 +167,12 @@ def pytest_addoption(parser):
153167
default=[],
154168
help="list of addresses to connect to: [name=]host[:port]",
155169
)
170+
parser.addoption(
171+
"--mysql-address-tls",
172+
action="append",
173+
default=[],
174+
help="list of addresses to connect to using implicit TLS: [name=]host:port",
175+
)
156176
parser.addoption(
157177
"--mysql-unix-socket",
158178
action="append",
@@ -249,6 +269,7 @@ def _register_table(table_name):
249269
@pytest.fixture(scope='session')
250270
def mysql_server(mysql_address):
251271
unix_socket = type(mysql_address) is str
272+
implicit_tls = not unix_socket and mysql_address[2]
252273

253274
if not unix_socket:
254275
ssl_directory = os.path.join(os.path.dirname(__file__),
@@ -270,14 +291,34 @@ def mysql_server(mysql_address):
270291
else:
271292
server_params["host"] = mysql_address[0]
272293
server_params["port"] = mysql_address[1]
294+
295+
if not implicit_tls:
273296
server_params["ssl"] = ctx
274297

275298
try:
276-
connection = pymysql.connect(
277-
db='mysql',
278-
charset='utf8mb4',
279-
cursorclass=pymysql.cursors.DictCursor,
280-
**server_params)
299+
if implicit_tls:
300+
sock = ctx.wrap_socket(
301+
socket.create_connection(
302+
(server_params["host"], server_params["port"]),
303+
),
304+
server_hostname=server_params["host"],
305+
)
306+
connection = pymysql.Connection(
307+
db='mysql',
308+
charset='utf8mb4',
309+
cursorclass=pymysql.cursors.DictCursor,
310+
**server_params,
311+
defer_connect=True,
312+
)
313+
connection.connect(sock)
314+
315+
else:
316+
connection = pymysql.connect(
317+
db='mysql',
318+
charset='utf8mb4',
319+
cursorclass=pymysql.cursors.DictCursor,
320+
**server_params,
321+
)
281322

282323
with connection.cursor() as cursor:
283324
cursor.execute("SELECT VERSION() AS version")
@@ -297,7 +338,7 @@ def mysql_server(mysql_address):
297338
pytest.fail("Unable to determine database type from {!r}"
298339
.format(server_version_tuple))
299340

300-
if not unix_socket:
341+
if not unix_socket and not implicit_tls:
301342
cursor.execute("SHOW VARIABLES LIKE '%ssl%';")
302343

303344
result = cursor.fetchall()
@@ -352,6 +393,10 @@ def mysql_server(mysql_address):
352393
except Exception:
353394
pytest.fail("Cannot initialize MySQL environment")
354395

396+
if implicit_tls:
397+
server_params["ssl"] = ctx
398+
server_params["implicit_tls"] = implicit_tls
399+
355400
return {
356401
"conn_params": server_params,
357402
"server_version": server_version,

tests/ssl_resources/haproxy.cfg

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# partially based on https://github.com/haproxytech/haproxy-docker-alpine/blob/8df4bf3078a338759ae484ab26908f7a4ba9484e/2.6/haproxy.cfg
2+
3+
global
4+
log stdout format raw local0
5+
6+
chroot /var/lib/haproxy
7+
pidfile /var/run/haproxy.pid
8+
maxconn 4000
9+
user haproxy
10+
group haproxy
11+
12+
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
13+
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
14+
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
15+
ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384
16+
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
17+
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
18+
19+
20+
defaults
21+
mode tcp
22+
log global
23+
timeout connect 10s
24+
timeout client 1m
25+
timeout server 1m
26+
27+
28+
frontend tcp-13306-front
29+
bind :13306 ssl crt /usr/local/etc/haproxy/haproxy.pem
30+
default_backend tcp-13306-backend
31+
32+
33+
backend tcp-13306-backend
34+
server mysql mysqld:3306
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEogIBAAKCAQEAn1LwhlVDVK3k0W3LgUENsPflTN0zzDa94HWVdV9coN07RCXP
3+
5AWJ0j34c90tEWq/mPKXcQi765RiXgWnr2l8M4/LVvOUv95r+QRbRu9ceWwf3bnh
4+
uMIBGhBIblySEB7PPZ8xoIIAm4WQrg6dpasKbV9lPz6/WCQ20rpKN6adBLzIu6n5
5+
zcs62WPYC6lJE3dUB/mzVkFuRgzCvX2w17xpnv8cg+DiyxB8uwnPishgc0jhaizV
6+
aZqaQuDOY3/WkzNrOe3xGgxPraU+BPCynIl2CgFtN5/qX6BAn9SZ3rJRpbZoW7wD
7+
Ri7gOJXzq/yOr+X9kLDUvCElf3e/mEfjvZOaFQIDAQABAoIBAFNubFP8LEEYut1M
8+
4Kez+EZ22hXRNEG5XN9A095d7LS0hUefgWkH2W9GUmfiJ6qaOvEOAG4Jw9aOoqBX
9+
18LMu2SI5VOIRJnhEKubM21HBSb0jw9eOqy0sz0Bz9wzD63vZFkBl0xVJ5pJbEUp
10+
lDZgBhrWPL/MzQiMFkVtllXkIw+KNRIokV0HJn0VNUm+ORaDO0TnTAiqL7Wv553+
11+
lWvGaeI4NpMZOPtlgqym1neQmllkeB07pSEtTopO0iINwuTuDUU7IMleN4eOomfh
12+
GwirEHUban8BDL5djckS0GrkUq9EuJbvjEikHAFuNwz7D1Sn3LsDYygD6pKWoXxh
13+
8Ng/AZUCgYEAy1ma5xKvRZZ6QUFssZjtwbruxhj0j3lPA3t9VGlp4SQb3g6FgeOV
14+
6dyJLuuFfSLT9ps+k8D1Er+v85OqO0IM8TR12rPjWMzSjBz/xHb+0uRbEoPwArfn
15+
wcPj3NN6M+tcZH//djogrpphN7u+BudBG6YWzUOQjLwdStM52s8hFD8CgYEAyJM2
16+
M5AWPYL57CI1lqzN135aS6OFyG4N2+rtEWEGAmoeP+NoSGFmQPLTWBdT4ZOGl59/
17+
fSBrWRKted6/H9frjZuSdsGXFMb7e71DDyYZq7tJbYEExc0a6BePINLCwLCIMKHj
18+
PcoPGVsdQXfZK17+qzACwDNbEis/J3H7xcPv7KsCgYBO+SG7k/oV4HbiWPJJlsbf
19+
ciXBMXfpMIeLJq5p1faUxV09RA59f1F9XXS5kCZrjtca8ve+kjWbbm56/mIiWWiF
20+
VIZgxXQJzKIIYErEliIo7R6hdjQEGkAbdGROIqNW/pUHQt6Hn9OJe9M9vd/y9mTG
21+
xB4e4ZqFzZjisl3JqJ+EKQKBgFyJboxDgb9HWj7TWZ32g9FT/hy/iM172PEJZe6K
22+
sNcUVnhrVoVuSlrUrSULPivogEQb1hnIhz5FG7wKRGtQluByUhRwJF/1nbjtDK9E
23+
iLtuYOYgjC8l/a/ujp46Hpf/2hV12v1655RvMQQvYwZbgWtBb0N1biLnyO9N6zbG
24+
uz6ZAoGAXW3mhN9zbN6EgsIlLu+wCdPuC9Vs968gCT612E8Ijul1kiSODG3rbMoG
25+
2FbjqZLahyX8vWVhX/m4xDqO7DXTwi81polfFuxbc/PimOLI4DKq62lWLMBDmUba
26+
X8Bxal3FXLvdcNEplcXadqmxJeXMYnYsfC+MCQhe6im0bGyDOAg=
27+
-----END RSA PRIVATE KEY-----
28+
-----BEGIN CERTIFICATE-----
29+
MIIC/jCCAeYCAQEwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCQVUxEzARBgNV
30+
BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
31+
ZDAeFw0xODAzMjkyMTMzMTFaFw0yODAyMDUyMTMzMTFaMEUxCzAJBgNVBAYTAkFV
32+
MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz
33+
IFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfUvCGVUNU
34+
reTRbcuBQQ2w9+VM3TPMNr3gdZV1X1yg3TtEJc/kBYnSPfhz3S0Rar+Y8pdxCLvr
35+
lGJeBaevaXwzj8tW85S/3mv5BFtG71x5bB/dueG4wgEaEEhuXJIQHs89nzGgggCb
36+
hZCuDp2lqwptX2U/Pr9YJDbSuko3pp0EvMi7qfnNyzrZY9gLqUkTd1QH+bNWQW5G
37+
DMK9fbDXvGme/xyD4OLLEHy7Cc+KyGBzSOFqLNVpmppC4M5jf9aTM2s57fEaDE+t
38+
pT4E8LKciXYKAW03n+pfoECf1JneslGltmhbvANGLuA4lfOr/I6v5f2QsNS8ISV/
39+
d7+YR+O9k5oVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACUWEAWfv3EOy8JmUbWA
40+
jEytJIh/N55hfknninjOBPMv1U1BRixJwXVKlwA8o+8JiacriObWeje2rDUOt6zY
41+
U5DnySQbTYJcJZ9jprqU7VXST7D9NvA0ueLclWTZcqIr/josyhK+l1YbezFYBf41
42+
JQ4PVzkNz9Of4e022qONnlEX0MbtFlcyPEK4yWyXLAhidPAV9QcOCy85vob0+3EE
43+
hmRVVzcTv4Pbzgpee0ZORqozSLzZ3N6RvDyYIczqaytcbyvaQ7GuykE7XvIK5hz0
44+
EM8pwsvxSY1z2yNIw38M8ZOYk18LsEGkf/TyT6eQqymMMD9Qy8rOTsOLfY5eQCf7
45+
pKQ=
46+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)