Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/test_basic_client.yml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
simonmicro marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Test-Build Docker Image
name: "Test: Build Docker Image"

on:
workflow_dispatch:
Expand Down
2 changes: 1 addition & 1 deletion docker/docker-py3-kms-minimal/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down
11 changes: 11 additions & 0 deletions docs/Contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@ 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 # 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
```
9 changes: 5 additions & 4 deletions py-kms/pykms_Base.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,17 @@ def serverLogic(self, kmsRequest):
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)'))
Expand All @@ -211,7 +212,7 @@ def serverLogic(self, kmsRequest):
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)

Expand Down
4 changes: 2 additions & 2 deletions py-kms/pykms_Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down Expand Up @@ -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')))
Expand Down
16 changes: 7 additions & 9 deletions py-kms/pykms_Server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@
import socketserver
import queue as Queue
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
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"
Expand Down Expand Up @@ -124,7 +124,8 @@ def handle_timeout(self):
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):
Expand Down Expand Up @@ -379,12 +380,9 @@ 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.
Expand Down
188 changes: 99 additions & 89 deletions py-kms/pykms_Sql.py
Original file line number Diff line number Diff line change
@@ -1,121 +1,131 @@
#!/usr/bin/env python3

import datetime
from datetime import datetime
import os
import logging

#--------------------------------------------------------------------------------------------------------------------------------------------------------

loggersrv = logging.getLogger('logsrv')
_column_names = ('clientMachineId', 'machineName', 'applicationId', 'skuId', 'licenseStatus', 'lastRequestTime', 'kmsEpid', 'requestCount', 'lastRequestIP')

# sqlite3 is optional.
available = False
try:
import sqlite3
available = True
except ImportError:
pass

from pykms_Format import pretty_printer

#--------------------------------------------------------------------------------------------------------------------------------------------------------

loggersrv = logging.getLogger('logsrv')

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.
# Initialize the database
loggersrv.debug(f'Initializing database file "{dbName}"...')
con = None
try:
con = sqlite3.connect(dbName)
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))")
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))")
Comment thread
simonmicro marked this conversation as resolved.

if os.path.isfile(dbName):
# Update database
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:
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 available is False:
return
if not os.path.isfile(dbName):
return None
with sqlite3.connect(dbName) as con:
con.row_factory = sqlite3.Row
cur = con.cursor()
cur.execute("SELECT * FROM clients")
cur.execute(f"SELECT {', '.join(_column_names)} 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 in _column_names:
if col_name == "lastRequestTime":
obj[col_name] = datetime.fromtimestamp(row['lastRequestTime']).isoformat()
else:
obj[col_name] = row[col_name]
loggersrv.debug(f"Obj: {obj}")
clients.append(obj)
return clients

def sql_update(dbName, infoDict):
con = None
try:
con = sqlite3.connect(dbName)
if available is False:
return

# make sure all column names are present
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("SELECT * FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict)
try:
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)

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()
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_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'"
Comment thread
simonmicro marked this conversation as resolved.
if column_name not in _column_names:
raise ValueError(f"Unknown column name: {column_name}")
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_names:
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)

Comment thread
simonmicro marked this conversation as resolved.
def sql_update_epid(dbName, kmsRequest, response, appName):
if available is False:
return

cmid = str(kmsRequest['clientMachineId'].get())
con = None
try:
con = sqlite3.connect(dbName)
with sqlite3.connect(dbName) as con:
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))

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()
cur.execute("UPDATE clients SET kmsEpid=? WHERE clientMachineId=? AND applicationId=?;",
(str(response["kmsEpid"].decode('utf-16le')), cmid, appName))
Loading
Loading