- {{ 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 af2527af09fd31134a898bb1056f5d336764d89d Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 20:41:06 +0100
Subject: [PATCH 18/34] 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 cadcb7a226eef7d7acfd5226f9b41e487a827d89 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 20:45:21 +0100
Subject: [PATCH 19/34] 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 c8d460f1b7b5a620b2ac2143b2c057741c0e323f Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 20:48:11 +0100
Subject: [PATCH 20/34] 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 95b4c2e83f140f6152ff3b574adcbb2366d12bf6 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 20:51:49 +0100
Subject: [PATCH 21/34] 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 3fad613..7e3833b 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 ca722bb12a86c79f02dfebc6549b626a7e8e82b7 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 20:53:15 +0100
Subject: [PATCH 22/34] 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 dcf90513540eadd0e7c6cf023112b0f5bcbb9174 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 20:53:41 +0100
Subject: [PATCH 23/34] 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 cd7cbd113f3862090e1e6a404a434f69621a1b39 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 20:57:19 +0100
Subject: [PATCH 24/34] 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 7e3833b..6ab49ca 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 bb9946b78880a44bd5260b571ea93753124a9bf0 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 20:57:46 +0100
Subject: [PATCH 25/34] 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 24b263057e675a96ff665733d99c2eaaebef40e5 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 21:01:24 +0100
Subject: [PATCH 26/34] 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 73119bd..0d8f072 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 f8e4253786e3f778143baa029b49fe2a8e1524fe Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 21:13:25 +0100
Subject: [PATCH 27/34] 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 9c83557e4da972338df5a8a1b9ccc1040d2390ce Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 21:18:52 +0100
Subject: [PATCH 28/34] 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 ba9d1f0ca6ec1db8a1ba137c29737b5455800181 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 6 Dec 2025 21:25:05 +0100
Subject: [PATCH 29/34] 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":
From 2d374000578e7aede22dbb2c069f5a7154632059 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 11 Apr 2026 16:08:52 +0200
Subject: [PATCH 30/34] Do not drop permissions, if not needed
Signed-off-by: simonmicro
---
docker/entrypoint.py | 24 ++++++++++++++----------
1 file changed, 14 insertions(+), 10 deletions(-)
diff --git a/docker/entrypoint.py b/docker/entrypoint.py
index b564966..e920110 100755
--- a/docker/entrypoint.py
+++ b/docker/entrypoint.py
@@ -15,15 +15,20 @@ PYTHON3 = '/usr/bin/python3'
dbPath = os.path.join(os.sep, 'home', 'py-kms', 'db') # Do not include the database file name, as we must correct the folder permissions (the db file is recursively reachable)
def change_uid_grp(logger):
- if os.geteuid() != 0:
- logger.info(f'not root user, cannot change uid/gid.')
- return None
user_db_entries = pwd.getpwnam("py-kms")
user_grp_db_entries = grp.getgrnam("users")
- uid = int(user_db_entries.pw_uid)
- gid = int(user_grp_db_entries.gr_gid)
- new_gid = int(os.getenv('GID', str(gid)))
- new_uid = int(os.getenv('UID', str(uid)))
+ now_uid = os.geteuid() # as what are we running effectively right now?
+ now_gid = os.getegid()
+ ebd_uid = int(user_db_entries.pw_uid) # what was compiled (embedded) into the image?
+ ebd_gid = int(user_grp_db_entries.gr_gid)
+ new_gid = int(os.getenv('GID', str(ebd_gid))) # what is desired by the user at runtime?
+ new_uid = int(os.getenv('UID', str(ebd_uid)))
+ if now_uid == new_uid and now_gid == new_gid:
+ logger.info(f'UID/GID already set to {new_uid}:{new_gid}')
+ return None
+ if now_uid != 0:
+ logger.warning(f'Not root user (UID is {now_uid}), cannot change UID/GID to {new_uid}:{new_gid}!')
+ return None
os.chown("/home/py-kms", new_uid, new_gid)
os.chmod("/home/py-kms", 0o700)
if os.path.isdir(dbPath):
@@ -50,9 +55,8 @@ def change_uid_grp(logger):
os.chmod(os.environ['LOGFILE'], 0o777)
logger.error(str(subprocess.check_output(['ls', '-la', os.environ['LOGFILE']])))
# Drop actual permissions
- logger.info(f"Setting gid to {new_gid}")
+ logger.info(f"Setting UID/GID to {new_uid}:{new_gid}")
os.setgid(new_gid)
- logger.info(f"Setting uid to {new_uid}")
os.setuid(new_uid)
def change_tz(logger):
@@ -75,7 +79,7 @@ if __name__ == "__main__":
streamhandler.setFormatter(formatter)
loggersrv.addHandler(streamhandler)
loggersrv.info("Log level: %s" % log_level)
- loggersrv.debug("user id: %s" % os.getuid())
+ loggersrv.debug("Running as UID/GID %s:%s" % (os.geteuid(), os.getegid()))
change_tz(loggersrv)
childProcess = subprocess.Popen(PYTHON3 + " -u /usr/bin/start.py", preexec_fn=change_uid_grp(loggersrv), shell=True)
From e99762fe7a45fcb5a936a0df3ffa02d43cd83472 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 11 Apr 2026 16:23:06 +0200
Subject: [PATCH 31/34] More logging for issues with permissions
Signed-off-by: simonmicro
---
docker/start.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/docker/start.py b/docker/start.py
index 5b192fe..4feceab 100755
--- a/docker/start.py
+++ b/docker/start.py
@@ -72,9 +72,12 @@ def start_kms(logger):
pass
except KeyboardInterrupt:
pass
+ logger.info("Shutting down...")
if pykms_webui_process:
+ logger.debug("Terminating webui process...")
pykms_webui_process.terminate()
+ logger.debug("Terminating KMS process...")
pykms_process.terminate()
@@ -90,6 +93,7 @@ if __name__ == "__main__":
formatter = logging.Formatter(fmt='\x1b[94m%(asctime)s %(levelname)-8s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S')
streamhandler.setFormatter(formatter)
loggersrv.addHandler(streamhandler)
- loggersrv.debug("user id: %s" % os.getuid())
+ loggersrv.info("Log level: %s" % log_level)
+ loggersrv.debug("Running as UID/GID %s:%s" % (os.geteuid(), os.getegid()))
start_kms(loggersrv)
From 7490ba92a472c126ce5af3ba02f6f3f64656b530 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 11 Apr 2026 16:26:55 +0200
Subject: [PATCH 32/34] Fixed hardening to allow already dropped users to
access the app-dir, fixes #139
Signed-off-by: simonmicro
---
docker/docker-py3-kms-minimal/Dockerfile | 9 ++++-----
docker/docker-py3-kms/Dockerfile | 9 ++++-----
2 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/docker/docker-py3-kms-minimal/Dockerfile b/docker/docker-py3-kms-minimal/Dockerfile
index 4fb1f36..64204e5 100644
--- a/docker/docker-py3-kms-minimal/Dockerfile
+++ b/docker/docker-py3-kms-minimal/Dockerfile
@@ -36,11 +36,10 @@ COPY docker/start.py /usr/bin/start.py
RUN chmod 555 /usr/bin/entrypoint.py /usr/bin/healthcheck.py /usr/bin/start.py
# Additional permission hardening: All files read-only for the executing user
-RUN chown root: -R /home/py-kms && \
- chmod 444 -R /home/py-kms && \
- chown py-kms: /home/py-kms && \
- chmod 700 /home/py-kms && \
- find /home/py-kms -type d -print -exec chmod +x {} ';'
+RUN find /home/py-kms -type f -print -exec chmod 444 {} ';' && \
+ find /home/py-kms -type d -print -exec chmod 555 {} ';' && \
+ chown root: -R /home/py-kms && \
+ chown py-kms: /home/py-kms
WORKDIR /home/py-kms
diff --git a/docker/docker-py3-kms/Dockerfile b/docker/docker-py3-kms/Dockerfile
index 3c9846d..4b5e387 100644
--- a/docker/docker-py3-kms/Dockerfile
+++ b/docker/docker-py3-kms/Dockerfile
@@ -42,11 +42,10 @@ COPY docker/start.py /usr/bin/start.py
RUN chmod 555 /usr/bin/entrypoint.py /usr/bin/healthcheck.py /usr/bin/start.py
# Additional permission hardening: All files read-only for the executing user
-RUN chown root: -R /home/py-kms && \
- chmod 444 -R /home/py-kms && \
- chown py-kms: /home/py-kms && \
- chmod 700 /home/py-kms && \
- find /home/py-kms -type d -print -exec chmod +x {} ';'
+RUN find /home/py-kms -type f -print -exec chmod 444 {} ';' && \
+ find /home/py-kms -type d -print -exec chmod 555 {} ';' && \
+ chown root: -R /home/py-kms && \
+ chown py-kms: /home/py-kms
# Web-interface specifics
COPY LICENSE /LICENSE
From 6790958d82c4ef6e085effee9017f2b01d9c5fc7 Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 11 Apr 2026 16:45:27 +0200
Subject: [PATCH 33/34] Provide ephemeral db-path by default for web-ui
container
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
docker/docker-py3-kms/Dockerfile | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/docker/docker-py3-kms/Dockerfile b/docker/docker-py3-kms/Dockerfile
index 4b5e387..836fe74 100644
--- a/docker/docker-py3-kms/Dockerfile
+++ b/docker/docker-py3-kms/Dockerfile
@@ -29,7 +29,7 @@ RUN apk add --no-cache --update \
tzdata \
shadow \
&& pip3 install --break-system-packages --no-cache-dir -r /home/py-kms/requirements.txt \
- && mkdir /db/ \
+ && mkdir /db/ /home/py-kms/db \
&& adduser -S py-kms -G users -s /bin/bash \
&& chown py-kms:users /home/py-kms \
# Fix undefined timezone, in case the user did not mount the /etc/localtime
@@ -41,11 +41,13 @@ COPY docker/healthcheck.py /usr/bin/healthcheck.py
COPY docker/start.py /usr/bin/start.py
RUN chmod 555 /usr/bin/entrypoint.py /usr/bin/healthcheck.py /usr/bin/start.py
-# Additional permission hardening: All files read-only for the executing user
+# Additional permission hardening: keep application files read-only, but preserve
+# a dedicated writable database directory for WebUI/SQLite at runtime.
RUN find /home/py-kms -type f -print -exec chmod 444 {} ';' && \
find /home/py-kms -type d -print -exec chmod 555 {} ';' && \
chown root: -R /home/py-kms && \
- chown py-kms: /home/py-kms
+ chown py-kms: /home/py-kms && \
+ chmod 1777 /home/py-kms/db
# Web-interface specifics
COPY LICENSE /LICENSE
From 937bb68f1effee7dc18d0588672663402f3bdd5a Mon Sep 17 00:00:00 2001
From: simonmicro
Date: Sat, 11 Apr 2026 17:08:18 +0200
Subject: [PATCH 34/34] Replace tabs with spaces
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
README.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 799f7df..34e0cbd 100644
--- a/README.md
+++ b/README.md
@@ -27,10 +27,10 @@ The wiki has been completely reworked and is now available on [readthedocs.io](h
## Quick start
- To start the server, execute `python3 pykms_Server.py [IPADDRESS] [PORT]`, the default _IPADDRESS_ is `::` ( all interfaces ) and the default _PORT_ is `1688`.
- - Note that both the address and port are optional.
- - It's allowed to use IPv4 and IPv6 addresses.
- - If you have an IPv6-capable dual-stack OS, a dual-stack socket is created when using a IPv6 address.
- - **[In case your OS does not support IPv6](https://github.com/Py-KMS-Organization/py-kms/issues/108), make sure to explicitly specify the legacy IPv4 of `0.0.0.0`!**
+ - Note that both the address and port are optional.
+ - It's allowed to use IPv4 and IPv6 addresses.
+ - If you have an IPv6-capable dual-stack OS, a dual-stack socket is created when using a IPv6 address.
+ - **[In case your OS does not support IPv6](https://github.com/Py-KMS-Organization/py-kms/issues/108), make sure to explicitly specify the legacy IPv4 of `0.0.0.0`!**
- To start the server automatically using Docker, execute `docker run -d --name py-kms --restart always -p 1688:1688 ghcr.io/py-kms-organization/py-kms`.
- To show the help pages type: `python3 pykms_Server.py -h` and `python3 pykms_Client.py -h`.
|