Skip to content

Commit

Permalink
Add support for clientinfo
Browse files Browse the repository at this point in the history
Changelog entry:

* Tell the server more about the connecting client: hostname,
  application name, pymonetdb version, process id and an optional remark.
  This information will show up in the `sys.sessions` table.
  Configurable with the new settings `client_info`, `client_application`
  and `client_remark`.
  • Loading branch information
joerivanruth committed Jun 12, 2024
1 parent a4cabfb commit abfd659
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

changes since 1.8.1

* Tell the server more about the connecting client: hostname,
application name, pymonetdb version, process id and an optional remark.
This information will show up in the `sys.sessions` table.
Configurable with the new settings `client_info`, `client_application`
and `client_remark`.


# 1.8.1

Expand Down
20 changes: 19 additions & 1 deletion pymonetdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
#
# Copyright 1997 - July 2008 CWI, August 2008 - 2016 MonetDB B.V.

# Set __version__ first, so the imported modules can access it.
__version__ = '1.8.2a0'


from typing import Optional
from pymonetdb import sql
from pymonetdb import mapi
Expand All @@ -26,7 +30,6 @@
from pymonetdb.filetransfer.directoryhandler import SafeDirectoryHandler
from pymonetdb.target import Target, looks_like_url

__version__ = '1.8.2a0'

apilevel = "2.0"
threadsafety = 1
Expand Down Expand Up @@ -65,6 +68,9 @@ def connect( # noqa C901
clientcert: Optional[str] = None,
schema: Optional[str] = None,
timezone: Optional[int] = None,
client_info: Optional[bool] = None,
client_application: Optional[str] = None,
client_remark: Optional[str] = None,
dangerous_tls_nocheck: Optional[str] = None,
):
"""Set up a connection to a MonetDB SQL database
Expand Down Expand Up @@ -121,6 +127,12 @@ def connect( # noqa C901
the schema to select after connecting
timezone : int
the time zone to use, in minutes east of UTC
client_info : bool
whether to send client details when connecting
client_application : str
application name to send in the client details
client_remark : str
additional info to send in the client details
dangerous_tls_nocheck : str
comma-separated list of TLS certificate checks to skip during connecting:
'host': ignore host name mismatch,
Expand Down Expand Up @@ -171,6 +183,12 @@ def connect( # noqa C901
target.schema = schema
if timezone is not None:
target.timezone = timezone
if client_info is not None:
target.client_info = client_info
if client_application is not None:
target.client_application = client_application
if client_remark is not None:
target.client_remark = client_remark
if dangerous_tls_nocheck is not None:
target.dangerous_tls_nocheck = dangerous_tls_nocheck

Expand Down
34 changes: 33 additions & 1 deletion pymonetdb/mapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@


import os
import platform
import re
import socket
import logging
import struct
import hashlib
import ssl
import sys
import typing
from typing import Callable, List, Optional, Tuple, Union
from typing import Callable, Dict, List, Optional, Tuple, Union

# import pymonetdb
from pymonetdb.exceptions import OperationalError, DatabaseError, \
ProgrammingError, NotSupportedError, IntegrityError
from pymonetdb.target import Target
from pymonetdb import __version__

if typing.TYPE_CHECKING:
from pymonetdb.filetransfer.downloads import Downloader
Expand Down Expand Up @@ -98,6 +102,7 @@ class Connection(object):
is_raw_control: Optional[bool] = None
handshake_options_callback: Optional[Callable[[int], List['HandshakeOption']]] = None
remaining_handshake_options: List['HandshakeOption'] = []
clientinfo: Optional[Dict[str, Optional[str]]] = None
uploader: Optional['Uploader'] = None
downloader: Optional['Downloader'] = None
stashed_buffer: Optional[bytearray] = None
Expand Down Expand Up @@ -154,6 +159,18 @@ def connect(self, database: Optional[Union[Target, str]] = None, *args, **kwargs
# handle during the handshake
self.state = STATE_READY

if self.clientinfo:
info = "".join(
f"{k}={v or ''}\n"
for k, v in self.clientinfo.items()
if v is None or '\n' not in v
)
if info:
try:
self.cmd(f"Xclientinfo {info}")
except OperationalError as e:
logger.warn(f"Server rejected clientinfo: {e}")

for opt in self.remaining_handshake_options:
opt.fallback(opt.value)

Expand Down Expand Up @@ -578,6 +595,21 @@ def _challenge_response(self, challenge: str): # noqa: C901
assert part.startswith('BINARY=')
self.binexport_level = int(part[7:])

if len(challenges) >= 10:
part = challenges[9]
assert part == "CLIENTINFO"
if self.target.client_info:
application_name = self.target.client_application
if not application_name and sys.argv:
application_name = os.path.basename(sys.argv[0])
self.clientinfo = dict(
ClientHostName=platform.node() or None,
ApplicationName=application_name or None,
ClientLibrary=f"pymonetdb {__version__}",
ClientRemark=self.target.client_remark or None,
ClientPid=str(os.getpid()),
)

callback = self.handshake_options_callback
handshake_options = callback(self.binexport_level) if callback else []

Expand Down
11 changes: 9 additions & 2 deletions pymonetdb/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ def looks_like_url(text: str) -> bool:
'sock', 'sockdir', 'sockprefix', 'cert', 'certhash', 'clientkey', 'clientcert',
'user', 'password', 'language', 'autocommit', 'schema', 'timezone',
'binary', 'replysize', 'fetchsize', 'maxprefetch',
'connect_timeout']
)
'connect_timeout',
'client_info', 'client_application', 'client_remark',
])
IGNORED = set(['hash', 'debug', 'logfile'])
VIRTUAL = set([
'connect_scan', 'connect_sockdir',
Expand Down Expand Up @@ -73,6 +74,9 @@ def looks_like_url(text: str) -> bool:
fetchsize=None,
maxprefetch=None,
connect_timeout=0,
client_info=True,
client_application="",
client_remark="",
dangerous_tls_nocheck="",
)

Expand Down Expand Up @@ -172,6 +176,9 @@ def clone(self):
'rows beyond this limit are retrieved on demand, <1 means unlimited')
maxprefetch = urlparam('maxprefetch', 'integer', 'specific to pymonetdb')
connect_timeout = urlparam('connect_timeout', 'integer', 'abort if connect takes longer than this')
client_info = urlparam('client_info', 'bool', 'whether to send client details when connecting')
client_application = urlparam('client_application', 'string', 'application name to send in client details')
client_remark = urlparam('client_remark', 'string', 'application name to send in client details')
dangerous_tls_nocheck = urlparam(
'dangerous_tls_nocheck', 'bool',
'comma separated certificate checks to skip, host: do not verify host, cert: do not verify certificate chain')
Expand Down
73 changes: 73 additions & 0 deletions tests/test_clientinfo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright 1997 - July 2008 CWI, August 2008 - 2016 MonetDB B.V.

from unittest import TestCase, skipUnless
import pymonetdb

from tests.util import have_monetdb_version_at_least, test_url

QUERY = """\
SELECT language, peer, hostname, application, client, clientpid, remark
FROM sys.sessions
WHERE sessionid = sys.current_sessionid()
"""

SERVER_HAS_CLIENTINFO = have_monetdb_version_at_least(11, 51, 0)


@skipUnless(SERVER_HAS_CLIENTINFO, "server does not support clientinfo")
class TestClientInfo(TestCase):

def get_clientinfo(self, **connect_args):
with pymonetdb.connect(test_url, **connect_args) as conn, conn.cursor() as c:
nrows = c.execute(QUERY)
self.assertEqual(nrows, 1)
row = c.fetchone()
d = dict(
(descr.name, v) for descr, v in zip(c.description, row)
)
return d

# 'application': 'python3 -m unittest'
# 'client': 'pymonetdb 1.8.2a0'
# 'clientpid': 2762097
# 'hostname': 'totoro'
# 'language': 'sql'
# 'peer': '<UNIX SOCKET>'
# 'remark': None

def test_default_clientinfo(self):
d = self.get_clientinfo()
self.assertEqual(d['language'], 'sql')
self.assertIsNotNone(d['peer'])
self.assertIsNotNone(d['hostname'])
self.assertIsNotNone(d['application'])
self.assertIn('pymonetdb', d['client'])
self.assertIn(pymonetdb.__version__, d['client'])
self.assertGreater(d['clientpid'], 0)
self.assertIsNone(d['remark'])

def test_suppressed_clientinfo(self):
d = self.get_clientinfo(client_info=False)
self.assertEqual(d['language'], 'sql')
self.assertIsNotNone(d['peer'])
self.assertIsNone(d['hostname'])
self.assertIsNone(d['application'])
self.assertIsNone(d['client'])
self.assertIsNone(d['clientpid'])
self.assertIsNone(d['remark'])

def test_set_application_name(self):
d = self.get_clientinfo(client_application='banana')
self.assertEqual(d['application'], 'banana')
d = self.get_clientinfo(client_info=False, client_application='banana')
self.assertIsNone(d['application'])

def test_set_remark(self):
d = self.get_clientinfo(client_remark='banana')
self.assertEqual(d['remark'], 'banana')
d = self.get_clientinfo(client_info=False, client_remark='banana')
self.assertIsNone(d['remark'])

0 comments on commit abfd659

Please sign in to comment.