Skip to content

Commit 59aaa13

Browse files
authored
Merge pull request #141 from Py-KMS-Organization/feature/record_client_ips
Record client ips
2 parents 2d72cc8 + ba9d1f0 commit 59aaa13

9 files changed

Lines changed: 161 additions & 109 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: "Test: Basic Client"
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
7+
jobs:
8+
run-test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v5
13+
- name: Set up Python
14+
uses: actions/setup-python@v6
15+
with:
16+
python-version: "3.11"
17+
- name: Install dependencies
18+
run: |
19+
python -m pip install --upgrade pip
20+
pip install -r requirements.txt
21+
- name: Run tests
22+
run: |
23+
cd py-kms; timeout 30 python3 pykms_Server.py -F STDOUT -s ./pykms_database.db &
24+
sleep 5
25+
python3 pykms_Client.py -F STDOUT # fresh client
26+
python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b # (maybe) existing client
27+
python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b # now-for-sure existing client
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Test-Build Docker Image
1+
name: "Test: Build Docker Image"
22

33
on:
44
workflow_dispatch:

docker/docker-py3-kms-minimal/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ ENV LCID=1033
99
ENV CLIENT_COUNT=26
1010
ENV ACTIVATION_INTERVAL=120
1111
ENV RENEWAL_INTERVAL=10080
12-
ENV HWID RANDOM
12+
ENV HWID=RANDOM
1313
ENV LOGLEVEL=INFO
1414
ENV LOGFILE=STDOUT
1515
ENV LOGSIZE=""

docs/Contributing.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,14 @@ Awesome! But before you write or modify the existing source code, please note th
1313
```
1414
- Wrap lines only if really long (it does not matter 79 chars return)
1515
- For the rest a bit as it comes with a look at [PEP8](https://www.python.org/dev/peps/pep-0008/) :)
16+
17+
Test your changes, please. For example, run the server via:
18+
```bash
19+
python3 pykms_Server.py -F STDOUT -s ./pykms_database.db
20+
```
21+
Then trigger (multiple) client requests and check the output for errors via:
22+
```bash
23+
python3 pykms_Client.py -F STDOUT # fresh client
24+
python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b # (maybe) existing client
25+
python3 pykms_Client.py -F STDOUT -c 174f5409-0624-4ce3-b209-adde1091956b # now-for-sure existing client
26+
```

py-kms/pykms_Base.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,17 @@ def serverLogic(self, kmsRequest):
193193
infoDict = {
194194
"machineName" : kmsRequest.getMachineName(),
195195
"clientMachineId" : str(clientMachineId),
196-
"appId" : appName,
196+
"applicationId" : appName,
197197
"skuId" : skuName,
198198
"licenseStatus" : kmsRequest.getLicenseStatus(),
199-
"requestTime" : int(time.time()),
199+
"lastRequestIP" : self.srv_config['raddr'][0], # (ip, port)
200+
"lastRequestTime" : int(time.time()),
200201
"kmsEpid" : None
201202
}
202203

203204
loggersrv.info("Machine Name: %s" % infoDict["machineName"])
204205
loggersrv.info("Client Machine ID: %s" % infoDict["clientMachineId"])
205-
loggersrv.info("Application ID: %s" % infoDict["appId"])
206+
loggersrv.info("Application ID: %s" % infoDict["applicationId"])
206207
loggersrv.info("SKU ID: %s" % infoDict["skuId"])
207208
loggersrv.info("License Status: %s" % infoDict["licenseStatus"])
208209
loggersrv.info("Request Time: %s" % local_dt.strftime('%Y-%m-%d %H:%M:%S %Z (UTC%z)'))
@@ -211,7 +212,7 @@ def serverLogic(self, kmsRequest):
211212
loggersrv.mininfo("", extra = {'host': str(self.srv_config['raddr']),
212213
'status' : infoDict["licenseStatus"],
213214
'product' : infoDict["skuId"]})
214-
# Create database.
215+
# Send change to database.
215216
if self.srv_config['sqlite']:
216217
sql_update(self.srv_config['sqlite'], infoDict)
217218

py-kms/pykms_Client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def client_update():
170170
for appitem in appitems:
171171
kmsitems = appitem['KmsItems']
172172
for kmsitem in kmsitems:
173-
name = re.sub('\(.*\)', '', kmsitem['DisplayName']) # Remove bracets
173+
name = re.sub(r'\(.*\)', '', kmsitem['DisplayName']) # Remove brackets
174174
name = name.replace('2015', '') # Remove specific years
175175
name = name.replace(' ', '') # Ignore whitespaces
176176
name = name.replace('/11', '', 1) # Cut out Windows 11, as it is basically Windows 10
@@ -328,7 +328,7 @@ def createKmsRequestBase():
328328
requestDict['clientMachineId'] = UUID(uuid.UUID(clt_config['cmid']).bytes_le if (clt_config['cmid'] is not None) else uuid.uuid4().bytes_le)
329329
requestDict['previousClientMachineId'] = '\0' * 16 # I'm pretty sure this is supposed to be a null UUID.
330330
requestDict['requiredClientCount'] = clt_config['RequiredClientCount']
331-
requestDict['requestTime'] = dt_to_filetime(datetime.datetime.utcnow())
331+
requestDict['requestTime'] = dt_to_filetime(datetime.datetime.now(datetime.timezone.utc))
332332
requestDict['machineName'] = (clt_config['machine'] if (clt_config['machine'] is not None) else
333333
''.join(random.choice(string.ascii_letters + string.digits) for i in range(random.randint(2,63)))).encode('utf-16le')
334334
requestDict['mnPad'] = '\0'.encode('utf-16le') * (63 - len(requestDict['machineName'].decode('utf-16le')))

py-kms/pykms_Server.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@
1212
import socketserver
1313
import queue as Queue
1414
import selectors
15+
import traceback
1516
from time import monotonic as time
1617

17-
import pykms_RpcBind, pykms_RpcRequest
18+
import pykms_RpcBind, pykms_RpcRequest, pykms_Sql
1819
from pykms_RpcBase import rpcBase
1920
from pykms_Dcerpc import MSRPCHeader
2021
from pykms_Misc import check_setup, check_lcid, check_other
2122
from pykms_Misc import KmsParser, KmsParserException, KmsParserHelp
2223
from pykms_Misc import kms_parser_get, kms_parser_check_optionals, kms_parser_check_positionals, kms_parser_check_connect
2324
from pykms_Format import enco, deco, pretty_printer, justify
2425
from pykms_Connect import MultipleListener
25-
from pykms_Sql import sql_initialize
2626

2727
srv_version = "py-kms_2020-10-01"
2828
__license__ = "The Unlicense"
@@ -124,7 +124,8 @@ def handle_timeout(self):
124124
put_text = "{reverse}{red}{bold}Server connection timed out. Exiting...{end}")
125125

126126
def handle_error(self, request, client_address):
127-
pass
127+
pretty_printer(log_obj = loggersrv.error,
128+
put_text = "{reverse}{red}{bold}Exception happened during processing of request from %s:\n%s{end}" % (str(client_address), traceback.format_exc()))
128129

129130

130131
class server_thread(threading.Thread):
@@ -379,12 +380,9 @@ def server_check():
379380
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}")
380381
srv_config['sqlite'] = os.path.join(srv_config['sqlite'], 'pykms_database.db')
381382

382-
try:
383-
import sqlite3
384-
sql_initialize(srv_config['sqlite'])
385-
except ImportError:
386-
pretty_printer(log_obj = loggersrv.warning,
387-
put_text = "{reverse}{yellow}{bold}Module 'sqlite3' not installed, database support disabled.{end}")
383+
if pykms_Sql.available:
384+
pykms_Sql.sql_initialize(srv_config['sqlite'])
385+
else:
388386
srv_config['sqlite'] = False
389387

390388
# Check other specific server options.

py-kms/pykms_Sql.py

Lines changed: 99 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,131 @@
11
#!/usr/bin/env python3
22

3-
import datetime
3+
from datetime import datetime
44
import os
55
import logging
66

7+
#--------------------------------------------------------------------------------------------------------------------------------------------------------
8+
9+
loggersrv = logging.getLogger('logsrv')
10+
_column_names = ('clientMachineId', 'machineName', 'applicationId', 'skuId', 'licenseStatus', 'lastRequestTime', 'kmsEpid', 'requestCount', 'lastRequestIP')
11+
712
# sqlite3 is optional.
13+
available = False
814
try:
915
import sqlite3
16+
available = True
1017
except ImportError:
1118
pass
1219

13-
from pykms_Format import pretty_printer
14-
15-
#--------------------------------------------------------------------------------------------------------------------------------------------------------
16-
17-
loggersrv = logging.getLogger('logsrv')
18-
1920
def sql_initialize(dbName):
21+
if available is False:
22+
loggersrv.info("'sqlite3' module not found! SQLite database support cannot be enabled.")
23+
return
24+
loggersrv.debug(f'SQLite database support enabled. Database file: "{dbName}"')
2025
if not os.path.isfile(dbName):
21-
# Initialize the database.
26+
# Initialize the database
2227
loggersrv.debug(f'Initializing database file "{dbName}"...')
23-
con = None
24-
try:
25-
con = sqlite3.connect(dbName)
28+
with sqlite3.connect(dbName) as con:
2629
cur = con.cursor()
27-
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))")
30+
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))")
31+
32+
if os.path.isfile(dbName):
33+
# Update database
34+
with sqlite3.connect(dbName) as con:
35+
cur = con.cursor()
36+
# Create simple "metadata" table if not exists.
37+
cur.execute("CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT);")
38+
# Get the current schema version
39+
cur.execute("SELECT value FROM metadata WHERE key='schema_version';")
40+
row = cur.fetchone()
41+
if row is None:
42+
current_version = 0
43+
else:
44+
current_version = int(row[0])
45+
loggersrv.debug(f'Current database schema version: {current_version}')
46+
# Apply necessary migrations
47+
if current_version < 1:
48+
# v1: Add "lastRequestIP" column to "clients" table.
49+
loggersrv.info("Upgrading database schema to version 1...")
50+
cur.execute("ALTER TABLE clients ADD COLUMN lastRequestIP TEXT;")
51+
cur.execute("INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', '1');")
52+
loggersrv.info("Database schema updated to version 1.")
2853

29-
except sqlite3.Error as e:
30-
pretty_printer(log_obj = loggersrv.error, to_exit = True, put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e))
31-
finally:
32-
if con:
33-
con.commit()
34-
con.close()
3554

3655
def sql_get_all(dbName):
56+
if available is False:
57+
return
3758
if not os.path.isfile(dbName):
3859
return None
3960
with sqlite3.connect(dbName) as con:
61+
con.row_factory = sqlite3.Row
4062
cur = con.cursor()
41-
cur.execute("SELECT * FROM clients")
63+
cur.execute(f"SELECT {', '.join(_column_names)} FROM clients")
4264
clients = []
4365
for row in cur.fetchall():
44-
clients.append({
45-
'clientMachineId': row[0],
46-
'machineName': row[1],
47-
'applicationId': row[2],
48-
'skuId': row[3],
49-
'licenseStatus': row[4],
50-
'lastRequestTime': datetime.datetime.fromtimestamp(row[5]).isoformat(),
51-
'kmsEpid': row[6],
52-
'requestCount': row[7]
53-
})
66+
loggersrv.debug(f"Row: {row}")
67+
obj = {}
68+
for col_name in _column_names:
69+
if col_name == "lastRequestTime":
70+
obj[col_name] = datetime.fromtimestamp(row['lastRequestTime']).isoformat()
71+
else:
72+
obj[col_name] = row[col_name]
73+
loggersrv.debug(f"Obj: {obj}")
74+
clients.append(obj)
5475
return clients
5576

5677
def sql_update(dbName, infoDict):
57-
con = None
58-
try:
59-
con = sqlite3.connect(dbName)
78+
if available is False:
79+
return
80+
81+
# make sure all column names are present
82+
for col_name in _column_names:
83+
if col_name in ["requestCount", "kmsEpid"]:
84+
continue
85+
if col_name not in infoDict:
86+
raise ValueError(f"infoDict is missing required column: {col_name}")
87+
88+
with sqlite3.connect(dbName) as con:
89+
con.row_factory = sqlite3.Row
6090
cur = con.cursor()
61-
cur.execute("SELECT * FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict)
62-
try:
63-
data = cur.fetchone()
64-
if not data:
65-
# Insert row.
66-
cur.execute("INSERT INTO clients (clientMachineId, machineName, applicationId, \
67-
skuId, licenseStatus, lastRequestTime, requestCount) VALUES (:clientMachineId, :machineName, :appId, :skuId, :licenseStatus, :requestTime, 1);", infoDict)
68-
else:
69-
# Update data.
70-
if data[1] != infoDict["machineName"]:
71-
cur.execute("UPDATE clients SET machineName=:machineName WHERE \
72-
clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict)
73-
if data[2] != infoDict["appId"]:
74-
cur.execute("UPDATE clients SET applicationId=:appId WHERE \
75-
clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict)
76-
if data[3] != infoDict["skuId"]:
77-
cur.execute("UPDATE clients SET skuId=:skuId WHERE \
78-
clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict)
79-
if data[4] != infoDict["licenseStatus"]:
80-
cur.execute("UPDATE clients SET licenseStatus=:licenseStatus WHERE \
81-
clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict)
82-
if data[5] != infoDict["requestTime"]:
83-
cur.execute("UPDATE clients SET lastRequestTime=:requestTime WHERE \
84-
clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict)
85-
# Increment requestCount
86-
cur.execute("UPDATE clients SET requestCount=requestCount+1 WHERE \
87-
clientMachineId=:clientMachineId AND applicationId=:appId;", infoDict)
88-
89-
except sqlite3.Error as e:
90-
pretty_printer(log_obj = loggersrv.error, to_exit = True,
91-
put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e))
92-
except sqlite3.Error as e:
93-
pretty_printer(log_obj = loggersrv.error, to_exit = True,
94-
put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e))
95-
finally:
96-
if con:
97-
con.commit()
98-
con.close()
91+
cur.execute(f"SELECT {', '.join(_column_names)} FROM clients WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId;", infoDict)
92+
data = cur.fetchone()
93+
if not data:
94+
# Insert new row with all given info
95+
infoDict["kmsEpid"] = "" # Default empty value
96+
infoDict["requestCount"] = 1
97+
cur.execute(f"""INSERT INTO clients ({', '.join(_column_names)})
98+
VALUES ({', '.join(':' + col for col in _column_names)});""", infoDict)
99+
100+
else:
101+
# Update only changed columns
102+
common_postfix = "WHERE clientMachineId=:clientMachineId AND applicationId=:applicationId"
103+
def update_column_if_changed(column_name, new_value):
104+
assert "clientMachineId" in infoDict and "applicationId" in infoDict, "infoDict must contain 'clientMachineId' and 'applicationId'"
105+
if column_name not in _column_names:
106+
raise ValueError(f"Unknown column name: {column_name}")
107+
if data[column_name] != new_value:
108+
query = f"UPDATE clients SET {column_name}=:value {common_postfix}"
109+
cur.execute(query, {"value": new_value, "clientMachineId": infoDict['clientMachineId'], "applicationId": infoDict['applicationId']})
110+
111+
# Dynamically check and maybe update all columns
112+
for column_name in _column_names:
113+
if column_name in ["clientMachineId", "applicationId", "requestCount"]:
114+
continue # Skip these columns
115+
if column_name == "kmsEpid":
116+
# this one can only be updated by the special function
117+
continue
118+
update_column_if_changed(column_name, infoDict[column_name])
119+
120+
# Finally increment requestCount
121+
cur.execute(f"UPDATE clients SET requestCount=requestCount+1 {common_postfix}", infoDict)
99122

100123
def sql_update_epid(dbName, kmsRequest, response, appName):
124+
if available is False:
125+
return
126+
101127
cmid = str(kmsRequest['clientMachineId'].get())
102-
con = None
103-
try:
104-
con = sqlite3.connect(dbName)
128+
with sqlite3.connect(dbName) as con:
105129
cur = con.cursor()
106-
cur.execute("SELECT * FROM clients WHERE clientMachineId=? AND applicationId=?;", (cmid, appName))
107-
try:
108-
data = cur.fetchone()
109-
cur.execute("UPDATE clients SET kmsEpid=? WHERE \
110-
clientMachineId=? AND applicationId=?;", (str(response["kmsEpid"].decode('utf-16le')), cmid, appName))
111-
112-
except sqlite3.Error as e:
113-
pretty_printer(log_obj = loggersrv.error, to_exit = True,
114-
put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e))
115-
except sqlite3.Error as e:
116-
pretty_printer(log_obj = loggersrv.error, to_exit = True,
117-
put_text = "{reverse}{red}{bold}Sqlite Error: %s. Exiting...{end}" %str(e))
118-
finally:
119-
if con:
120-
con.commit()
121-
con.close()
130+
cur.execute("UPDATE clients SET kmsEpid=? WHERE clientMachineId=? AND applicationId=?;",
131+
(str(response["kmsEpid"].decode('utf-16le')), cmid, appName))

0 commit comments

Comments
 (0)