From 517e945266b505a0e52e992583f970e4b0aade4c Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:59:08 +0100 Subject: [PATCH 01/18] Cleanup Signed-off-by: simonmicro --- py-kms/pykms_Sql.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index 841c557..3ed7bfd 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -20,18 +20,12 @@ def sql_initialize(dbName): if not os.path.isfile(dbName): # Initialize the database. loggersrv.debug(f'Initializing database file "{dbName}"...') - con = None try: - con = sqlite3.connect(dbName) - cur = con.cursor() - cur.execute("CREATE TABLE clients(clientMachineId TEXT , machineName TEXT, applicationId TEXT, skuId TEXT, licenseStatus TEXT, lastRequestTime INTEGER, kmsEpid TEXT, requestCount INTEGER, PRIMARY KEY(clientMachineId, applicationId))") - + with sqlite3.connect(dbName) as con: + cur = con.cursor() + cur.execute("CREATE TABLE clients(clientMachineId TEXT, machineName TEXT, applicationId TEXT, skuId TEXT, licenseStatus TEXT, lastRequestTime INTEGER, kmsEpid TEXT, requestCount INTEGER, PRIMARY KEY(clientMachineId, applicationId))") except sqlite3.Error as e: pretty_printer(log_obj = loggersrv.error, to_exit = True, put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e)) - finally: - if con: - con.commit() - con.close() def sql_get_all(dbName): if not os.path.isfile(dbName): From f99416b60e84326d5d34c11c8e46465cf5f4bf8e Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:32:46 +0100 Subject: [PATCH 02/18] Fix warnings due to deprecation Signed-off-by: simonmicro --- py-kms/pykms_Client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py-kms/pykms_Client.py b/py-kms/pykms_Client.py index 142b316..f134ae6 100644 --- a/py-kms/pykms_Client.py +++ b/py-kms/pykms_Client.py @@ -170,7 +170,7 @@ def client_update(): for appitem in appitems: kmsitems = appitem['KmsItems'] for kmsitem in kmsitems: - name = re.sub('\(.*\)', '', kmsitem['DisplayName']) # Remove bracets + name = re.sub(r'\(.*\)', '', kmsitem['DisplayName']) # Remove bracets name = name.replace('2015', '') # Remove specific years name = name.replace(' ', '') # Ignore whitespaces name = name.replace('/11', '', 1) # Cut out Windows 11, as it is basically Windows 10 @@ -328,7 +328,7 @@ def createKmsRequestBase(): requestDict['clientMachineId'] = UUID(uuid.UUID(clt_config['cmid']).bytes_le if (clt_config['cmid'] is not None) else uuid.uuid4().bytes_le) requestDict['previousClientMachineId'] = '\0' * 16 # I'm pretty sure this is supposed to be a null UUID. requestDict['requiredClientCount'] = clt_config['RequiredClientCount'] - requestDict['requestTime'] = dt_to_filetime(datetime.datetime.utcnow()) + requestDict['requestTime'] = dt_to_filetime(datetime.datetime.now(datetime.timezone.utc)) requestDict['machineName'] = (clt_config['machine'] if (clt_config['machine'] is not None) else ''.join(random.choice(string.ascii_letters + string.digits) for i in range(random.randint(2,63)))).encode('utf-16le') requestDict['mnPad'] = '\0'.encode('utf-16le') * (63 - len(requestDict['machineName'].decode('utf-16le'))) From d27a014d960087a79837177da69407c83cf0b586 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:34:40 +0100 Subject: [PATCH 03/18] Added lastRequstIP and renamed appId to applicationId Added db schema migration Rewrite to a more generic ORM column handling Signed-off-by: simonmicro --- py-kms/pykms_Base.py | 9 +- py-kms/pykms_Sql.py | 190 +++++++++++++++++++++++++------------------ 2 files changed, 117 insertions(+), 82 deletions(-) diff --git a/py-kms/pykms_Base.py b/py-kms/pykms_Base.py index e0e9a6b..75ca3a8 100644 --- a/py-kms/pykms_Base.py +++ b/py-kms/pykms_Base.py @@ -193,16 +193,17 @@ could be detected as not genuine !{end}" %currentClientCount) infoDict = { "machineName" : kmsRequest.getMachineName(), "clientMachineId" : str(clientMachineId), - "appId" : appName, + "applicationId" : appName, "skuId" : skuName, "licenseStatus" : kmsRequest.getLicenseStatus(), - "requestTime" : int(time.time()), + "lastRequestIP" : self.srv_config['raddr'][0], # (ip, port) + "lastRequestTime" : int(time.time()), "kmsEpid" : None } loggersrv.info("Machine Name: %s" % infoDict["machineName"]) loggersrv.info("Client Machine ID: %s" % infoDict["clientMachineId"]) - loggersrv.info("Application ID: %s" % infoDict["appId"]) + loggersrv.info("Application ID: %s" % infoDict["applicationId"]) loggersrv.info("SKU ID: %s" % infoDict["skuId"]) loggersrv.info("License Status: %s" % infoDict["licenseStatus"]) loggersrv.info("Request Time: %s" % local_dt.strftime('%Y-%m-%d %H:%M:%S %Z (UTC%z)')) @@ -211,7 +212,7 @@ could be detected as not genuine !{end}" %currentClientCount) loggersrv.mininfo("", extra = {'host': str(self.srv_config['raddr']), 'status' : infoDict["licenseStatus"], 'product' : infoDict["skuId"]}) - # Create database. + # Send change to database. if self.srv_config['sqlite']: sql_update(self.srv_config['sqlite'], infoDict) diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index 3ed7bfd..1fd2fbc 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -1,22 +1,37 @@ #!/usr/bin/env python3 -import datetime +from datetime import datetime import os import logging -# sqlite3 is optional. -try: - import sqlite3 -except ImportError: - pass - -from pykms_Format import pretty_printer - #-------------------------------------------------------------------------------------------------------------------------------------------------------- loggersrv = logging.getLogger('logsrv') +_column_name_to_index = { + 'clientMachineId': 0, + 'machineName': 1, + 'applicationId': 2, + 'skuId': 3, + 'licenseStatus': 4, + 'lastRequestTime': 5, + 'kmsEpid': 6, + 'requestCount': 7, + 'lastRequestIP': 8, +} + +# sqlite3 is optional. +available = False +try: + import sqlite3 + available = True +except ImportError: + pass def sql_initialize(dbName): + if available is False: + loggersrv.info("'sqlite3' module not found! SQLite database support cannot be enabled.") + return + loggersrv.debug(f'SQLite database support enabled. Database file: "{dbName}"') if not os.path.isfile(dbName): # Initialize the database. loggersrv.debug(f'Initializing database file "{dbName}"...') @@ -25,9 +40,37 @@ def sql_initialize(dbName): cur = con.cursor() cur.execute("CREATE TABLE clients(clientMachineId TEXT, machineName TEXT, applicationId TEXT, skuId TEXT, licenseStatus TEXT, lastRequestTime INTEGER, kmsEpid TEXT, requestCount INTEGER, PRIMARY KEY(clientMachineId, applicationId))") except sqlite3.Error as e: - pretty_printer(log_obj = loggersrv.error, to_exit = True, put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e)) + loggersrv.exception("Sqlite Error during database initialization!") + raise + if os.path.isfile(dbName): + # Update database + try: + with sqlite3.connect(dbName) as con: + cur = con.cursor() + # Create simple "metadata" table if not exists. + cur.execute("CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT);") + # Get the current schema version + cur.execute("SELECT value FROM metadata WHERE key='schema_version';") + row = cur.fetchone() + if row is None: + current_version = 0 + else: + current_version = int(row[0]) + loggersrv.debug(f'Current database schema version: {current_version}') + # Apply necessary migrations + if current_version < 1: + # v1: Add "lastRequestIP" column to "clients" table. + loggersrv.info("Upgrading database schema to version 1...") + cur.execute("ALTER TABLE clients ADD COLUMN lastRequestIP TEXT;") + cur.execute("INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '1');") + loggersrv.info("Database schema updated to version 1.") + except sqlite3.Error as e: + loggersrv.exception("Sqlite Error during database upgrade!") + raise def sql_get_all(dbName): + if available is False: + return if not os.path.isfile(dbName): return None with sqlite3.connect(dbName) as con: @@ -35,81 +78,72 @@ def sql_get_all(dbName): cur.execute("SELECT * FROM clients") clients = [] for row in cur.fetchall(): - clients.append({ - 'clientMachineId': row[0], - 'machineName': row[1], - 'applicationId': row[2], - 'skuId': row[3], - 'licenseStatus': row[4], - 'lastRequestTime': datetime.datetime.fromtimestamp(row[5]).isoformat(), - 'kmsEpid': row[6], - 'requestCount': row[7] - }) + loggersrv.debug(f"Row: {row}") + obj = {} + for col_name, index in _column_name_to_index.items(): + if col_name == "lastRequestTime": + obj[col_name] = datetime.fromtimestamp(row[_column_name_to_index['lastRequestTime']]).isoformat() + else: + obj[col_name] = row[index] + loggersrv.debug(f"Obj: {obj}") + clients.append(obj) return clients def sql_update(dbName, infoDict): - con = None + if available is False: + return + + # make sure all column names are present + for col_name in _column_name_to_index.keys(): + if col_name in ["requestCount", "kmsEpid"]: + continue + if col_name not in infoDict: + raise ValueError(f"infoDict is missing required column: {col_name}") + try: - con = sqlite3.connect(dbName) - cur = con.cursor() - cur.execute("SELECT * FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict) - try: + with sqlite3.connect(dbName) as con: + cur = con.cursor() + cur.execute(f"SELECT {', '.join(_column_name_to_index.keys())} FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId;", infoDict) data = cur.fetchone() if not data: - # Insert row. - cur.execute("INSERT INTO clients (clientMachineId, machineName, applicationId, \ -skuId, licenseStatus, lastRequestTime, requestCount) VALUES (:clientMachineId, :machineName, :appId, :skuId, :licenseStatus, :requestTime, 1);", infoDict) - else: - # Update data. - if data[1] != infoDict["machineName"]: - cur.execute("UPDATE clients SET machineName=:machineName WHERE \ -clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict) - if data[2] != infoDict["appId"]: - cur.execute("UPDATE clients SET applicationId=:appId WHERE \ -clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict) - if data[3] != infoDict["skuId"]: - cur.execute("UPDATE clients SET skuId=:skuId WHERE \ -clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict) - if data[4] != infoDict["licenseStatus"]: - cur.execute("UPDATE clients SET licenseStatus=:licenseStatus WHERE \ -clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict) - if data[5] != infoDict["requestTime"]: - cur.execute("UPDATE clients SET lastRequestTime=:requestTime WHERE \ -clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict) - # Increment requestCount - cur.execute("UPDATE clients SET requestCount=requestCount+1 WHERE \ -clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict) + # Insert new row with all given info + infoDict["requestCount"] = 1 + cur.execute(f"""INSERT INTO clients ({', '.join(_column_name_to_index.keys())}) + VALUES ({', '.join(':' + col for col in _column_name_to_index.keys())});""", infoDict) - except sqlite3.Error as e: - pretty_printer(log_obj = loggersrv.error, to_exit = True, - put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e)) - except sqlite3.Error as e: - pretty_printer(log_obj = loggersrv.error, to_exit = True, - put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e)) - finally: - if con: - con.commit() - con.close() + else: + # Update only changed columns + common_postfix = "WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId;" + def update_column_if_changed(column_name, new_value): + assert column_name in _column_name_to_index, f"Unknown column name: {column_name}" + assert "clientMachineId" in infoDict and "applicationId" in infoDict, "infoDict must contain 'clientMachineId' and 'applicationId'" + if data[_column_name_to_index[column_name]] != new_value: + query = f"UPDATE clients SET {column_name}=? {common_postfix}" + cur.execute(query, (new_value, infoDict['clientMachineId'], infoDict['applicationId'])) + + # Dynamically check and maybe up date all columns + for column_name in _column_name_to_index.keys(): + if column_name in ["clientMachineId", "applicationId", "requestCount"]: + continue # Skip these columns + if column_name == "kmsEpid": + # this one can only be updated by the special function + continue + update_column_if_changed(column_name, infoDict[column_name]) + + # Finally increment requestCount + cur.execute(f"UPDATE clients SET requestCount=requestCount+1 {common_postfix}", infoDict) + except sqlite3.Error: + loggersrv.exception("Sqlite Error during sql_update!") def sql_update_epid(dbName, kmsRequest, response, appName): - cmid = str(kmsRequest['clientMachineId'].get()) - con = None - try: - con = sqlite3.connect(dbName) - cur = con.cursor() - cur.execute("SELECT * FROM clients WHERE clientMachineId=? AND applicationId=?;", (cmid, appName)) - try: - data = cur.fetchone() - cur.execute("UPDATE clients SET kmsEpid=? WHERE \ -clientMachineId=? AND applicationId=?;", (str(response["kmsEpid"].decode('utf-16le')), cmid, appName)) + if available is False: + return - except sqlite3.Error as e: - pretty_printer(log_obj = loggersrv.error, to_exit = True, - put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e)) - except sqlite3.Error as e: - pretty_printer(log_obj = loggersrv.error, to_exit = True, - put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e)) - finally: - if con: - con.commit() - con.close() + cmid = str(kmsRequest['clientMachineId'].get()) + try: + with sqlite3.connect(dbName) as con: + cur = con.cursor() + cur.execute("UPDATE clients SET kmsEpid=? WHERE clientMachineId=? AND applicationId=?;", + (str(response["kmsEpid"].decode('utf-16le')), cmid, appName)) + except sqlite3.Error: + loggersrv.exception("Sqlite Error during sql_update_epid!") From 7acc318a37d4486c02eff5d025985fe795e66678 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:35:19 +0100 Subject: [PATCH 04/18] Actually show exceptions during processing Signed-off-by: simonmicro --- py-kms/pykms_Server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/py-kms/pykms_Server.py b/py-kms/pykms_Server.py index 9298c4a..90c8de0 100755 --- a/py-kms/pykms_Server.py +++ b/py-kms/pykms_Server.py @@ -12,6 +12,7 @@ import threading import socketserver import queue as Queue import selectors +import traceback from time import monotonic as time import pykms_RpcBind, pykms_RpcRequest @@ -124,7 +125,8 @@ class KeyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): put_text = "{reverse}{red}{bold}Server connection timed out. Exiting...{end}") def handle_error(self, request, client_address): - pass + pretty_printer(log_obj = loggersrv.error, + put_text = "{reverse}{red}{bold}Exception happened during processing of request from %s:\n%s{end}" % (str(client_address), traceback.format_exc())) class server_thread(threading.Thread): From 00339f69ccc37387e3f991758a1acfa9e838d032 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:35:58 +0100 Subject: [PATCH 05/18] Adapt new sqlite-availability check Signed-off-by: simonmicro --- py-kms/pykms_Server.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/py-kms/pykms_Server.py b/py-kms/pykms_Server.py index 90c8de0..ccaa025 100755 --- a/py-kms/pykms_Server.py +++ b/py-kms/pykms_Server.py @@ -15,7 +15,7 @@ import selectors import traceback from time import monotonic as time -import pykms_RpcBind, pykms_RpcRequest +import pykms_RpcBind, pykms_RpcRequest, pykms_Sql from pykms_RpcBase import rpcBase from pykms_Dcerpc import MSRPCHeader from pykms_Misc import check_setup, check_lcid, check_other @@ -23,7 +23,6 @@ from pykms_Misc import KmsParser, KmsParserException, KmsParserHelp from pykms_Misc import kms_parser_get, kms_parser_check_optionals, kms_parser_check_positionals, kms_parser_check_connect from pykms_Format import enco, deco, pretty_printer, justify from pykms_Connect import MultipleListener -from pykms_Sql import sql_initialize srv_version = "py-kms_2020-10-01" __license__ = "The Unlicense" @@ -381,13 +380,11 @@ def server_check(): put_text = "{reverse}{yellow}{bold}You specified a folder instead of a database file! This behavior is not officially supported anymore, please change your start parameters soon.{end}") srv_config['sqlite'] = os.path.join(srv_config['sqlite'], 'pykms_database.db') - try: - import sqlite3 - sql_initialize(srv_config['sqlite']) - except ImportError: - pretty_printer(log_obj = loggersrv.warning, - put_text = "{reverse}{yellow}{bold}Module 'sqlite3' not installed, database support disabled.{end}") + if pykms_Sql.available: + pykms_Sql.sql_initialize(srv_config['sqlite']) + else: srv_config['sqlite'] = False + # Check other specific server options. opts = [('clientcount', '-c/--client-count'), From bd63e8833573c19bc966f765ec08eef39df4daed Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:36:20 +0100 Subject: [PATCH 06/18] Reformatting and exposed new ip-field Signed-off-by: simonmicro --- py-kms/templates/clients.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/py-kms/templates/clients.html b/py-kms/templates/clients.html index fb3340b..f489389 100644 --- a/py-kms/templates/clients.html +++ b/py-kms/templates/clients.html @@ -57,6 +57,7 @@ th { Application ID SKU ID License Status + Last Address Last Seen KMS EPID Seen Count @@ -65,7 +66,9 @@ th { {% for client in clients %} -
{{ client.clientMachineId }}
+ +
{{ client.clientMachineId }}
+ {% if client.machineName | length > 16 %} {{ client.machineName | truncate(16, True, '...') }} @@ -76,6 +79,7 @@ th { {{ client.applicationId }} {{ client.skuId }} {{ client.licenseStatus }} + {{ client.lastRequestIP or "N/A" }} {{ client.lastRequestTime }} {% if client.kmsEpid | length > 16 %} @@ -95,9 +99,10 @@ th {

Whoops?

- This page seems to be empty, because no clients are available. Try to use the server with a compartible client to add it to the database. + This page seems to be empty, because no clients are available. Try to use the server with a compartible client + to add it to the database.
{% endif %} {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} From 7764232e378bbed0be9fece1176cf0f52ebe7217 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:41:06 +0100 Subject: [PATCH 07/18] Also fix legacy format Signed-off-by: simonmicro --- docker/docker-py3-kms-minimal/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-py3-kms-minimal/Dockerfile b/docker/docker-py3-kms-minimal/Dockerfile index 1807b37..4fb1f36 100644 --- a/docker/docker-py3-kms-minimal/Dockerfile +++ b/docker/docker-py3-kms-minimal/Dockerfile @@ -9,7 +9,7 @@ ENV LCID=1033 ENV CLIENT_COUNT=26 ENV ACTIVATION_INTERVAL=120 ENV RENEWAL_INTERVAL=10080 -ENV HWID RANDOM +ENV HWID=RANDOM ENV LOGLEVEL=INFO ENV LOGFILE=STDOUT ENV LOGSIZE="" From de1bc6eaaa2aa529d47e9b62a7ba563d41901d3a Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:45:21 +0100 Subject: [PATCH 08/18] Typos Signed-off-by: simonmicro --- py-kms/pykms_Client.py | 2 +- py-kms/pykms_Server.py | 1 - py-kms/pykms_Sql.py | 2 +- py-kms/templates/clients.html | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/py-kms/pykms_Client.py b/py-kms/pykms_Client.py index f134ae6..6ff92e0 100644 --- a/py-kms/pykms_Client.py +++ b/py-kms/pykms_Client.py @@ -170,7 +170,7 @@ def client_update(): for appitem in appitems: kmsitems = appitem['KmsItems'] for kmsitem in kmsitems: - name = re.sub(r'\(.*\)', '', kmsitem['DisplayName']) # Remove bracets + name = re.sub(r'\(.*\)', '', kmsitem['DisplayName']) # Remove brackets name = name.replace('2015', '') # Remove specific years name = name.replace(' ', '') # Ignore whitespaces name = name.replace('/11', '', 1) # Cut out Windows 11, as it is basically Windows 10 diff --git a/py-kms/pykms_Server.py b/py-kms/pykms_Server.py index ccaa025..31e7e2e 100755 --- a/py-kms/pykms_Server.py +++ b/py-kms/pykms_Server.py @@ -385,7 +385,6 @@ def server_check(): else: srv_config['sqlite'] = False - # Check other specific server options. opts = [('clientcount', '-c/--client-count'), ('timeoutidle', '-t0/--timeout-idle'), diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index 1fd2fbc..2c9989c 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -121,7 +121,7 @@ def sql_update(dbName, infoDict): query = f"UPDATE clients SET {column_name}=? {common_postfix}" cur.execute(query, (new_value, infoDict['clientMachineId'], infoDict['applicationId'])) - # Dynamically check and maybe up date all columns + # Dynamically check and maybe update all columns for column_name in _column_name_to_index.keys(): if column_name in ["clientMachineId", "applicationId", "requestCount"]: continue # Skip these columns diff --git a/py-kms/templates/clients.html b/py-kms/templates/clients.html index f489389..e2fab38 100644 --- a/py-kms/templates/clients.html +++ b/py-kms/templates/clients.html @@ -99,7 +99,7 @@ th {

Whoops?

- This page seems to be empty, because no clients are available. Try to use the server with a compartible client + This page seems to be empty, because no clients are available. Try to use the server with a compatible client to add it to the database.
From 2210d995c578f7ae1eabf978775b02d99541f8f9 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:48:11 +0100 Subject: [PATCH 09/18] Corrected parameter bindings Signed-off-by: simonmicro --- py-kms/pykms_Sql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index 2c9989c..5a22991 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -118,8 +118,8 @@ def sql_update(dbName, infoDict): assert column_name in _column_name_to_index, f"Unknown column name: {column_name}" assert "clientMachineId" in infoDict and "applicationId" in infoDict, "infoDict must contain 'clientMachineId' and 'applicationId'" if data[_column_name_to_index[column_name]] != new_value: - query = f"UPDATE clients SET {column_name}=? {common_postfix}" - cur.execute(query, (new_value, infoDict['clientMachineId'], infoDict['applicationId'])) + query = f"UPDATE clients SET {column_name}=:value {common_postfix}" + cur.execute(query, {"value": new_value, "clientMachineId": infoDict['clientMachineId'], "applicationId": infoDict['applicationId']}) # Dynamically check and maybe update all columns for column_name in _column_name_to_index.keys(): From d05c99a7adfbb1936086fdf59d1bd20694aedf1a Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:51:49 +0100 Subject: [PATCH 10/18] Add test instructions Signed-off-by: simonmicro --- docs/Contributing.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/Contributing.md b/docs/Contributing.md index 6718bae..45b21a4 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -13,3 +13,12 @@ Awesome! But before you write or modify the existing source code, please note th ``` - Wrap lines only if really long (it does not matter 79 chars return) - For the rest a bit as it comes with a look at [PEP8](https://www.python.org/dev/peps/pep-0008/) :) + +Test your changes, please. For example, run the server via: +```bash +python3 pykms_Server.py -F STDOUT -s ./pykms_database.db +``` +Then trigger (multiple) client requests and check the output for errors via: +```bash +python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b +``` From 12cd3ac783e3b0179ab90b74aab9d676f38b8f2b Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:53:15 +0100 Subject: [PATCH 11/18] Fixed another location of db-ordered fields Signed-off-by: simonmicro --- py-kms/pykms_Sql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index 5a22991..a699576 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -75,7 +75,7 @@ def sql_get_all(dbName): return None with sqlite3.connect(dbName) as con: cur = con.cursor() - cur.execute("SELECT * FROM clients") + cur.execute(f"SELECT {', '.join(_column_name_to_index.keys())} FROM clients") clients = [] for row in cur.fetchall(): loggersrv.debug(f"Row: {row}") @@ -113,7 +113,7 @@ def sql_update(dbName, infoDict): else: # Update only changed columns - common_postfix = "WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId;" + common_postfix = "WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId" def update_column_if_changed(column_name, new_value): assert column_name in _column_name_to_index, f"Unknown column name: {column_name}" assert "clientMachineId" in infoDict and "applicationId" in infoDict, "infoDict must contain 'clientMachineId' and 'applicationId'" From 71a00df471375cade5ffd291456b9ea5bca6082e Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:53:41 +0100 Subject: [PATCH 12/18] Nitpick newline spaces Signed-off-by: simonmicro --- py-kms/pykms_Server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py-kms/pykms_Server.py b/py-kms/pykms_Server.py index 31e7e2e..5420bcd 100755 --- a/py-kms/pykms_Server.py +++ b/py-kms/pykms_Server.py @@ -384,7 +384,7 @@ def server_check(): pykms_Sql.sql_initialize(srv_config['sqlite']) else: srv_config['sqlite'] = False - + # Check other specific server options. opts = [('clientcount', '-c/--client-count'), ('timeoutidle', '-t0/--timeout-idle'), From c747f57dec54819ff97a6aafca95d986c4fa74b2 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:57:19 +0100 Subject: [PATCH 13/18] Also test new clients explicitly :/ Signed-off-by: simonmicro --- docs/Contributing.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Contributing.md b/docs/Contributing.md index 45b21a4..9b836ec 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -20,5 +20,7 @@ python3 pykms_Server.py -F STDOUT -s ./pykms_database.db ``` Then trigger (multiple) client requests and check the output for errors via: ```bash -python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b +python3 pykms_Client.py -F STDOUT # fresh client +python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b # (maybe) existing client +python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b # now-for-sure existing client ``` From f53bbd4f6a6846c503c34d6891790777901ac6ce Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:57:46 +0100 Subject: [PATCH 14/18] Enforce correct empty default value Signed-off-by: simonmicro --- py-kms/pykms_Sql.py | 1 + 1 file changed, 1 insertion(+) diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index a699576..ca70c41 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -107,6 +107,7 @@ def sql_update(dbName, infoDict): data = cur.fetchone() if not data: # Insert new row with all given info + infoDict["kmsEpid"] = "" # Default empty value infoDict["requestCount"] = 1 cur.execute(f"""INSERT INTO clients ({', '.join(_column_name_to_index.keys())}) VALUES ({', '.join(':' + col for col in _column_name_to_index.keys())});""", infoDict) From da80390b731fee53a50537f5f6fe3aa2c095812e Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 21:01:24 +0100 Subject: [PATCH 15/18] Added a simple client test Signed-off-by: simonmicro --- .github/workflows/test_basic_client.yml | 27 +++++++++++++++++++ ...{bake_to_test.yml => test_image_build.yml} | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test_basic_client.yml rename .github/workflows/{bake_to_test.yml => test_image_build.yml} (97%) diff --git a/.github/workflows/test_basic_client.yml b/.github/workflows/test_basic_client.yml new file mode 100644 index 0000000..2f6628d --- /dev/null +++ b/.github/workflows/test_basic_client.yml @@ -0,0 +1,27 @@ +name: "Test: Basic Client" + +on: + workflow_dispatch: + push: + +jobs: + run-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run tests + run: | + cd py-kms; timeout 30 python3 pykms_Server.py -F STDOUT -s ./pykms_database.db & + sleep 5 + python3 pykms_Client.py -F STDOUT # fresh client + python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b # (maybe) existing client + python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b # now-for-sure existing client diff --git a/.github/workflows/bake_to_test.yml b/.github/workflows/test_image_build.yml similarity index 97% rename from .github/workflows/bake_to_test.yml rename to .github/workflows/test_image_build.yml index 72fc196..9256d71 100644 --- a/.github/workflows/bake_to_test.yml +++ b/.github/workflows/test_image_build.yml @@ -1,4 +1,4 @@ -name: Test-Build Docker Image +name: "Test: Build Docker Image" on: workflow_dispatch: From 49fb60fe6bbdf4147919e7feda7f8df14501ebe1 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 21:13:25 +0100 Subject: [PATCH 16/18] Removed now useless error-wrapping Signed-off-by: simonmicro --- py-kms/pykms_Sql.py | 128 ++++++++++++++++++++------------------------ 1 file changed, 58 insertions(+), 70 deletions(-) diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index ca70c41..d163462 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -33,40 +33,34 @@ def sql_initialize(dbName): return loggersrv.debug(f'SQLite database support enabled. Database file: "{dbName}"') if not os.path.isfile(dbName): - # Initialize the database. + # Initialize the database loggersrv.debug(f'Initializing database file "{dbName}"...') - try: - with sqlite3.connect(dbName) as con: - cur = con.cursor() - cur.execute("CREATE TABLE clients(clientMachineId TEXT, machineName TEXT, applicationId TEXT, skuId TEXT, licenseStatus TEXT, lastRequestTime INTEGER, kmsEpid TEXT, requestCount INTEGER, PRIMARY KEY(clientMachineId, applicationId))") - except sqlite3.Error as e: - loggersrv.exception("Sqlite Error during database initialization!") - raise + with sqlite3.connect(dbName) as con: + cur = con.cursor() + cur.execute("CREATE TABLE clients(clientMachineId TEXT, machineName TEXT, applicationId TEXT, skuId TEXT, licenseStatus TEXT, lastRequestTime INTEGER, kmsEpid TEXT, requestCount INTEGER, PRIMARY KEY(clientMachineId, applicationId))") + if os.path.isfile(dbName): # Update database - try: - with sqlite3.connect(dbName) as con: - cur = con.cursor() - # Create simple "metadata" table if not exists. - cur.execute("CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT);") - # Get the current schema version - cur.execute("SELECT value FROM metadata WHERE key='schema_version';") - row = cur.fetchone() - if row is None: - current_version = 0 - else: - current_version = int(row[0]) - loggersrv.debug(f'Current database schema version: {current_version}') - # Apply necessary migrations - if current_version < 1: - # v1: Add "lastRequestIP" column to "clients" table. - loggersrv.info("Upgrading database schema to version 1...") - cur.execute("ALTER TABLE clients ADD COLUMN lastRequestIP TEXT;") - cur.execute("INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '1');") - loggersrv.info("Database schema updated to version 1.") - except sqlite3.Error as e: - loggersrv.exception("Sqlite Error during database upgrade!") - raise + with sqlite3.connect(dbName) as con: + cur = con.cursor() + # Create simple "metadata" table if not exists. + cur.execute("CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT);") + # Get the current schema version + cur.execute("SELECT value FROM metadata WHERE key='schema_version';") + row = cur.fetchone() + if row is None: + current_version = 0 + else: + current_version = int(row[0]) + loggersrv.debug(f'Current database schema version: {current_version}') + # Apply necessary migrations + if current_version < 1: + # v1: Add "lastRequestIP" column to "clients" table. + loggersrv.info("Upgrading database schema to version 1...") + cur.execute("ALTER TABLE clients ADD COLUMN lastRequestIP TEXT;") + cur.execute("INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '1');") + loggersrv.info("Database schema updated to version 1.") + def sql_get_all(dbName): if available is False: @@ -100,51 +94,45 @@ def sql_update(dbName, infoDict): if col_name not in infoDict: raise ValueError(f"infoDict is missing required column: {col_name}") - try: - with sqlite3.connect(dbName) as con: - cur = con.cursor() - cur.execute(f"SELECT {', '.join(_column_name_to_index.keys())} FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId;", infoDict) - data = cur.fetchone() - if not data: - # Insert new row with all given info - infoDict["kmsEpid"] = "" # Default empty value - infoDict["requestCount"] = 1 - cur.execute(f"""INSERT INTO clients ({', '.join(_column_name_to_index.keys())}) - VALUES ({', '.join(':' + col for col in _column_name_to_index.keys())});""", infoDict) + with sqlite3.connect(dbName) as con: + cur = con.cursor() + cur.execute(f"SELECT {', '.join(_column_name_to_index.keys())} FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId;", infoDict) + data = cur.fetchone() + if not data: + # Insert new row with all given info + infoDict["kmsEpid"] = "" # Default empty value + infoDict["requestCount"] = 1 + cur.execute(f"""INSERT INTO clients ({', '.join(_column_name_to_index.keys())}) + VALUES ({', '.join(':' + col for col in _column_name_to_index.keys())});""", infoDict) - else: - # Update only changed columns - common_postfix = "WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId" - def update_column_if_changed(column_name, new_value): - assert column_name in _column_name_to_index, f"Unknown column name: {column_name}" - assert "clientMachineId" in infoDict and "applicationId" in infoDict, "infoDict must contain 'clientMachineId' and 'applicationId'" - if data[_column_name_to_index[column_name]] != new_value: - query = f"UPDATE clients SET {column_name}=:value {common_postfix}" - cur.execute(query, {"value": new_value, "clientMachineId": infoDict['clientMachineId'], "applicationId": infoDict['applicationId']}) + else: + # Update only changed columns + common_postfix = "WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId" + def update_column_if_changed(column_name, new_value): + assert column_name in _column_name_to_index, f"Unknown column name: {column_name}" + assert "clientMachineId" in infoDict and "applicationId" in infoDict, "infoDict must contain 'clientMachineId' and 'applicationId'" + if data[_column_name_to_index[column_name]] != new_value: + query = f"UPDATE clients SET {column_name}=:value {common_postfix}" + cur.execute(query, {"value": new_value, "clientMachineId": infoDict['clientMachineId'], "applicationId": infoDict['applicationId']}) - # Dynamically check and maybe update all columns - for column_name in _column_name_to_index.keys(): - if column_name in ["clientMachineId", "applicationId", "requestCount"]: - continue # Skip these columns - if column_name == "kmsEpid": - # this one can only be updated by the special function - continue - update_column_if_changed(column_name, infoDict[column_name]) + # Dynamically check and maybe update all columns + for column_name in _column_name_to_index.keys(): + if column_name in ["clientMachineId", "applicationId", "requestCount"]: + continue # Skip these columns + if column_name == "kmsEpid": + # this one can only be updated by the special function + continue + update_column_if_changed(column_name, infoDict[column_name]) - # Finally increment requestCount - cur.execute(f"UPDATE clients SET requestCount=requestCount+1 {common_postfix}", infoDict) - except sqlite3.Error: - loggersrv.exception("Sqlite Error during sql_update!") + # Finally increment requestCount + cur.execute(f"UPDATE clients SET requestCount=requestCount+1 {common_postfix}", infoDict) def sql_update_epid(dbName, kmsRequest, response, appName): if available is False: return cmid = str(kmsRequest['clientMachineId'].get()) - try: - with sqlite3.connect(dbName) as con: - cur = con.cursor() - cur.execute("UPDATE clients SET kmsEpid=? WHERE clientMachineId=? AND applicationId=?;", - (str(response["kmsEpid"].decode('utf-16le')), cmid, appName)) - except sqlite3.Error: - loggersrv.exception("Sqlite Error during sql_update_epid!") + with sqlite3.connect(dbName) as con: + cur = con.cursor() + cur.execute("UPDATE clients SET kmsEpid=? WHERE clientMachineId=? AND applicationId=?;", + (str(response["kmsEpid"].decode('utf-16le')), cmid, appName)) From 6c57a8e5b43530542ea5ea4553e1277791f09795 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 21:18:52 +0100 Subject: [PATCH 17/18] Prevent unknown column names from being passed to DB Signed-off-by: simonmicro --- py-kms/pykms_Sql.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index d163462..0f6f73c 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -109,8 +109,9 @@ def sql_update(dbName, infoDict): # Update only changed columns common_postfix = "WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId" def update_column_if_changed(column_name, new_value): - assert column_name in _column_name_to_index, f"Unknown column name: {column_name}" assert "clientMachineId" in infoDict and "applicationId" in infoDict, "infoDict must contain 'clientMachineId' and 'applicationId'" + if column_name not in _column_name_to_index: + raise ValueError(f"Unknown column name: {column_name}") if data[_column_name_to_index[column_name]] != new_value: query = f"UPDATE clients SET {column_name}=:value {common_postfix}" cur.execute(query, {"value": new_value, "clientMachineId": infoDict['clientMachineId'], "applicationId": infoDict['applicationId']}) From 1c28865f3de5fee456b84cd06814952ae828e05d Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 21:25:05 +0100 Subject: [PATCH 18/18] Migrated to named columns to prevent any accidential re-order Signed-off-by: simonmicro --- py-kms/pykms_Sql.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/py-kms/pykms_Sql.py b/py-kms/pykms_Sql.py index 0f6f73c..07d6548 100644 --- a/py-kms/pykms_Sql.py +++ b/py-kms/pykms_Sql.py @@ -7,17 +7,7 @@ import logging #-------------------------------------------------------------------------------------------------------------------------------------------------------- loggersrv = logging.getLogger('logsrv') -_column_name_to_index = { - 'clientMachineId': 0, - 'machineName': 1, - 'applicationId': 2, - 'skuId': 3, - 'licenseStatus': 4, - 'lastRequestTime': 5, - 'kmsEpid': 6, - 'requestCount': 7, - 'lastRequestIP': 8, -} +_column_names = ('clientMachineId', 'machineName', 'applicationId', 'skuId', 'licenseStatus', 'lastRequestTime', 'kmsEpid', 'requestCount', 'lastRequestIP') # sqlite3 is optional. available = False @@ -68,17 +58,18 @@ def sql_get_all(dbName): if not os.path.isfile(dbName): return None with sqlite3.connect(dbName) as con: + con.row_factory = sqlite3.Row cur = con.cursor() - cur.execute(f"SELECT {', '.join(_column_name_to_index.keys())} FROM clients") + cur.execute(f"SELECT {', '.join(_column_names)} FROM clients") clients = [] for row in cur.fetchall(): loggersrv.debug(f"Row: {row}") obj = {} - for col_name, index in _column_name_to_index.items(): + for col_name in _column_names: if col_name == "lastRequestTime": - obj[col_name] = datetime.fromtimestamp(row[_column_name_to_index['lastRequestTime']]).isoformat() + obj[col_name] = datetime.fromtimestamp(row['lastRequestTime']).isoformat() else: - obj[col_name] = row[index] + obj[col_name] = row[col_name] loggersrv.debug(f"Obj: {obj}") clients.append(obj) return clients @@ -88,36 +79,37 @@ def sql_update(dbName, infoDict): return # make sure all column names are present - for col_name in _column_name_to_index.keys(): + for col_name in _column_names: if col_name in ["requestCount", "kmsEpid"]: continue if col_name not in infoDict: raise ValueError(f"infoDict is missing required column: {col_name}") with sqlite3.connect(dbName) as con: + con.row_factory = sqlite3.Row cur = con.cursor() - cur.execute(f"SELECT {', '.join(_column_name_to_index.keys())} FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId;", infoDict) + cur.execute(f"SELECT {', '.join(_column_names)} FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId;", infoDict) data = cur.fetchone() if not data: # Insert new row with all given info infoDict["kmsEpid"] = "" # Default empty value infoDict["requestCount"] = 1 - cur.execute(f"""INSERT INTO clients ({', '.join(_column_name_to_index.keys())}) - VALUES ({', '.join(':' + col for col in _column_name_to_index.keys())});""", infoDict) + cur.execute(f"""INSERT INTO clients ({', '.join(_column_names)}) + VALUES ({', '.join(':' + col for col in _column_names)});""", infoDict) else: # Update only changed columns common_postfix = "WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId" def update_column_if_changed(column_name, new_value): assert "clientMachineId" in infoDict and "applicationId" in infoDict, "infoDict must contain 'clientMachineId' and 'applicationId'" - if column_name not in _column_name_to_index: + if column_name not in _column_names: raise ValueError(f"Unknown column name: {column_name}") - if data[_column_name_to_index[column_name]] != new_value: + if data[column_name] != new_value: query = f"UPDATE clients SET {column_name}=:value {common_postfix}" cur.execute(query, {"value": new_value, "clientMachineId": infoDict['clientMachineId'], "applicationId": infoDict['applicationId']}) # Dynamically check and maybe update all columns - for column_name in _column_name_to_index.keys(): + for column_name in _column_names: if column_name in ["clientMachineId", "applicationId", "requestCount"]: continue # Skip these columns if column_name == "kmsEpid":