From 45d7f825df41dd1ba72d32a193bee4fb8181d32d Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:04:43 +0100 Subject: [PATCH 01/34] Reformatting and a bit of cleanup Signed-off-by: simonmicro --- README.md | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 8eb8138..fb4db98 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![read-the-docs](https://img.shields.io/readthedocs/py-kms) *** -_Keep in mind that this project is not intended for production use. Feel free to use it to test your own systems or maybe even learn something from the protocol structure. :)_ +_Keep in mind that this project is not intended for production use. Feel free to use it to test your own systems or maybe even learn something from the protocol structure._ 😉 ## History _py-kms_ is a port of node-kms created by [cyrozap](http://forums.mydigitallife.info/members/183074-markedsword), which is a port of either the C#, C++, or .NET implementations of KMS Emulator. The original version was written by [CODYQX4](http://forums.mydigitallife.info/members/89933-CODYQX4) and is derived from the reverse-engineered code of Microsoft's official KMS. @@ -14,35 +14,20 @@ This version of _py-kms_ is for itself a fork of the original implementation by ## Features - Responds to `v4`, `v5`, and `v6` KMS requests. -- Supports activating: - - Windows Vista - - Windows 7 - - Windows 8 - - Windows 8.1 - - Windows 10 ( 1511 / 1607 / 1703 / 1709 / 1803 / 1809 ) - - Windows 10 ( 1903 / 1909 / 20H1, 20H2, 21H1, 21H2 ) - - Windows 11 ( 21H2 ) - - Windows Server 2008 - - Windows Server 2008 R2 - - Windows Server 2012 - - Windows Server 2012 R2 - - Windows Server 2016 - - Windows Server 2019 - - Windows Server 2022 - - Microsoft Office 2010 ( Volume License ) - - Microsoft Office 2013 ( Volume License ) - - Microsoft Office 2016 ( Volume License ) - - Microsoft Office 2019 ( Volume License ) - - Microsoft Office 2021 ( Volume License ) - - It's written in Python (tested with Python 3.10.1). +- Supports activating [a lot of products](docs/Keys.md), so checkout the docs for more information. + - It's written in Python. - Supports execution by `Docker`, `systemd` and many more... - Uses `sqlite` for persistent data storage (with a simple web-based explorer). ## Documentation -The wiki has been completly reworked and is now available on [readthedocs.io](https://py-kms.readthedocs.io/en/latest/). It should provide you all the necessary information about how to setup and to use _py-kms_ , all without clumping this readme. The documentation also houses more details about activation with _py-kms_ and how to get GVLK keys. +The wiki has been completly reworked and is now available on [readthedocs.io](https://py-kms.readthedocs.io/en/latest/). It should provide you all the necessary information about how to setup and to use _py-kms_, all without clumping this readme. The documentation also houses more details about the activation procedure with _py-kms_ and how to get GVLK keys. ## 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 a IPv6-capable dual-stack OS, a dual-stack socket is created when using a IPv6 address. **In case your OS does not support IPv6, make sure to explicitly specify the legacy IPv4 of `0.0.0.0`!** +- 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`!** - 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`. From df0b7d3f6c4db4d7690b1a41b68edc04d53d4108 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:10:32 +0100 Subject: [PATCH 02/34] Moved changelog and added semantic-versioning note into README Signed-off-by: simonmicro --- README.md | 7 +++++-- CHANGELOG.md => docs/Historic Releases.md | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) rename CHANGELOG.md => docs/Historic Releases.md (98%) diff --git a/README.md b/README.md index fb4db98..bbc73ec 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Readme +# py-kms + ![repo-size](https://img.shields.io/github/repo-size/Py-KMS-Organization/py-kms) ![open-issues](https://img.shields.io/github/issues/Py-KMS-Organization/py-kms) ![last-commit](https://img.shields.io/github/last-commit/Py-KMS-Organization/py-kms/master) ![docker-pulls](https://img.shields.io/docker/pulls/pykmsorg/py-kms) ![read-the-docs](https://img.shields.io/readthedocs/py-kms) -*** _Keep in mind that this project is not intended for production use. Feel free to use it to test your own systems or maybe even learn something from the protocol structure._ 😉 @@ -12,6 +12,9 @@ _Keep in mind that this project is not intended for production use. Feel free to _py-kms_ is a port of node-kms created by [cyrozap](http://forums.mydigitallife.info/members/183074-markedsword), which is a port of either the C#, C++, or .NET implementations of KMS Emulator. The original version was written by [CODYQX4](http://forums.mydigitallife.info/members/89933-CODYQX4) and is derived from the reverse-engineered code of Microsoft's official KMS. This version of _py-kms_ is for itself a fork of the original implementation by [SystemRage](https://github.com/SystemRage/py-kms), which was abandoned early 2021. +### What is with version `1.0.0`? +Semantic versioning is now being used in this project, so checkout the [GitHub Releases](https://github.com/Py-KMS-Organization/py-kms/releases). Before, a `CHANGELOG.md` file was used to track changes, but got abandoned over time. Its content got moved into the [Historic Releases](docs/Historic%20Releases.md) document for reference. + ## Features - Responds to `v4`, `v5`, and `v6` KMS requests. - Supports activating [a lot of products](docs/Keys.md), so checkout the docs for more information. diff --git a/CHANGELOG.md b/docs/Historic Releases.md similarity index 98% rename from CHANGELOG.md rename to docs/Historic Releases.md index 47c1fac..d47cd95 100644 --- a/CHANGELOG.md +++ b/docs/Historic Releases.md @@ -1,5 +1,7 @@ # Changelog +**THIS IS A HISTORIC RELEASES FILE.** Nowadays we moved to proper semantic versioning and use Git-based tagging to track releases instead. + ## py-kms_2022-12-16 - Added support for new web-gui into Docker - Implemented whole-new web-based GUI with Flask From 99be2c9ffcc79e0c89f0584db1ec6b36685d6ee9 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:14:25 +0100 Subject: [PATCH 03/34] Prepare to switch to main as stable branch Signed-off-by: simonmicro --- .github/workflows/bake_to_latest.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bake_to_latest.yml b/.github/workflows/bake_to_latest.yml index 73a8c25..ac61d6f 100644 --- a/.github/workflows/bake_to_latest.yml +++ b/.github/workflows/bake_to_latest.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - master + - main jobs: bake-latest: diff --git a/README.md b/README.md index bbc73ec..3c431e3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![repo-size](https://img.shields.io/github/repo-size/Py-KMS-Organization/py-kms) ![open-issues](https://img.shields.io/github/issues/Py-KMS-Organization/py-kms) -![last-commit](https://img.shields.io/github/last-commit/Py-KMS-Organization/py-kms/master) +![last-commit](https://img.shields.io/github/last-commit/Py-KMS-Organization/py-kms/main) ![docker-pulls](https://img.shields.io/docker/pulls/pykmsorg/py-kms) ![read-the-docs](https://img.shields.io/readthedocs/py-kms) From 565ef8d500ce9d42d77461febe04f641bef8c170 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:21:23 +0100 Subject: [PATCH 04/34] Updated a few workflow actions Signed-off-by: simonmicro --- .github/workflows/bake_to_latest.yml | 10 +++++----- .github/workflows/bake_to_next.yml | 10 +++++----- .github/workflows/bake_to_test.yml | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/bake_to_latest.yml b/.github/workflows/bake_to_latest.yml index ac61d6f..8154f67 100644 --- a/.github/workflows/bake_to_latest.yml +++ b/.github/workflows/bake_to_latest.yml @@ -14,20 +14,20 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v5 - - name: Set up QEMU + uses: actions/checkout@v6 + - name: Docker Setup QEMU uses: docker/setup-qemu-action@v3 with: platforms: all - - name: Set up Docker Buildx + - name: Docker Setup Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/bake_to_next.yml b/.github/workflows/bake_to_next.yml index 99032e0..376b04a 100644 --- a/.github/workflows/bake_to_next.yml +++ b/.github/workflows/bake_to_next.yml @@ -14,20 +14,20 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v5 - - name: Set up QEMU + uses: actions/checkout@v6 + - name: Docker Setup QEMU uses: docker/setup-qemu-action@v3 with: platforms: all - - name: Set up Docker Buildx + - name: Docker Setup Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.10.0 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/bake_to_test.yml b/.github/workflows/bake_to_test.yml index 72fc196..d409aef 100644 --- a/.github/workflows/bake_to_test.yml +++ b/.github/workflows/bake_to_test.yml @@ -9,12 +9,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 - - name: Set up QEMU + uses: actions/checkout@v6 + - name: Docker Setup QEMU uses: docker/setup-qemu-action@v3 with: platforms: all - - name: Set up Docker Buildx + - name: Docker Setup Buildx uses: docker/setup-buildx-action@v3 - name: Build (full) uses: docker/build-push-action@v6 From 62508dfc495b937e960f0f023d9060921405fe8b Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:38:47 +0100 Subject: [PATCH 05/34] Update workflows to also build tagged releases Signed-off-by: simonmicro --- .github/workflows/bake_to_latest.yml | 24 ++++++++--- .github/workflows/bake_to_next.yml | 24 ++++++++--- .github/workflows/bake_to_test.yml | 4 +- .github/workflows/bake_to_version.yml | 62 +++++++++++++++++++++++++++ docker/docker-py3-kms/Dockerfile | 4 +- 5 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/bake_to_version.yml diff --git a/.github/workflows/bake_to_latest.yml b/.github/workflows/bake_to_latest.yml index 8154f67..080afb2 100644 --- a/.github/workflows/bake_to_latest.yml +++ b/.github/workflows/bake_to_latest.yml @@ -1,4 +1,4 @@ -name: Build release-tags +name: Build latest/main tags on: workflow_dispatch: @@ -39,10 +39,17 @@ jobs: file: ./docker/docker-py3-kms/Dockerfile platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6 push: true - tags: pykmsorg/py-kms:python3,ghcr.io/py-kms-organization/py-kms:python3 + # the tag "python3" is for backward compatibility only + tags: | + pykmsorg/py-kms:main + ghcr.io/py-kms-organization/py-kms:main + pykmsorg/py-kms:main-full + ghcr.io/py-kms-organization/py-kms:main-full + pykmsorg/py-kms:python3 + ghcr.io/py-kms-organization/py-kms:python3 build-args: | BUILD_COMMIT=${{ github.sha }} - BUILD_BRANCH=${{ github.ref_name }} + BUILD_REFERENCE=${{ github.ref_name }} - name: Build (minimal) uses: docker/build-push-action@v6 with: @@ -50,7 +57,14 @@ jobs: file: ./docker/docker-py3-kms-minimal/Dockerfile platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6 push: true - tags: pykmsorg/py-kms:latest,ghcr.io/py-kms-organization/py-kms:latest,pykmsorg/py-kms:minimal,ghcr.io/py-kms-organization/py-kms:minimal + # the tag "minimal" is for backward compatibility only + tags: | + pykmsorg/py-kms:latest + ghcr.io/py-kms-organization/py-kms:latest + pykmsorg/py-kms:main-minimal + ghcr.io/py-kms-organization/py-kms:main-minimal + pykmsorg/py-kms:minimal + ghcr.io/py-kms-organization/py-kms:minimal build-args: | BUILD_COMMIT=${{ github.sha }} - BUILD_BRANCH=${{ github.ref_name }} + BUILD_REFERENCE=${{ github.ref_name }} diff --git a/.github/workflows/bake_to_next.yml b/.github/workflows/bake_to_next.yml index 376b04a..b3128a3 100644 --- a/.github/workflows/bake_to_next.yml +++ b/.github/workflows/bake_to_next.yml @@ -1,4 +1,4 @@ -name: Build next-tags +name: Build next tags on: workflow_dispatch: @@ -39,10 +39,17 @@ jobs: file: ./docker/docker-py3-kms/Dockerfile platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6 push: true - tags: pykmsorg/py-kms:python3-next,ghcr.io/py-kms-organization/py-kms:python3-next + # the tag "python3-next" is for backward compatibility only + tags: | + pykmsorg/py-kms:next + ghcr.io/py-kms-organization/py-kms:next + pykmsorg/py-kms:next-full + ghcr.io/py-kms-organization/py-kms:next-full + pykmsorg/py-kms:python3-next + ghcr.io/py-kms-organization/py-kms:python3-next build-args: | BUILD_COMMIT=${{ github.sha }} - BUILD_BRANCH=${{ github.ref_name }} + BUILD_REFERENCE=${{ github.ref_name }} - name: Build (minimal) uses: docker/build-push-action@v6 with: @@ -50,7 +57,14 @@ jobs: file: ./docker/docker-py3-kms-minimal/Dockerfile platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6 push: true - tags: pykmsorg/py-kms:latest-next,ghcr.io/py-kms-organization/py-kms:latest-next,pykmsorg/py-kms:minimal-next,ghcr.io/py-kms-organization/py-kms:minimal-next + # the tag "latest-next" and "minimal-next" are for backward compatibility only + tags: | + pykmsorg/py-kms:next-minimal + ghcr.io/py-kms-organization/py-kms:next-minimal + pykmsorg/py-kms:latest-next + ghcr.io/py-kms-organization/py-kms:latest-next + pykmsorg/py-kms:minimal-next + ghcr.io/py-kms-organization/py-kms:minimal-next build-args: | BUILD_COMMIT=${{ github.sha }} - BUILD_BRANCH=${{ github.ref_name }} + BUILD_REFERENCE=${{ github.ref_name }} diff --git a/.github/workflows/bake_to_test.yml b/.github/workflows/bake_to_test.yml index d409aef..73119bd 100644 --- a/.github/workflows/bake_to_test.yml +++ b/.github/workflows/bake_to_test.yml @@ -25,7 +25,7 @@ jobs: push: false build-args: | BUILD_COMMIT=${{ github.sha }} - BUILD_BRANCH=${{ github.ref_name }} + BUILD_REFERENCE=${{ github.ref_name }} - name: Build (minimal) uses: docker/build-push-action@v6 with: @@ -35,4 +35,4 @@ jobs: push: false build-args: | BUILD_COMMIT=${{ github.sha }} - BUILD_BRANCH=${{ github.ref_name }} + BUILD_REFERENCE=${{ github.ref_name }} diff --git a/.github/workflows/bake_to_version.yml b/.github/workflows/bake_to_version.yml new file mode 100644 index 0000000..7b13841 --- /dev/null +++ b/.github/workflows/bake_to_version.yml @@ -0,0 +1,62 @@ +name: Build version tags + +on: + workflow_dispatch: + push: + tags: + - "v*" + +jobs: + bake-latest: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Docker Setup QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + - name: Docker Setup Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build (full) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/docker-py3-kms/Dockerfile + platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6 + push: true + tags: | + pykmsorg/py-kms:${{ github.ref_name }}-full + ghcr.io/py-kms-organization/py-kms:${{ github.ref_name }}-full + build-args: | + BUILD_COMMIT=${{ github.sha }} + BUILD_REFERENCE=${{ github.ref_name }} + - name: Build (minimal) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/docker-py3-kms-minimal/Dockerfile + platforms: linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6 + push: true + tags: | + pykmsorg/py-kms:${{ github.ref_name }} + ghcr.io/py-kms-organization/py-kms:${{ github.ref_name }} + pykmsorg/py-kms:${{ github.ref_name }}-minimal + ghcr.io/py-kms-organization/py-kms:${{ github.ref_name }}-minimal + build-args: | + BUILD_COMMIT=${{ github.sha }} + BUILD_REFERENCE=${{ github.ref_name }} diff --git a/docker/docker-py3-kms/Dockerfile b/docker/docker-py3-kms/Dockerfile index 547171b..3c9846d 100644 --- a/docker/docker-py3-kms/Dockerfile +++ b/docker/docker-py3-kms/Dockerfile @@ -2,7 +2,7 @@ FROM alpine:3.22 ARG BUILD_COMMIT=unknown -ARG BUILD_BRANCH=unknown +ARG BUILD_REFERENCE=unknown ENV IP=:: ENV DUALSTACK=1 @@ -50,7 +50,7 @@ RUN chown root: -R /home/py-kms && \ # Web-interface specifics COPY LICENSE /LICENSE -RUN echo "$BUILD_COMMIT" > /VERSION && echo "$BUILD_BRANCH" >> /VERSION +RUN echo "$BUILD_COMMIT" > /VERSION && echo "$BUILD_REFERENCE" >> /VERSION WORKDIR /home/py-kms From bc7381895bf83515089664f99427150e09dacd0d Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:42:47 +0100 Subject: [PATCH 06/34] Specify the branch to base on Signed-off-by: simonmicro --- docs/Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Contributing.md b/docs/Contributing.md index 6718bae..3fad613 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -3,7 +3,7 @@ You want to improve this project? Awesome! But before you write or modify the existing source code, please note the following guideline: -- Always make sure to add your changes to the wiki. +- Always base your branch on the latest `next` branch to avoid merge conflicts. - 8-space indentation without tabs. - Docstrings as this: ```python From 1621f9a74582e0716f011290b1d74866f696a28e Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:54:19 +0100 Subject: [PATCH 07/34] Use correct non-branch wording Signed-off-by: simonmicro --- py-kms/pykms_WebUI.py | 2 +- py-kms/templates/base.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/py-kms/pykms_WebUI.py b/py-kms/pykms_WebUI.py index 06b7d7e..696e1bd 100644 --- a/py-kms/pykms_WebUI.py +++ b/py-kms/pykms_WebUI.py @@ -55,7 +55,7 @@ if os.path.exists(_version_info_path): with open(_version_info_path, 'r') as f: app.jinja_env.globals['version_info'] = { 'hash': f.readline().strip(), - 'branch': f.readline().strip() + 'reference': f.readline().strip() } _dbEnvVarName = 'PYKMS_SQLITE_DB_PATH' diff --git a/py-kms/templates/base.html b/py-kms/templates/base.html index f64588d..11110da 100644 --- a/py-kms/templates/base.html +++ b/py-kms/templates/base.html @@ -41,7 +41,7 @@ py-kms is online since {{ start_time }}. This instance was accessed {{ get_serve_count() }} times. View this softwares license here. {% if version_info %} -
This instance is running version "{{ version_info['hash'] }}" from branch "{{ version_info['branch'] }}" of py-kms. +
This instance is running version "{{ version_info['hash'] }}" from Git "{{ version_info['reference'] }}" of py-kms. {% endif %}

From 1a9ebe7eb15cd301317214867c7f8b8d54d36bac Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 19:02:35 +0100 Subject: [PATCH 08/34] Typo Signed-off-by: simonmicro --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c431e3..799f7df 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Semantic versioning is now being used in this project, so checkout the [GitHub R - Uses `sqlite` for persistent data storage (with a simple web-based explorer). ## Documentation -The wiki has been completly reworked and is now available on [readthedocs.io](https://py-kms.readthedocs.io/en/latest/). It should provide you all the necessary information about how to setup and to use _py-kms_, all without clumping this readme. The documentation also houses more details about the activation procedure with _py-kms_ and how to get GVLK keys. +The wiki has been completely reworked and is now available on [readthedocs.io](https://py-kms.readthedocs.io/en/latest/). It should provide you all the necessary information about how to setup and to use _py-kms_, all without clumping this readme. The documentation also houses more details about the activation procedure with _py-kms_ and how to get GVLK keys. ## 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`. From 047bff1232c28e487fc5aa5bc234906ba194f06c Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 19:48:54 +0100 Subject: [PATCH 09/34] Dropped changelog symlink Signed-off-by: simonmicro --- docs/changelog.md | 1 - 1 file changed, 1 deletion(-) delete mode 120000 docs/changelog.md diff --git a/docs/changelog.md b/docs/changelog.md deleted file mode 120000 index 04c99a5..0000000 --- a/docs/changelog.md +++ /dev/null @@ -1 +0,0 @@ -../CHANGELOG.md \ No newline at end of file From ecf50e06905f200a80d744cc9e19c4d0f32590e1 Mon Sep 17 00:00:00 2001 From: Rileran Date: Thu, 22 Jan 2026 15:53:54 +0100 Subject: [PATCH 10/34] fix: windows server 2019 activation failing because of incomplete entry in KmsDataBase.xml --- py-kms/pykms_PidGenerator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/py-kms/pykms_PidGenerator.py b/py-kms/pykms_PidGenerator.py index f73102a..8403b9c 100644 --- a/py-kms/pykms_PidGenerator.py +++ b/py-kms/pykms_PidGenerator.py @@ -27,6 +27,9 @@ def epidGenerator(kmsId, version, lcid): except IndexError: # fallback to Windows Server 2019 parameters. pkeys.append( ('206', '551000000', '570999999', '[0,1,2]') ) + except KeyError: + # ignore malformed/incomplete entries + pass pkey = random.choice(pkeys) GroupId, MinKeyId, MaxKeyId, Invalid = int(pkey[0]), int(pkey[1]), int(pkey[2]), literal_eval(pkey[3]) From 2d72cc858635efdc609c813a8ef05d4a555c87da Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 11 Apr 2026 15:19:25 +0200 Subject: [PATCH 11/34] Replaced dead watchtower with fork, fixes #142 Signed-off-by: simonmicro --- docs/Getting Started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Getting Started.md b/docs/Getting Started.md index 54d2b08..61a9776 100644 --- a/docs/Getting Started.md +++ b/docs/Getting Started.md @@ -14,7 +14,7 @@ command will download, "install" and start _py-kms_ and also keep it alive after ```bash docker run -d --name py-kms --restart always -p 1688:1688 -v /etc/localtime:/etc/localtime:ro ghcr.io/py-kms-organization/py-kms ``` -If you just want to use the image and don't want to build them yourself, you can always use the official image at the [GitHub Container Registry](https://github.com/Py-KMS-Organization/py-kms/pkgs/container/py-kms) (`ghcr.io/py-kms-organization/py-kms`). To ensure that you are using always the latest version you should check something like [watchtower](https://github.com/containrrr/watchtower) out! +If you just want to use the image and don't want to build them yourself, you can always use the official image at the [GitHub Container Registry](https://github.com/Py-KMS-Organization/py-kms/pkgs/container/py-kms) (`ghcr.io/py-kms-organization/py-kms`). To ensure that you are using always the latest version you should check something like [watchtower](https://github.com/nicholas-fedor/watchtower/) out! #### Tags There are currently three tags of the image available (select one just by appending `:` to the image from above): From eae3e5f7a6a99188129649d395e93354cb28cf44 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 18:59:08 +0100 Subject: [PATCH 12/34] 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 4c1d7b5bbc0f7a168cb4467afa942b84c84855e3 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:32:46 +0100 Subject: [PATCH 13/34] 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 2baf218fd46b5f9c6f51089a3fef1588a3af4a1f Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:34:40 +0100 Subject: [PATCH 14/34] 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 a5502e5a1c3a1bc625263914c694c322c7e1365e Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:35:19 +0100 Subject: [PATCH 15/34] 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 eafe3874ffb7db51152f4b8dd1781b56b088a3c4 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:35:58 +0100 Subject: [PATCH 16/34] 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 25679ef6275d1208a1a93037b8f2b5c239e1bbe6 Mon Sep 17 00:00:00 2001 From: simonmicro Date: Sat, 6 Dec 2025 20:36:20 +0100 Subject: [PATCH 17/34] 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 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`.