diff --git a/.github/workflows/c_release.yml b/.github/workflows/c_release.yml
index 9efaab995..0e98f1985 100644
--- a/.github/workflows/c_release.yml
+++ b/.github/workflows/c_release.yml
@@ -125,7 +125,7 @@ jobs:
with:
ref: c_release-${{github.run_number}}
- uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
- - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
+ - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
- run: |
docker buildx build -t atsigncompany/sshnpdc -f sshnpd/tools/Dockerfile.package \
--platform ${{ matrix.platform }} -o type=tar,dest=bins.tar .
@@ -163,7 +163,7 @@ jobs:
with:
ref: c_release-${{github.run_number}}
- uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
- - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
+ - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
- run: |
docker buildx build -t atsigncompany/sshnpdcmusl -f sshnpd/tools/Dockerfile.musl \
--platform ${{ matrix.platform }} -o type=tar,dest=bins.tar .
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 219ea20e1..83beb12f3 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -50,7 +50,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
+ uses: github/codeql-action/init@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -60,7 +60,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
+ uses: github/codeql-action/autobuild@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -73,6 +73,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
+ uses: github/codeql-action/analyze@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dockerhub_sshnpd.yml b/.github/workflows/dockerhub_sshnpd.yml
index 9790195b9..641fe40bc 100644
--- a/.github/workflows/dockerhub_sshnpd.yml
+++ b/.github/workflows/dockerhub_sshnpd.yml
@@ -35,7 +35,7 @@ jobs:
- name: Set up QEMU
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
+ uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
diff --git a/.github/workflows/multibuild.yaml b/.github/workflows/multibuild.yaml
index ca3b628a3..1d1b4fca8 100644
--- a/.github/workflows/multibuild.yaml
+++ b/.github/workflows/multibuild.yaml
@@ -189,7 +189,7 @@ jobs:
- if: ${{ ! inputs.main_build_only }}
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
- if: ${{ ! inputs.main_build_only }}
- uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
+ uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
- if: ${{ ! inputs.main_build_only }}
run: |
docker buildx build -t atsigncompany/sshnptarball -f ./tools/multibuild/Dockerfile.package \
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index dd59e3e55..707003c17 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@aa578102511db1f4524ed59b8cc2bae4f6e88195 # v3.27.6
+ uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
with:
sarif_file: results.sarif
diff --git a/.github/workflows/unit_tests.yaml b/.github/workflows/unit_tests.yaml
index 7409894cf..0d603dd96 100644
--- a/.github/workflows/unit_tests.yaml
+++ b/.github/workflows/unit_tests.yaml
@@ -31,7 +31,7 @@ jobs:
- uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 # v1.7.0
with:
sdk: ${{ matrix.dart-channel}}
- - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
go-version: "stable"
cache-dependency-path: tools/osv-scanner/go.sum
diff --git a/packages/dart/sshnoports/pubspec.lock b/packages/dart/sshnoports/pubspec.lock
index 2b9a48967..42481c0b0 100644
--- a/packages/dart/sshnoports/pubspec.lock
+++ b/packages/dart/sshnoports/pubspec.lock
@@ -5,23 +5,23 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
- sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
+ sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
- version: "73.0.0"
+ version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
- version: "0.3.2"
+ version: "0.3.3"
analyzer:
dependency: transitive
description:
name: analyzer
- sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
+ sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
- version: "6.8.0"
+ version: "6.11.0"
archive:
dependency: transitive
description:
@@ -59,10 +59,10 @@ packages:
dependency: transitive
description:
name: at_auth
- sha256: "4fba41a421cc55e0f53b2a506500d61d7a374bcf073e57927b9aeb457676cb5d"
+ sha256: "4d23e7ffd799104ccc74ce006a8a286dcefaeafcc8f72c6bcbbc6a893244abeb"
url: "https://pub.dev"
source: hosted
- version: "2.0.9"
+ version: "2.0.10"
at_base2e15:
dependency: transitive
description:
@@ -99,10 +99,10 @@ packages:
dependency: transitive
description:
name: at_commons
- sha256: "08a7be508c85f9b21d09a1d95225bb5164d20426c6d8564e1c31a74843b67ca7"
+ sha256: "02e9c06edf0d7fc9b68ee9df79c801a0e5e877c5eb7505d76adbd6fe691e6101"
url: "https://pub.dev"
source: hosted
- version: "5.1.0"
+ version: "5.1.1"
at_demo_data:
dependency: transitive
description:
@@ -123,10 +123,10 @@ packages:
dependency: "direct main"
description:
name: at_onboarding_cli
- sha256: f3eb8789671fd1580607d94b045992e9fee2ac3eb19797decc47466ac8a7d242
+ sha256: b5a4b5595f37ae4dfc5ffdec9302b5bd67309dbf29ecbb4483c7cfe17c260223
url: "https://pub.dev"
source: hosted
- version: "1.8.0"
+ version: "1.8.1"
at_persistence_secondary_server:
dependency: transitive
description:
@@ -211,18 +211,18 @@ packages:
dependency: "direct dev"
description:
name: build_runner
- sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
+ sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
url: "https://pub.dev"
source: hosted
- version: "2.4.13"
+ version: "2.4.14"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
- sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
+ sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
url: "https://pub.dev"
source: hosted
- version: "7.3.2"
+ version: "8.0.0"
build_version:
dependency: "direct dev"
description:
@@ -524,10 +524,10 @@ packages:
dependency: "direct dev"
description:
name: lints
- sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
+ sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3"
url: "https://pub.dev"
source: hosted
- version: "5.0.0"
+ version: "5.1.0"
logging:
dependency: "direct main"
description:
@@ -540,10 +540,10 @@ packages:
dependency: transitive
description:
name: macros
- sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
+ sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
- version: "0.1.2-main.4"
+ version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
@@ -811,10 +811,10 @@ packages:
dependency: "direct dev"
description:
name: test
- sha256: "22eb7769bee38c7e032d532e8daa2e1cc901b799f603550a4db8f3a5f5173ea2"
+ sha256: "43490fe4c0f5ecb898f3fa1cdcdad8d521d7f6ff17ebdc4e8cd32b2e99524a20"
url: "https://pub.dev"
source: hosted
- version: "1.25.12"
+ version: "1.25.13"
test_api:
dependency: transitive
description:
diff --git a/packages/dart/sshnoports/pubspec.yaml b/packages/dart/sshnoports/pubspec.yaml
index c9e0b988d..4c23e46df 100644
--- a/packages/dart/sshnoports/pubspec.yaml
+++ b/packages/dart/sshnoports/pubspec.yaml
@@ -10,7 +10,7 @@ dependencies:
noports_core:
path: "../noports_core"
version: 6.2.1
- at_onboarding_cli: 1.8.0
+ at_onboarding_cli: 1.8.1
at_cli_commons: 1.3.0
at_client: 3.3.0
args: 2.6.0
@@ -33,8 +33,8 @@ dependency_overrides:
url: https://github.com/gkc/args
dev_dependencies:
- lints: ^5.0.0
- test: ^1.25.12
+ lints: ^5.1.0
+ test: ^1.25.13
mocktail: ^1.0.4
- build_runner: ^2.4.13
+ build_runner: ^2.4.14
build_version: ^2.1.1
diff --git a/packages/dart/sshnoports/tools/Dockerfile b/packages/dart/sshnoports/tools/Dockerfile
index 04b89ecdc..dce171114 100644
--- a/packages/dart/sshnoports/tools/Dockerfile
+++ b/packages/dart/sshnoports/tools/Dockerfile
@@ -1,7 +1,7 @@
# Dockerfile
# Build image for a containerized instance of sshnpd
-FROM dart:3.5.4@sha256:3f3877b9a75a1695dd284151d2dab5787bc6cefd04313b6a8e0bee98230d347b AS buildimage
+FROM dart:3.6.0@sha256:b677df29e01bca3a2d5bdaffe204498834d56a4ed38ff56c0ac6a42d6541b58e AS buildimage
ENV PACKAGEDIR=packages/dart/sshnoports
ENV BINARYDIR=/usr/local/at
SHELL ["/bin/bash", "-c"]
diff --git a/packages/dart/sshnoports/tools/Dockerfile.activate b/packages/dart/sshnoports/tools/Dockerfile.activate
index 976592e1b..df0570a3d 100644
--- a/packages/dart/sshnoports/tools/Dockerfile.activate
+++ b/packages/dart/sshnoports/tools/Dockerfile.activate
@@ -1,6 +1,6 @@
# Dockerfile.activate
# Build image for a containerized call of the at_activate binary
-FROM dart:3.5.4@sha256:3f3877b9a75a1695dd284151d2dab5787bc6cefd04313b6a8e0bee98230d347b AS buildimage
+FROM dart:3.6.0@sha256:b677df29e01bca3a2d5bdaffe204498834d56a4ed38ff56c0ac6a42d6541b58e AS buildimage
ENV PACKAGEDIR=packages/dart/sshnoports
ENV BINARYDIR=/usr/local/at
SHELL ["/bin/bash", "-c"]
diff --git a/packages/dart/sshnoports/tools/Dockerfile.sshnpd-slim b/packages/dart/sshnoports/tools/Dockerfile.sshnpd-slim
index 5fcc4e5c2..364f5c88e 100644
--- a/packages/dart/sshnoports/tools/Dockerfile.sshnpd-slim
+++ b/packages/dart/sshnoports/tools/Dockerfile.sshnpd-slim
@@ -9,7 +9,7 @@
# as of 5th Feb 2024 - Will check state as 3.3 Stable is released
#FROM dart:beta-sdk AS buildimage
-FROM dart:3.5.4@sha256:3f3877b9a75a1695dd284151d2dab5787bc6cefd04313b6a8e0bee98230d347b AS buildimage
+FROM dart:3.6.0@sha256:b677df29e01bca3a2d5bdaffe204498834d56a4ed38ff56c0ac6a42d6541b58e AS buildimage
ENV PACKAGEDIR=packages/dart/sshnoports
ENV OPENSSH=tools/static-openssh
ENV BINARYDIR=/usr/local/at
diff --git a/tests/end2end_tests/image/Dockerfile b/tests/end2end_tests/image/Dockerfile
index 83e6f76b5..7b8e2149f 100644
--- a/tests/end2end_tests/image/Dockerfile
+++ b/tests/end2end_tests/image/Dockerfile
@@ -26,7 +26,7 @@ RUN set -eux ; \
# BRANCH
# BUILD BRANCH
-FROM dart:3.5.4@sha256:3f3877b9a75a1695dd284151d2dab5787bc6cefd04313b6a8e0bee98230d347b AS build-branch
+FROM dart:3.6.0@sha256:b677df29e01bca3a2d5bdaffe204498834d56a4ed38ff56c0ac6a42d6541b58e AS build-branch
ENV URL=https://github.com/atsign-foundation/noports.git
ENV REPO_DIR=/app/repo
@@ -65,7 +65,7 @@ ENTRYPOINT cp -r /mount/. ${HOMEDIR} && sudo service ssh start && sh ${HOMEDIR}/
# LOCAL
# BUILD LOCAL
-FROM dart:3.5.4@sha256:3f3877b9a75a1695dd284151d2dab5787bc6cefd04313b6a8e0bee98230d347b AS build-local
+FROM dart:3.6.0@sha256:b677df29e01bca3a2d5bdaffe204498834d56a4ed38ff56c0ac6a42d6541b58e AS build-local
ENV REPO_DIR=/app/repo
ENV PACKAGE_DIR=${REPO_DIR}/packages/dart/sshnoports
diff --git a/tools/.gitignore b/tools/.gitignore
index 50b5af78f..b940e2b1b 100644
--- a/tools/.gitignore
+++ b/tools/.gitignore
@@ -1 +1,10 @@
macos-signing.env
+
+#.NET Related
+.vs
+bin
+obj
+*.user
+launchSettings.json
+windows-installer/NoPortsInstaller/Resources/*
+windows-installer/NoPortsHealthCheck/publish
\ No newline at end of file
diff --git a/tools/multibuild/Dockerfile.package b/tools/multibuild/Dockerfile.package
index 6ac3fce1d..bc9c2aa0f 100644
--- a/tools/multibuild/Dockerfile.package
+++ b/tools/multibuild/Dockerfile.package
@@ -1,7 +1,7 @@
# Dockerfile.package
# A dockerfile for packaging SSH No Ports releases using docker buildx
-FROM atsigncompany/buildimage:3.5.4@sha256:0d21c9f6dc856f1e3df933b99dd88a4833057ffee9665857d31f32d32a9d1a74 AS build
+FROM atsigncompany/buildimage:3.6.0@sha256:bbaa6d8f5cf485988d2974cbd206645a096fb215cd220fd577652951f52eccd5 AS build
# Using atsigncompany/buildimage until official dart image has RISC-V support
# See https://github.com/atsign-company/at_dockerfiles for source and automated builds
WORKDIR /noports
diff --git a/tools/windows-installer/NoPortsHealthCheck/App.config b/tools/windows-installer/NoPortsHealthCheck/App.config
new file mode 100644
index 000000000..3916e0e4b
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/windows-installer/NoPortsHealthCheck/Form1.Designer.cs b/tools/windows-installer/NoPortsHealthCheck/Form1.Designer.cs
new file mode 100644
index 000000000..bccf76db7
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/Form1.Designer.cs
@@ -0,0 +1,239 @@
+namespace NoPortsHealthCheck
+{
+ partial class Form1
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.label1 = new System.Windows.Forms.Label();
+ this.label2 = new System.Windows.Forms.Label();
+ this.label3 = new System.Windows.Forms.Label();
+ this.label4 = new System.Windows.Forms.Label();
+ this.label5 = new System.Windows.Forms.Label();
+ this.label6 = new System.Windows.Forms.Label();
+ this.label7 = new System.Windows.Forms.Label();
+ this.checkBox1 = new System.Windows.Forms.CheckBox();
+ this.checkBox2 = new System.Windows.Forms.CheckBox();
+ this.checkBox3 = new System.Windows.Forms.CheckBox();
+ this.checkBox4 = new System.Windows.Forms.CheckBox();
+ this.checkBox5 = new System.Windows.Forms.CheckBox();
+ this.checkBox6 = new System.Windows.Forms.CheckBox();
+ this.checkBox7 = new System.Windows.Forms.CheckBox();
+ this.label8 = new System.Windows.Forms.Label();
+ this.SuspendLayout();
+ //
+ // label1
+ //
+ this.label1.AutoSize = true;
+ this.label1.Location = new System.Drawing.Point(25, 61);
+ this.label1.Name = "label1";
+ this.label1.Size = new System.Drawing.Size(100, 13);
+ this.label1.TabIndex = 0;
+ this.label1.Text = "Admin Permissions?";
+ //
+ // label2
+ //
+ this.label2.AutoSize = true;
+ this.label2.Location = new System.Drawing.Point(25, 96);
+ this.label2.Name = "label2";
+ this.label2.Size = new System.Drawing.Size(121, 13);
+ this.label2.TabIndex = 1;
+ this.label2.Text = "Can write to user home?";
+ //
+ // label3
+ //
+ this.label3.AutoSize = true;
+ this.label3.Location = new System.Drawing.Point(25, 127);
+ this.label3.Name = "label3";
+ this.label3.Size = new System.Drawing.Size(143, 13);
+ this.label3.TabIndex = 2;
+ this.label3.Text = "Can write to Local app data?";
+ //
+ // label4
+ //
+ this.label4.AutoSize = true;
+ this.label4.Location = new System.Drawing.Point(25, 157);
+ this.label4.Name = "label4";
+ this.label4.Size = new System.Drawing.Size(137, 13);
+ this.label4.TabIndex = 3;
+ this.label4.Text = "Can write to Local Service?";
+ //
+ // label5
+ //
+ this.label5.AutoSize = true;
+ this.label5.Location = new System.Drawing.Point(25, 187);
+ this.label5.Name = "label5";
+ this.label5.Size = new System.Drawing.Size(110, 13);
+ this.label5.TabIndex = 4;
+ this.label5.Text = "Can write to Registry?";
+ //
+ // label6
+ //
+ this.label6.AutoSize = true;
+ this.label6.Font = new System.Drawing.Font("Arial", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.label6.Location = new System.Drawing.Point(14, 9);
+ this.label6.Name = "label6";
+ this.label6.Size = new System.Drawing.Size(196, 22);
+ this.label6.TabIndex = 5;
+ this.label6.Text = "NoPorts Health Check";
+ //
+ // label7
+ //
+ this.label7.AutoSize = true;
+ this.label7.Location = new System.Drawing.Point(25, 221);
+ this.label7.Name = "label7";
+ this.label7.Size = new System.Drawing.Size(173, 13);
+ this.label7.TabIndex = 6;
+ this.label7.Text = "Can modify Environment Variables?";
+ //
+ // checkBox1
+ //
+ this.checkBox1.AutoCheck = false;
+ this.checkBox1.AutoSize = true;
+ this.checkBox1.Location = new System.Drawing.Point(229, 61);
+ this.checkBox1.Name = "checkBox1";
+ this.checkBox1.Size = new System.Drawing.Size(15, 14);
+ this.checkBox1.TabIndex = 7;
+ this.checkBox1.UseVisualStyleBackColor = true;
+ //
+ // checkBox2
+ //
+ this.checkBox2.AutoCheck = false;
+ this.checkBox2.AutoSize = true;
+ this.checkBox2.Location = new System.Drawing.Point(229, 96);
+ this.checkBox2.Name = "checkBox2";
+ this.checkBox2.Size = new System.Drawing.Size(15, 14);
+ this.checkBox2.TabIndex = 8;
+ this.checkBox2.UseVisualStyleBackColor = true;
+ //
+ // checkBox3
+ //
+ this.checkBox3.AutoCheck = false;
+ this.checkBox3.AutoSize = true;
+ this.checkBox3.Location = new System.Drawing.Point(229, 127);
+ this.checkBox3.Name = "checkBox3";
+ this.checkBox3.Size = new System.Drawing.Size(15, 14);
+ this.checkBox3.TabIndex = 9;
+ this.checkBox3.UseVisualStyleBackColor = true;
+ //
+ // checkBox4
+ //
+ this.checkBox4.AutoCheck = false;
+ this.checkBox4.AutoSize = true;
+ this.checkBox4.Location = new System.Drawing.Point(229, 157);
+ this.checkBox4.Name = "checkBox4";
+ this.checkBox4.Size = new System.Drawing.Size(15, 14);
+ this.checkBox4.TabIndex = 10;
+ this.checkBox4.UseVisualStyleBackColor = true;
+ //
+ // checkBox5
+ //
+ this.checkBox5.AutoCheck = false;
+ this.checkBox5.AutoSize = true;
+ this.checkBox5.Location = new System.Drawing.Point(229, 186);
+ this.checkBox5.Name = "checkBox5";
+ this.checkBox5.Size = new System.Drawing.Size(15, 14);
+ this.checkBox5.TabIndex = 11;
+ this.checkBox5.UseVisualStyleBackColor = true;
+ //
+ // checkBox6
+ //
+ this.checkBox6.AutoCheck = false;
+ this.checkBox6.AutoSize = true;
+ this.checkBox6.Location = new System.Drawing.Point(229, 221);
+ this.checkBox6.Name = "checkBox6";
+ this.checkBox6.Size = new System.Drawing.Size(15, 14);
+ this.checkBox6.TabIndex = 12;
+ this.checkBox6.UseVisualStyleBackColor = true;
+ //
+ // checkBox7
+ //
+ this.checkBox7.AutoCheck = false;
+ this.checkBox7.AutoSize = true;
+ this.checkBox7.Location = new System.Drawing.Point(229, 251);
+ this.checkBox7.Name = "checkBox7";
+ this.checkBox7.Size = new System.Drawing.Size(15, 14);
+ this.checkBox7.TabIndex = 14;
+ this.checkBox7.UseVisualStyleBackColor = true;
+ this.checkBox7.CheckedChanged += new System.EventHandler(this.checkBox7_CheckedChanged);
+ //
+ // label8
+ //
+ this.label8.AutoSize = true;
+ this.label8.Location = new System.Drawing.Point(25, 251);
+ this.label8.Name = "label8";
+ this.label8.Size = new System.Drawing.Size(107, 13);
+ this.label8.TabIndex = 13;
+ this.label8.Text = "Can create services?";
+ this.label8.Click += new System.EventHandler(this.label8_Click);
+ //
+ // Form1
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(256, 292);
+ this.Controls.Add(this.checkBox7);
+ this.Controls.Add(this.label8);
+ this.Controls.Add(this.checkBox6);
+ this.Controls.Add(this.checkBox5);
+ this.Controls.Add(this.checkBox4);
+ this.Controls.Add(this.checkBox3);
+ this.Controls.Add(this.checkBox2);
+ this.Controls.Add(this.checkBox1);
+ this.Controls.Add(this.label7);
+ this.Controls.Add(this.label6);
+ this.Controls.Add(this.label5);
+ this.Controls.Add(this.label4);
+ this.Controls.Add(this.label3);
+ this.Controls.Add(this.label2);
+ this.Controls.Add(this.label1);
+ this.Name = "Form1";
+ this.Text = "Form1";
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Label label1;
+ private System.Windows.Forms.Label label2;
+ private System.Windows.Forms.Label label3;
+ private System.Windows.Forms.Label label4;
+ private System.Windows.Forms.Label label5;
+ private System.Windows.Forms.Label label6;
+ private System.Windows.Forms.Label label7;
+ private System.Windows.Forms.CheckBox checkBox1;
+ private System.Windows.Forms.CheckBox checkBox2;
+ private System.Windows.Forms.CheckBox checkBox3;
+ private System.Windows.Forms.CheckBox checkBox4;
+ private System.Windows.Forms.CheckBox checkBox5;
+ private System.Windows.Forms.CheckBox checkBox6;
+ private System.Windows.Forms.CheckBox checkBox7;
+ private System.Windows.Forms.Label label8;
+ }
+}
+
diff --git a/tools/windows-installer/NoPortsHealthCheck/Form1.cs b/tools/windows-installer/NoPortsHealthCheck/Form1.cs
new file mode 100644
index 000000000..f9188024a
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/Form1.cs
@@ -0,0 +1,58 @@
+using Microsoft.Win32;
+using System;
+using System.Windows.Forms;
+
+namespace NoPortsHealthCheck
+{
+ public partial class Form1 : Form
+ {
+ static readonly string userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ static readonly string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ readonly bool isUserAdmin = Program.IsUserAdmin();
+ readonly bool canWriteUserHome = Program.CanWriteToDirectory(userHome);
+ readonly bool canWriteLocalAppData = Program.CanWriteToDirectory(localAppData);
+ readonly bool canWriteNetworkService = Program.CanWriteToDirectory(Environment.ExpandEnvironmentVariables("%systemroot%")
+ + @"\ServiceProfiles\LocalService\");
+ readonly bool canWriteRegistry = Program.CanWriteToRegistry(Registry.LocalMachine, "SOFTWARE\\TestKey");
+ readonly bool canModifyPath = Program.CanModifyEnvironmentVariable();
+ readonly bool canCreateServices = Program.CanCreateServices();
+ public Form1()
+ {
+ InitializeComponent();
+ this.Text = "NoPorts Health Check";
+ checkBox1.Checked = isUserAdmin;
+ if (isUserAdmin)
+ {
+ Program.Log("User is Admin");
+ }
+ else
+ {
+ Program.Log("User is not Admin, try running as admin.");
+ }
+ checkBox2.Checked = canWriteUserHome;
+ checkBox3.Checked = canWriteLocalAppData;
+ checkBox4.Checked = canWriteNetworkService;
+ checkBox5.Checked = canWriteRegistry;
+ checkBox6.Checked = canModifyPath;
+ checkBox7.Checked = canCreateServices;
+ if (isUserAdmin && canWriteUserHome && canWriteLocalAppData && canWriteNetworkService && canWriteRegistry && canModifyPath && canCreateServices)
+ {
+ Program.ShowSuccessLogs();
+ }
+ if (!isUserAdmin)
+ {
+ Program.ShowErrorLogs();
+ }
+ }
+
+ private void label8_Click(object sender, EventArgs e)
+ {
+
+ }
+
+ private void checkBox7_CheckedChanged(object sender, EventArgs e)
+ {
+
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsHealthCheck/Form1.resx b/tools/windows-installer/NoPortsHealthCheck/Form1.resx
new file mode 100644
index 000000000..29dcb1b3a
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/Form1.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/tools/windows-installer/NoPortsHealthCheck/NoPortsHealthCheck.csproj b/tools/windows-installer/NoPortsHealthCheck/NoPortsHealthCheck.csproj
new file mode 100644
index 000000000..4ea18028e
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/NoPortsHealthCheck.csproj
@@ -0,0 +1,128 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {CA561C30-B24B-4343-864F-B5115BF93358}
+ WinExe
+ NoPortsHealthCheck
+ NoPortsHealthCheck
+ v4.8
+ 512
+ true
+ true
+ true
+ publish\
+ true
+ Web
+ true
+ Foreground
+ 7
+ Days
+ false
+ false
+ true
+ https://localhost:8080/as/
+ true
+ publish.htm
+ 2
+ 1.0.0.%2a
+ false
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ 5DDC12A5906EFD65E20A1FC8E8DD9BBEF891A664
+
+
+ NoPortsHealthCheck_TemporaryKey.pfx
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Form
+
+
+ Form1.cs
+
+
+
+
+
+ Form1.cs
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+ Designer
+
+
+ True
+ Resources.resx
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+ True
+ Settings.settings
+ True
+
+
+
+
+
+
+
+ False
+ Microsoft .NET Framework 4.8 %28x86 and x64%29
+ true
+
+
+ False
+ .NET Framework 3.5 SP1
+ false
+
+
+
+
\ No newline at end of file
diff --git a/tools/windows-installer/NoPortsHealthCheck/Program.cs b/tools/windows-installer/NoPortsHealthCheck/Program.cs
new file mode 100644
index 000000000..0d5712f73
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/Program.cs
@@ -0,0 +1,153 @@
+using Microsoft.Win32;
+using NoPortsInstaller;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Security.Principal;
+using System.Windows.Forms;
+
+namespace NoPortsHealthCheck
+{
+ internal static class Program
+ {
+ ///
+ /// The main entry point for the application.
+ ///
+ [STAThread]
+ static void Main()
+ {
+ Application.EnableVisualStyles();
+ Application.SetCompatibleTextRenderingDefault(false);
+ Application.Run(new Form1());
+ }
+
+
+ private static readonly List logs = new List();
+
+
+ public static bool CanWriteToDirectory(string path)
+ {
+ try
+ {
+ string testFilePath = Path.Combine(path, "test.txt");
+ File.WriteAllText(testFilePath, "test");
+ Log($"Wrote to directory: {path}");
+ File.Delete(testFilePath);
+ Log($"Deleted file: {testFilePath}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogErr($"Failed {path}", ex);
+ ShowErrorLogs();
+ return false;
+ }
+ }
+
+ public static bool IsUserAdmin()
+ {
+ using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
+ {
+ WindowsPrincipal principal = new WindowsPrincipal(identity);
+ try
+ {
+ Log($"Account: {principal.Identity}");
+ return principal.IsInRole(WindowsBuiltInRole.Administrator);
+ }
+ catch (Exception ex)
+ {
+ LogErr("Failed to check if user is admin", ex);
+ ShowErrorLogs();
+ return false;
+ }
+ }
+ }
+
+ public static bool CanWriteToRegistry(RegistryKey baseKey, string subKey)
+ {
+ try
+ {
+ Log("Creating registry key");
+ baseKey.CreateSubKey(subKey);
+ }
+ catch (Exception ex)
+ {
+ LogErr($"Failed to create registry key {baseKey} {subKey}", ex);
+ ShowErrorLogs();
+ return false;
+ }
+
+ try
+ {
+ using (var key = baseKey.OpenSubKey(subKey, true))
+ {
+ key.SetValue("test", "test");
+ Log("Wrote to registry key");
+ key.DeleteValue("test");
+ Log("Deleted registry key");
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ LogErr($"Failed to write to registry {baseKey} {subKey}", ex);
+ ShowErrorLogs();
+ return false;
+ }
+ }
+
+ public static bool CanModifyEnvironmentVariable()
+ {
+ try
+ {
+ Environment.SetEnvironmentVariable("TEST_ENV_VAR", "test_value", EnvironmentVariableTarget.User);
+ Log("Modified environment variable");
+ Environment.SetEnvironmentVariable("TEST_ENV_VAR", null, EnvironmentVariableTarget.User); // Clean up
+ Log("Deleted environment variable");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogErr("Failed to modify environment variable", ex);
+ ShowErrorLogs();
+ return false;
+ }
+ }
+
+ public static bool CanCreateServices()
+ {
+ try
+ {
+ ServiceController.Install("TestService", "Test Service", "C:\\Windows\\System32\\svchost.exe");
+ ServiceController.TryUninstall("TestService");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogErr("Failed to create service", ex);
+ ShowErrorLogs();
+ return false;
+ }
+ }
+
+ public static void Log(string what)
+ {
+ logs.Add(what);
+ }
+
+ public static void LogErr(string what, Exception ex)
+ {
+ logs.Add($"{what} - {ex.Message} Stack Trace {ex.StackTrace}");
+ }
+
+ public static void ShowSuccessLogs()
+ {
+ MessageBox.Show($"All checks passed \n \n {string.Join(Environment.NewLine, logs)}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
+
+ public static void ShowErrorLogs()
+ {
+ MessageBox.Show(string.Join(Environment.NewLine, logs), "Health Check Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsHealthCheck/Properties/AssemblyInfo.cs b/tools/windows-installer/NoPortsHealthCheck/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..e698b7cf1
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/Properties/AssemblyInfo.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("NoPortsHealthCheck")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("NoPortsHealthCheck")]
+[assembly: AssemblyCopyright("Copyright © 2024")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("ca561c30-b24b-4343-864f-b5115bf93358")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/tools/windows-installer/NoPortsHealthCheck/Properties/Resources.resx b/tools/windows-installer/NoPortsHealthCheck/Properties/Resources.resx
new file mode 100644
index 000000000..ffecec851
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/tools/windows-installer/NoPortsHealthCheck/ServiceController.cs b/tools/windows-installer/NoPortsHealthCheck/ServiceController.cs
new file mode 100644
index 000000000..8f408ba97
--- /dev/null
+++ b/tools/windows-installer/NoPortsHealthCheck/ServiceController.cs
@@ -0,0 +1,465 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace NoPortsInstaller
+{
+ public static class ServiceController
+ {
+ private const int STANDARD_RIGHTS_REQUIRED = 0xF0000;
+ private const int SERVICE_WIN32_OWN_PROCESS = 0x00000010;
+
+ [StructLayout(LayoutKind.Sequential)]
+ private class SERVICE_STATUS
+ {
+ public int dwServiceType = 0;
+ public ServiceState dwCurrentState = 0;
+ public int dwControlsAccepted = 0;
+ public int dwWin32ExitCode = 0;
+ public int dwServiceSpecificExitCode = 0;
+ public int dwCheckPoint = 0;
+ public int dwWaitHint = 0;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private class SERVICE_FAILURE_ACTIONS
+ {
+ public int dwResetPeriod = 0;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string lpRebootMsg = null;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string lpCommand = null;
+ public int cActions = 0;
+ public SC_ACTION[] lpsaActions = new SC_ACTION[10];
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private class SC_ACTION
+ {
+ public int type;
+ public int delay;
+ }
+
+ #region OpenSCManager
+ [DllImport("advapi32.dll", EntryPoint = "OpenSCManagerW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
+ static extern IntPtr OpenSCManager(string machineName, string databaseName, ScmAccessRights dwDesiredAccess);
+ #endregion
+
+ #region OpenService
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, ServiceAccessRights dwDesiredAccess);
+ #endregion
+
+ #region CreateService
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern IntPtr CreateService(IntPtr hSCManager, string lpServiceName, string lpDisplayName, ServiceAccessRights dwDesiredAccess, int dwServiceType, ServiceBootFlag dwStartType, ServiceError dwErrorControl, string lpBinaryPathName, string lpLoadOrderGroup, IntPtr lpdwTagId, string lpDependencies, string lp, string lpPassword);
+ #endregion
+
+ #region CloseServiceHandle
+ [DllImport("advapi32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ static extern bool CloseServiceHandle(IntPtr hSCObject);
+ #endregion
+
+ #region QueryServiceStatus
+ [DllImport("advapi32.dll")]
+ private static extern int QueryServiceStatus(IntPtr hService, SERVICE_STATUS lpServiceStatus);
+ #endregion
+
+ #region QueryServiceConfig
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern int ChangeServiceConfig(IntPtr hService, int dwInfoLevel, IntPtr lpInfo);
+ #endregion
+
+ #region DeleteService
+ [DllImport("advapi32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool DeleteService(IntPtr hService);
+ #endregion
+
+ #region ControlService
+ [DllImport("advapi32.dll")]
+ private static extern int ControlService(IntPtr hService, ServiceControl dwControl, SERVICE_STATUS lpServiceStatus);
+ #endregion
+
+ #region StartService
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern int StartService(IntPtr hService, int dwNumServiceArgs, int lpServiceArgVectors);
+ #endregion
+
+ public static void Uninstall(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.AllAccess);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.AllAccess);
+ if (service == IntPtr.Zero)
+ {
+ throw new ApplicationException("Service not installed.");
+ }
+
+ try
+ {
+ StopService(service);
+ if (!DeleteService(service))
+ {
+ throw new ApplicationException("Could not delete service " + Marshal.GetLastWin32Error());
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ public static bool ServiceIsInstalled(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.Connect);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.QueryStatus);
+
+ if (service == IntPtr.Zero)
+ {
+ return false;
+ }
+
+ CloseServiceHandle(service);
+ return true;
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ public static void Install(string serviceName, string displayName, string fileName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.AllAccess);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.AllAccess);
+
+ if (service == IntPtr.Zero)
+ {
+
+ service = CreateService(scm, serviceName, displayName, ServiceAccessRights.AllAccess, SERVICE_WIN32_OWN_PROCESS, ServiceBootFlag.AutoStart, ServiceError.Normal, fileName, null, IntPtr.Zero, null, @"NT AUTHORITY\LocalService", null);
+ }
+
+ if (service == IntPtr.Zero)
+ {
+ throw new ApplicationException("Failed to install service.");
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ public static void StartService(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.Connect);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.QueryStatus | ServiceAccessRights.Start);
+ if (service == IntPtr.Zero)
+ {
+ throw new ApplicationException("Could not open service.");
+ }
+
+ try
+ {
+ StartService(service);
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ public static void StopService(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.Connect);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.QueryStatus | ServiceAccessRights.Stop);
+ if (service == IntPtr.Zero)
+ {
+ throw new ApplicationException("Could not open service.");
+ }
+
+ try
+ {
+ StopService(service);
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ private static void StartService(IntPtr service)
+ {
+ SERVICE_STATUS status = new SERVICE_STATUS();
+ StartService(service, 0, 0);
+ var changedStatus = WaitForServiceStatus(service, ServiceState.StartPending, ServiceState.Running);
+ if (!changedStatus)
+ {
+ throw new ApplicationException("Unable to start service");
+ }
+ }
+
+ private static void StopService(IntPtr service)
+ {
+ SERVICE_STATUS status = new SERVICE_STATUS();
+ ControlService(service, ServiceControl.Stop, status);
+ var changedStatus = WaitForServiceStatus(service, ServiceState.StopPending, ServiceState.Stopped);
+ if (!changedStatus)
+ {
+ throw new ApplicationException("Unable to stop service");
+ }
+ }
+
+ public static ServiceState GetServiceStatus(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.Connect);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.QueryStatus);
+ if (service == IntPtr.Zero)
+ {
+ return ServiceState.NotFound;
+ }
+
+ try
+ {
+ return GetServiceStatus(service);
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ private static ServiceState GetServiceStatus(IntPtr service)
+ {
+ SERVICE_STATUS status = new SERVICE_STATUS();
+
+ if (QueryServiceStatus(service, status) == 0)
+ {
+ throw new ApplicationException("Failed to query service status.");
+ }
+
+ return status.dwCurrentState;
+ }
+
+
+ private static bool WaitForServiceStatus(IntPtr service, ServiceState waitStatus, ServiceState desiredStatus)
+ {
+ SERVICE_STATUS status = new SERVICE_STATUS();
+
+ QueryServiceStatus(service, status);
+ if (status.dwCurrentState == desiredStatus)
+ {
+ return true;
+ }
+
+ int dwStartTickCount = Environment.TickCount;
+ int dwOldCheckPoint = status.dwCheckPoint;
+
+ while (status.dwCurrentState == waitStatus)
+ {
+ // Do not wait longer than the wait hint. A good interval is
+ // one tenth the wait hint, but no less than 1 second and no
+ // more than 10 seconds.
+
+ int dwWaitTime = status.dwWaitHint / 10;
+
+ if (dwWaitTime < 1000)
+ {
+ dwWaitTime = 1000;
+ }
+ else if (dwWaitTime > 10000)
+ {
+ dwWaitTime = 10000;
+ }
+
+ Thread.Sleep(dwWaitTime);
+
+ // Check the status again.
+
+ if (QueryServiceStatus(service, status) == 0)
+ {
+ break;
+ }
+
+ if (status.dwCheckPoint > dwOldCheckPoint)
+ {
+ // The service is making progress.
+ dwStartTickCount = Environment.TickCount;
+ dwOldCheckPoint = status.dwCheckPoint;
+ }
+ else
+ {
+ if (Environment.TickCount - dwStartTickCount > status.dwWaitHint)
+ {
+ // No progress made within the wait hint
+ break;
+ }
+ }
+ }
+ return (status.dwCurrentState == desiredStatus);
+ }
+
+ private static IntPtr OpenSCManager(ScmAccessRights rights)
+ {
+ IntPtr scm = OpenSCManager(null, null, rights);
+ if (scm == IntPtr.Zero)
+ {
+ throw new ApplicationException("Could not connect to service control manager.");
+ }
+
+ return scm;
+ }
+
+
+
+ public static async Task TryUninstall(string service)
+ {
+ if (ServiceController.ServiceIsInstalled(service))
+ {
+ int maxAttempts = 3;
+ for (int i = 0; i < maxAttempts; i++)
+ {
+ try
+ {
+ StopService(service);
+ Uninstall(service);
+ return;
+ }
+ catch (Exception ex)
+ {
+ if (i == maxAttempts - 1)
+ {
+ throw new Exception("Failed to uninstall service", ex);
+ }
+ await Task.Delay(500);
+ }
+ }
+ }
+ }
+ }
+
+ public enum ServiceState
+ {
+ Unknown = -1, // The state cannot be (has not been) retrieved.
+ NotFound = 0, // The service is not known on the host server.
+ Stopped = 1,
+ StartPending = 2,
+ StopPending = 3,
+ Running = 4,
+ ContinuePending = 5,
+ PausePending = 6,
+ Paused = 7
+ }
+
+ [Flags]
+ public enum ScmAccessRights
+ {
+ Connect = 0x0001,
+ CreateService = 0x0002,
+ EnumerateService = 0x0004,
+ Lock = 0x0008,
+ QueryLockStatus = 0x0010,
+ ModifyBootConfig = 0x0020,
+ StandardRightsRequired = 0xF0000,
+ AllAccess = (StandardRightsRequired | Connect | CreateService |
+ EnumerateService | Lock | QueryLockStatus | ModifyBootConfig)
+ }
+
+ [Flags]
+ public enum ServiceAccessRights
+ {
+ QueryConfig = 0x1,
+ ChangeConfig = 0x2,
+ QueryStatus = 0x4,
+ EnumerateDependants = 0x8,
+ Start = 0x10,
+ Stop = 0x20,
+ PauseContinue = 0x40,
+ Interrogate = 0x80,
+ UserDefinedControl = 0x100,
+ Delete = 0x00010000,
+ StandardRightsRequired = 0xF0000,
+ AllAccess = (StandardRightsRequired | QueryConfig | ChangeConfig |
+ QueryStatus | EnumerateDependants | Start | Stop | PauseContinue |
+ Interrogate | UserDefinedControl)
+ }
+
+ public enum ServiceBootFlag
+ {
+ Start = 0x00000000,
+ SystemStart = 0x00000001,
+ AutoStart = 0x00000002,
+ DemandStart = 0x00000003,
+ Disabled = 0x00000004
+ }
+
+ public enum ServiceControl
+ {
+ Stop = 0x00000001,
+ Pause = 0x00000002,
+ Continue = 0x00000003,
+ Interrogate = 0x00000004,
+ Shutdown = 0x00000005,
+ ParamChange = 0x00000006,
+ NetBindAdd = 0x00000007,
+ NetBindRemove = 0x00000008,
+ NetBindEnable = 0x00000009,
+ NetBindDisable = 0x0000000A
+ }
+
+ public enum ServiceError
+ {
+ Ignore = 0x00000000,
+ Normal = 0x00000001,
+ Severe = 0x00000002,
+ Critical = 0x00000003
+ }
+
+ public enum ServiceAction
+ {
+ None = 0,
+ Restart = 1,
+ Reboot = 2,
+ RunCommand = 3
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/AccessRules.cs b/tools/windows-installer/NoPortsInstaller/AccessRules.cs
new file mode 100644
index 000000000..56e53ff8d
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/AccessRules.cs
@@ -0,0 +1,61 @@
+namespace NoPortsInstaller
+{
+ public class AccessEntry : IAccessEntry
+ {
+ public string atSign { get; set; }
+ public AccessType type { get; set; }
+
+ AccessEntry()
+ {
+ atSign = "";
+ type = AccessType.Manager;
+ }
+ }
+
+ public class AccessRules : IAccessRules
+ {
+ public List Entries { get; set; }
+
+ public List Managers
+ {
+ get { return Entries.FindAll(entry => IsManager((AccessEntry)entry)); }
+ }
+
+ public IAccessEntry? Policy
+ {
+ get { return Entries.Find(entry => IsPolicy((AccessEntry)entry)); }
+ }
+
+ public bool IsValid
+ {
+ get { return Entries.Count > 0; }
+ }
+
+ AccessRules()
+ {
+ Entries = new();
+ }
+
+ public void SetEntryType(string atSign, AccessType type)
+ {
+ // Unset the current policy atSign if it exists (there can only be one)
+ if (type == AccessType.Policy && Policy != null)
+ {
+ InstallLogger.Log($"Unsetting current policy atSign: {Policy.atSign}");
+ Policy.type = AccessType.Manager;
+ }
+ InstallLogger.Log($"Setting policy atSign: {atSign}");
+ Entries.Find(entry => entry.atSign == atSign)!.type = type;
+ }
+
+ private static bool IsManager(AccessEntry entry)
+ {
+ return entry.type == AccessType.Manager;
+ }
+
+ private static bool IsPolicy(AccessEntry entry)
+ {
+ return entry.type == AccessType.Policy;
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/ActivateController.cs b/tools/windows-installer/NoPortsInstaller/ActivateController.cs
new file mode 100644
index 000000000..332fa7171
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/ActivateController.cs
@@ -0,0 +1,248 @@
+using System.Diagnostics;
+using System.IO;
+using System.Text.Json.Nodes;
+
+namespace NoPortsInstaller
+{
+ class ActivateController
+ {
+ private static readonly Controller _controller = App.ControllerInstance;
+
+ ///
+ /// Run a command with the at_activate.exe, if you want the return codes that comes in stdErr, use returnStdErr = true
+ ///
+ ///
+ ///
+ /// returns the values that come in stdErr if the errorCode is not 0 (an error exit)
+ ///
+ /// stdOut of the command
+ ///
+ private static string RunCommand(string args, bool returnStdErr = false)
+ {
+ string stdout = "";
+ string stderr = "";
+ int exitCode;
+ using (var process = new Process())
+ {
+ var startInfo = process.StartInfo;
+
+ startInfo.FileName = Path.Combine(_controller.InstallDirectory, "at_activate.exe");
+ startInfo.UseShellExecute = false;
+ startInfo.RedirectStandardOutput = true;
+ startInfo.RedirectStandardInput = true;
+ startInfo.RedirectStandardError = true;
+ startInfo.CreateNoWindow = true;
+ startInfo.Arguments = args;
+
+ try
+ {
+ process.Start();
+ process.WaitForExit();
+ stdout = process.StandardOutput.ReadToEnd();
+ stderr = process.StandardError.ReadToEnd();
+ exitCode = process.ExitCode;
+ }
+ catch(Exception ex)
+ {
+ exitCode = 1;
+ _controller.LoadError(ex);
+ }
+ }
+
+ if (exitCode != 0 && !returnStdErr)
+ {
+ throw new System.Exception(string.Join("\n", stderr));
+ }
+ if (!string.IsNullOrEmpty(stderr) && returnStdErr)
+ {
+ return stderr;
+ }
+ return stdout;
+
+ }
+
+ public static void Approve(string enrollmentId)
+ {
+ var args = $"approve -a \"{_controller.DeviceAtsign}\" -i {enrollmentId}";
+ try
+ {
+ RunCommand(args);
+ }
+ catch(Exception ex)
+ {
+ _controller.LoadError(ex);
+ }
+ }
+
+ public static void Deny(string enrollmentId)
+ {
+ var args = $"deny -a \"{_controller.DeviceAtsign}\" -i {enrollmentId}";
+ try
+ {
+ RunCommand(args);
+ }
+ catch(Exception ex)
+ {
+ _controller.LoadError(ex);
+ }
+ }
+
+ public static string GenerateOTP()
+ {
+ var args = $"otp -a \"{_controller.DeviceAtsign}\"";
+ try
+ {
+ return RunCommand(args);
+ }
+ catch(Exception ex)
+ {
+ _controller.LoadError(ex);
+ return "";
+ }
+ }
+
+ public static AtsignStatus Status(string atsign)
+ {
+ var args = $"status -a \"{atsign}\"";
+ string response = "";
+ try
+ {
+ response = RunCommand(args, true);
+ }
+ catch (Exception ex)
+ {
+ _controller.LoadError(ex);
+ }
+ var returnString = response.Split(":").ToList();
+ if(returnString.Count < 1)
+ {
+ // Most likely at_activate binary is missing if the output was empty
+ _controller.LoadError(new Exception("No output from at_activate status"));
+ }
+ var exitCode = returnString[0].Split(" ")[1];
+
+ if (exitCode == "0")
+ {
+ return AtsignStatus.Activated;
+ }
+ if (exitCode == "1")
+ {
+ return AtsignStatus.NotActivated;
+ }
+ return AtsignStatus.DNE;
+
+ }
+
+ private static readonly string AppName = "noports_win";
+ private static readonly List Namespaces = ["sshnp: rw, sshrvd: rw", "sshrvd: rw, sshnp: rw"];
+
+ public static bool Enroll(string otp)
+ {
+ string devName = _controller.DeviceName;
+ var args =
+ $"enroll -a \"{_controller.DeviceAtsign}\" -s \"{otp}\" -p \"{AppName}\" -d \"{devName}\" -n \"sshnp:rw,sshrvd:rw\" -k \"{Path.Combine(_controller.AtsignKeysDirectory, _controller.DeviceAtsign + "_key.atKeys")}\"";
+ string response = "";
+ try
+ {
+ response = RunCommand(args);
+ }
+ catch(Exception ex)
+ {
+ _controller.LoadError(ex);
+ }
+
+ if (response.Contains("[Success]"))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static List ListEnrollments()
+ {
+ var args = $"list -a \"{_controller.DeviceAtsign}\" -s pending";
+ var response = "";
+ try
+ {
+ response = RunCommand(args);
+ }
+ catch(Exception ex)
+ {
+ _controller.LoadError(ex);
+ }
+ List strings = response.Split("\n").ToList();
+ List lines = [];
+ for (int i = 2; i < strings.Count; i++)
+ {
+ var parts = strings[i].Trim().Split().ToList();
+ if (parts.Count < 4) continue;
+ parts.RemoveAll(x => x == "");
+
+ EnrollmentRecord record = new(parts[0], parts[3]);
+ lines.Add(record);
+ }
+ return lines;
+ }
+
+ public static bool CheckIfMPKAM(string atsign)
+ {
+ var dir = Path.Combine(_controller.AtsignKeysDirectory, atsign + "_key.atKeys");
+ if (File.Exists(dir))
+ {
+ string enrollmentId = "";
+ var fileContent = File.ReadAllText(dir);
+ if (fileContent.Contains("enrollmentId"))
+ {
+ try
+ {
+ var json = JsonObject.Parse(fileContent);
+ if (json != null) enrollmentId = (String?)json["enrollmentId"] ?? "";
+ }
+ catch
+ {
+ throw new Exception("Failed to parse the atKeys file json");
+ }
+ var args = $"list -a \"{atsign}\" -s \"approved\"";
+ var response = RunCommand(args);
+
+ List strings = response.Split("\n").ToList();
+ int count = strings.Count;
+ for (int i = 2; i <= count; i++)
+ {
+ if (strings[i].Contains(enrollmentId))
+ {
+ var braceStart = strings[i].IndexOf("{");
+ var braceEnd = strings[i].IndexOf("}");
+ if (braceStart != -1 && braceEnd != -1)
+ {
+ var permissions = strings[i].Substring(braceStart, braceEnd - braceStart);
+ return permissions.Contains("__manage: rw") && permissions.Contains("*: rw");
+ }
+ }
+ }
+
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ throw new Exception("Failed to locate atKeys file");
+ }
+ }
+
+ public class EnrollmentRecord(string Id, string DeviceName)
+ {
+ public string Id { get; set; } = Id;
+ public string DeviceName { get; set; } = DeviceName;
+ }
+
+ public enum AtsignStatus
+ {
+ DNE,
+ NotActivated,
+ Activated
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/App.xaml b/tools/windows-installer/NoPortsInstaller/App.xaml
new file mode 100644
index 000000000..29eeceeb0
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/App.xaml.cs b/tools/windows-installer/NoPortsInstaller/App.xaml.cs
new file mode 100644
index 000000000..d50bbfd07
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/App.xaml.cs
@@ -0,0 +1,27 @@
+using NoPortsInstaller.Pages;
+using System.Windows;
+
+namespace NoPortsInstaller;
+
+///
+/// Interaction logic for App.xaml
+///
+public partial class App : Application
+{
+ public static Controller ControllerInstance { get; set; }
+ public App()
+ {
+ ControllerInstance = new Controller();
+ }
+
+ public void OnStartup(object sender, StartupEventArgs e)
+ {
+ MainWindow mainWindow = new();
+ mainWindow.Show();
+ if (e.Args.Length == 1 && e.Args[0] == "u")
+ {
+ ControllerInstance.LoadPages(InstallType.Uninstall);
+ }
+ }
+}
+
diff --git a/tools/windows-installer/NoPortsInstaller/AssemblyInfo.cs b/tools/windows-installer/NoPortsInstaller/AssemblyInfo.cs
new file mode 100644
index 000000000..fbfbb0bc9
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/AssemblyInfo.cs
@@ -0,0 +1,11 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
diff --git a/tools/windows-installer/NoPortsInstaller/Assets/NoPortsDark.png b/tools/windows-installer/NoPortsInstaller/Assets/NoPortsDark.png
new file mode 100644
index 000000000..2a964eb99
Binary files /dev/null and b/tools/windows-installer/NoPortsInstaller/Assets/NoPortsDark.png differ
diff --git a/tools/windows-installer/NoPortsInstaller/Assets/noports-stacked-dark.ico b/tools/windows-installer/NoPortsInstaller/Assets/noports-stacked-dark.ico
new file mode 100644
index 000000000..2aeba216a
Binary files /dev/null and b/tools/windows-installer/NoPortsInstaller/Assets/noports-stacked-dark.ico differ
diff --git a/tools/windows-installer/NoPortsInstaller/Controller.cs b/tools/windows-installer/NoPortsInstaller/Controller.cs
new file mode 100644
index 000000000..fa0597bc2
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Controller.cs
@@ -0,0 +1,766 @@
+using Microsoft.Win32;
+using NoPortsInstaller.Pages;
+using NoPortsInstaller.Pages.Activate;
+using NoPortsInstaller.Pages.Install;
+using NoPortsInstaller.Pages.Update;
+using System.Diagnostics;
+using System.IO;
+using System.Security.AccessControl;
+using System.Windows;
+using System.Windows.Controls;
+using static NoPortsInstaller.ActivateController;
+
+namespace NoPortsInstaller
+{
+ public class Controller
+ {
+ public string InstallDirectory { get; set; }
+ public InstallType InstallType { get; set; }
+ public string ClientAtsign { get; set; }
+ public string DeviceAtsign { get; set; }
+ public string DeviceName { get; set; }
+ public string RegionAtsign { get; set; }
+ public string AdditionalArgs { get; set; }
+ public string AccessGroup { get; set; }
+ public bool IsInstalled
+ {
+ get { return Directory.Exists(InstallDirectory); }
+ set { }
+ }
+ public bool IsActivateInstalled
+ {
+ get { return Directory.Exists(InstallDirectory) && File.Exists(InstallDirectory + "/at_activate.exe"); }
+ set { }
+ }
+ public List Pages { get; set; }
+ private int index = 0;
+ private readonly string serviceDirectory =
+ Environment.ExpandEnvironmentVariables("%systemroot%")
+ + @"\ServiceProfiles\LocalService\";
+ public Window? Window { get; set; }
+ public AccessRules AccessRules { get; set; }
+
+ public string AtsignKeysDirectory
+ {
+ get
+ {
+ return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ @".atsign\keys"
+ );
+ }
+ }
+
+ public Controller()
+ {
+ InstallDirectory = "C:\\Program Files\\NoPorts";
+ InstallType = InstallType.Home;
+ ClientAtsign = "";
+ DeviceAtsign = "";
+ DeviceName = "";
+ RegionAtsign = "";
+ AdditionalArgs = "";
+ Pages = [];
+ IsInstalled = false;
+ AccessGroup = "Users";
+ LogEnvironment();
+ }
+
+ ///
+ /// Installs NoPorts depending on the InstallType.
+ ///
+ ///
+ ///
+ public async Task Install(ProgressBar progress, Label status )
+ {
+ try
+ {
+ status.Content = "Creating directories...";
+ CreateDirectories(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
+ await UpdateProgressBar(progress, 25);
+ status.Content = "Installing NoPorts...";
+ await MoveResources();
+ VerifyInstall();
+ status.Content = "Updating Trusted Certificates...";
+ await UpdateProgressBar(progress, 50);
+ UpdateTrustedCerts();
+ await UpdateProgressBar(progress, 75);
+ status.Content = "Creating Registries NoPorts...";
+ CreateRegistryKeys();
+ await UpdateProgressBar(progress, 100);
+ InstallLogger.Log($"Installation Complete");
+ if (InstallType.Equals(InstallType.Client))
+ {
+ Pages.Add(new FinishInstall());
+ }
+ if (InstallType.Equals(InstallType.Device))
+ {
+ EnrollDevice();
+ }
+
+ }
+ catch (Exception ex)
+ {
+ await Cleanup();
+ LoadError(ex);
+ }
+ }
+
+ public async Task InstallService(ProgressBar progress, Label status)
+ {
+ try
+ {
+ await UpdateProgressBar(progress, 25);
+ status.Content = "Setting up NoPorts Service...";
+ CopyIntoServiceAccount();
+ await SetupService(status);
+ await UpdateProgressBar(progress, 100);
+ NextPage();
+ }
+ catch (Exception ex)
+ {
+ await Cleanup();
+ LoadError(ex);
+ }
+ }
+
+ ///
+ /// Uninstalls NoPorts, including the service.
+ ///
+ ///
+ public async Task Uninstall(ProgressBar progress)
+ {
+ try
+ {
+ RegistryKey? registryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\NoPorts");
+ object? binPath = null;
+ if (registryKey != null)
+ {
+ binPath = registryKey.GetValue("BinPath");
+ Registry.LocalMachine.DeleteSubKey(@"SOFTWARE\NoPorts");
+ Registry.LocalMachine.DeleteSubKey(
+ @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NoPorts"
+ );
+ }
+
+ await ServiceController.TryUninstall("sshnpd");
+
+ if (binPath != null)
+ {
+ DirectoryInfo di = new(binPath.ToString()!);
+ foreach (FileInfo file in di.GetFiles())
+ {
+ file.Delete();
+ }
+ foreach (DirectoryInfo dir in di.GetDirectories())
+ {
+ dir.Delete(true);
+ }
+ di.Delete();
+ }
+ await UpdateProgressBar(progress, 100);
+ }
+ catch (Exception ex)
+ {
+ LoadError(ex);
+ }
+ }
+
+ ///
+ /// will change
+ ///
+ public void EnrollDevice()
+ {
+ if (!KeysInstalled())
+ {
+ if (ActivateController.Status(DeviceAtsign) != AtsignStatus.Activated)
+ {
+ throw new Exception("Keys not found locally and on registrar. Please onboard first.");
+ }
+
+ InstallLogger.Log("Starting enrollment process before continuing to install...");
+ Pages.Add(new Enroll());
+ }
+ else
+ {
+ InstallLogger.Log("Keys found. Continuing to service install...");
+ Pages.Add(new InstallService());
+ Pages.Add(new FinishInstall());
+ }
+ }
+
+ public void UpdateConfigRegistry()
+ {
+ try
+ {
+ RegistryKey? registryKey = Registry.LocalMachine.OpenSubKey(@"Software\NoPorts");
+ var args = $"-a {DeviceAtsign} -m {ClientAtsign} -d {DeviceName} -sv";
+ if (registryKey != null)
+ {
+ if (AdditionalArgs != "")
+ {
+ args += AdditionalArgs;
+ }
+ registryKey.SetValue("DeviceArgs", args);
+ registryKey.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ LoadError(ex);
+ }
+ }
+
+ private void CreateDirectories(string userHome)
+ {
+ DirectorySecurity securityRules = new();
+ DirectoryInfo di;
+ securityRules.AddAccessRule(
+ new FileSystemAccessRule(AccessGroup, FileSystemRights.ExecuteFile, AccessControlType.Allow)
+ );
+ List directories =
+ [
+ Path.Combine(userHome, ".sshnp"),
+ Path.Combine(userHome, ".atsign"),
+ Path.Combine(userHome, ".atsign", "keys"),
+ InstallDirectory,
+ ];
+
+ if (InstallType.Equals(InstallType.Device))
+ {
+ directories.Add(Path.Combine(userHome, ".ssh"));
+ }
+
+ foreach (string dir in directories)
+ {
+ if (!Directory.Exists(dir))
+ {
+ InstallLogger.Log($"Creating directory: {dir}");
+ Directory.CreateDirectory(dir);
+ di = new DirectoryInfo(dir);
+ di.SetAccessControl(securityRules);
+ }
+ else
+ {
+ InstallLogger.Log($"Directory already exists, skipping: {dir}");
+ }
+ }
+ if (InstallType.Equals(InstallType.Device))
+ {
+ var authkeys = Path.Combine(userHome, ".ssh", "authorized_keys");
+ InstallLogger.Log($"Creating authorized_keys file: {authkeys}");
+ if (!File.Exists(authkeys))
+ {
+ FileInfo fi = new(authkeys);
+ fi.Create().Close();
+ }
+ }
+ }
+
+ private void CopyIntoServiceAccount()
+ {
+ string sourceFile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ string[] sources =
+ [
+ Path.Combine(sourceFile, ".ssh", "authorized_keys"),
+ Path.Combine(sourceFile, ".atsign", "keys", DeviceAtsign + "_key.atKeys"),
+ ];
+ InstallLogger.Log($"Creating Directories in LocalService Account: {serviceDirectory}");
+ CreateDirectories(serviceDirectory);
+ string[] destinations =
+ [
+ Path.Combine(serviceDirectory, ".ssh", "authorized_keys"),
+ Path.Combine(serviceDirectory, ".atsign", "keys", DeviceAtsign + "_key.atKeys"),
+ ];
+ for (int i = 0; i < sources.Length; i++)
+ {
+ try
+ {
+ if (File.Exists(sources[i]))
+ {
+ InstallLogger.Log($"Copying {sources[i]} to {destinations[i]}");
+ File.Copy(sources[i], destinations[i], true);
+ }
+ }
+ catch
+ {
+ throw new FileNotFoundException(
+ "No keys found. Use at_activate to onboard keys or enroll/approve this device."
+ );
+ }
+ }
+ }
+
+ private async Task MoveResources()
+ {
+ Dictionary resources =
+ new()
+ {
+ { Properties.Resources.at_activate, "at_activate.exe" },
+ };
+
+ if (InstallType.Equals(InstallType.Device) || InstallType.Equals(InstallType.Client))
+ {
+ resources.Add(Properties.Resources.srv, "srv.exe");
+ }
+ if (InstallType.Equals(InstallType.Device))
+ {
+ await ServiceController.TryUninstall("sshnpd");
+ resources.Add(Properties.Resources.sshnpd, "sshnpd.exe");
+ resources.Add(Properties.Resources.SshnpdService, "SshnpdService.exe");
+ }
+ else if (InstallType.Equals(InstallType.Client))
+ {
+ resources.Add(Properties.Resources.sshnp, "sshnp.exe");
+ resources.Add(Properties.Resources.npt, "npt.exe");
+ }
+
+ foreach (var resource in resources)
+ {
+ string path = Path.Combine(InstallDirectory, resource.Value);
+ if (Directory.Exists(InstallDirectory))
+ {
+ InstallLogger.Log($"placing resource: {resource.Value} into {path}");
+ await Task.Run(() => File.WriteAllBytes(path, resource.Key));
+ }
+ else
+ {
+ throw new Exception("Install directory does not exist.");
+ }
+ }
+ InstallLogger.Log("Adding NoPorts to PATH...");
+ var newValue =
+ Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine)
+ + ";"
+ + InstallDirectory;
+ Environment.SetEnvironmentVariable("PATH", newValue, EnvironmentVariableTarget.Machine);
+ }
+
+ private async Task SetupService(Label status)
+ {
+ if (!EventLog.SourceExists("NoPorts"))
+ {
+ InstallLogger.Log("Makign Event Logger for NoPorts");
+ EventLog.CreateEventSource("NoPorts", "NoPorts");
+ }
+
+ try
+ {
+ InstallLogger.Log("Installing sshnpd service...");
+ status.Content = "Installing sshnpd service...";
+ await Task.Run(
+ () =>
+ ServiceController.InstallAndStart(
+ "sshnpd",
+ "sshnpd",
+ InstallDirectory + @"\SshnpdService.exe"
+ )
+ );
+ InstallLogger.Log($"sshnpd for {DeviceAtsign} at {DeviceName} is now running.");
+ InstallLogger.Log("Setting service options for sshnpd...");
+ status.Content = "Configuring the Windows Service...";
+ await Task.Run(() => ServiceController.SetRecoveryOptions("sshnpd"));
+ Process.Start("sc", "description sshnpd NoPorts-SSH-Daemon");
+ await Task.Run(
+ () =>
+ ServiceController.CreateUninstaller(
+ Path.Combine(
+ Path.GetDirectoryName(Environment.ProcessPath)!,
+ "NoPortsInstaller.exe u"
+ )
+ )
+ );
+ }
+ catch
+ {
+ throw;
+ }
+ return;
+ }
+
+ private static void UpdateTrustedCerts()
+ {
+ var tempPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ @"Temp\trusted-certs"
+ );
+ if (!Directory.Exists(tempPath))
+ {
+ Directory.CreateDirectory(tempPath);
+ }
+ tempPath += @"\roots.sst";
+ InstallLogger.Log("Generating certificates...");
+ ProcessStartInfo startInfo =
+ new()
+ {
+ FileName = "Certutil.exe",
+ Arguments = $"-generateSSTFromWU {tempPath}",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+
+ using (Process? process = Process.Start(startInfo))
+ {
+ process?.WaitForExit();
+ }
+ InstallLogger.Log("Importing certificates...");
+ startInfo = new ProcessStartInfo
+ {
+ FileName = "powershell.exe",
+ Verb = "runas",
+ Arguments =
+ $"-WindowStyle Hidden -Command \"(Get-ChildItem '{tempPath}') | Import-Certificate -CertStoreLocation Cert:\\LocalMachine\\Root\"",
+ UseShellExecute = true,
+ CreateNoWindow = true,
+ };
+
+ using (Process? process = Process.Start(startInfo))
+ {
+ process?.WaitForExit();
+ }
+ }
+
+ private void CreateRegistryKeys()
+ {
+ InstallLogger.Log("Creating registry keys...");
+ RegistryKey registryKey = Registry.LocalMachine.CreateSubKey(@"Software\NoPorts");
+ registryKey.SetValue("BinPath", InstallDirectory);
+ if (InstallType.Equals(InstallType.Device))
+ {
+ var args = $"-a {DeviceAtsign} -m {ClientAtsign} -d {DeviceName}";
+ if (AdditionalArgs != "")
+ {
+ args += AdditionalArgs;
+ }
+
+ args += " -v";
+ registryKey.SetValue("DeviceArgs", args);
+ }
+ registryKey.Close();
+ }
+
+ private static Task Cleanup()
+ {
+ return Task.Run(() =>
+ {
+ try
+ {
+ Directory.Delete(
+ Path.Combine(
+ Environment.GetFolderPath(
+ Environment.SpecialFolder.LocalApplicationData
+ ),
+ @"Temp\NoPorts"
+ ),
+ true
+ );
+ RegistryKey? registryKey = Registry.LocalMachine.OpenSubKey(
+ @"SOFTWARE\NoPorts"
+ );
+ if (registryKey != null)
+ {
+ registryKey.DeleteValue("BinPath");
+ registryKey.DeleteValue("DeviceArgs");
+ registryKey.Close();
+ }
+ }
+ catch { }
+ });
+ }
+
+ ///
+ /// Loads the appropriate pages based on the InstallType.
+ ///
+ public void LoadPages(InstallType type)
+ {
+ InstallType = type;
+ index = 0;
+ Pages.Clear();
+ switch (InstallType)
+ {
+ case InstallType.Home:
+ InstallLogger.DumpLog();
+ Pages.Add(new Setup());
+ break;
+ case InstallType.Uninstall:
+ Pages.Add(new UninstallPage());
+ break;
+ case InstallType.Client:
+ Pages.Add(new Setup());
+ Pages.Add(new ClientConfig());
+ break;
+ case InstallType.Device:
+ Pages.Add(new Setup());
+ Pages.Add(new DeviceConfig1());
+ Pages.Add(new DeviceConfig2());
+ break;
+ case InstallType.Enroll:
+ Pages.Add(new Setup());
+ if(!IsActivateInstalled)
+ {
+ Pages.Add(new Download());
+ }
+ Pages.Add(new PreEnroll());
+ break;
+ case InstallType.Onboard:
+ Pages.Add(new Setup());
+ if(!IsActivateInstalled)
+ {
+ Pages.Add(new Download());
+ }
+ Pages.Add(new Onboard());
+ Pages.Add(new FinishGeneratingKeys());
+ break;
+ case InstallType.Approve:
+ Pages.Add(new Setup());
+ if (!IsActivateInstalled)
+ {
+ Pages.Add(new Download());
+ }
+ Pages.Add(new AtsignApprove());
+ break;
+ }
+ if (Window != null)
+ {
+ Window.Content = Pages[index];
+ }
+ else
+ {
+ Pages.Add(new ServiceErrorPage("Window is null"));
+ }
+ }
+
+ public void LoadError(Exception ex)
+ {
+ index = 0;
+ Pages.Clear();
+ InstallLogger.Log($"Error message: {ex.Message}");
+ InstallLogger.Log($"Error trace: {ex.StackTrace}");
+ InstallLogger.DumpLog();
+ Pages.Add(new ServiceErrorPage(ex.Message));
+ NextPage();
+ }
+
+ ///
+ /// Moves to the next page in the Pages list.
+ ///
+ public void NextPage()
+ {
+ if (index < Pages.Count - 1)
+ {
+ index++;
+ }
+ Window!.Content = Pages[index];
+ }
+
+ ///
+ /// Moves to the previous page in the Pages list.
+ ///
+ public void PreviousPage()
+ {
+ if (index > 0)
+ {
+ index--;
+ }
+ Window!.Content = Pages[index];
+ }
+
+ ///
+ /// Updates the progress bar value asynchronously.
+ ///
+ /// The progress bar control.
+ /// The target value for the progress bar.
+ /// A task representing the asynchronous operation.
+ private async static Task UpdateProgressBar(ProgressBar pb, int value)
+ {
+ double start = pb.Value;
+ await Task.Run(() =>
+ {
+ for (double i = start; i <= value; i++)
+ {
+ pb.Dispatcher.Invoke(() => pb.Value = i);
+ Thread.Sleep(20);
+ }
+ });
+ }
+
+ ///
+ /// Populates the given ComboBox with available Atsigns.
+ ///
+ /// The ComboBox control to populate.
+ public void PopulateAtsigns(ComboBox box)
+ {
+ List files = [];
+ if (
+ Directory.Exists(
+ Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ @".atsign\keys"
+ )
+ )
+ )
+ {
+ files =
+ [
+ .. Directory.GetFiles(
+ Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ @".atsign\keys"
+ ),
+ "*.atKeys",
+ SearchOption.AllDirectories
+ ),
+ ];
+ }
+ foreach (var key in files)
+ {
+ ComboBoxItem item =
+ new() { Content = Path.GetFileNameWithoutExtension(key).Replace("_key", "") };
+ box.Items.Add(item);
+ }
+ }
+
+ ///
+ /// Normalizes the given Atsign by adding the '@' symbol if it is missing.
+ ///
+ /// The Atsign to normalize.
+ /// The normalized Atsign.
+ public string NormalizeAtsign(string atsign)
+ {
+ if (atsign.StartsWith('@'))
+ {
+ return atsign;
+ }
+ else
+ {
+ return "@" + atsign;
+ }
+ }
+
+ public string NormalizeArgs(string args)
+ {
+ string[] argArray = args.Split(args, ' ');
+ for (int i = 0; i < argArray.Length; i++)
+ {
+ if (argArray[i].Equals("--managers"))
+ {
+ argArray[i + 1] = NormalizeMultipleManagers(argArray[i + 1]);
+ }
+ if (argArray[i].Equals("--po"))
+ {
+ argArray[i + 1] = NormalizePermittedPorts(argArray[i + 1]);
+ }
+ }
+ return string.Join(' ', argArray);
+ }
+
+ private string NormalizeMultipleManagers(string atsigns)
+ {
+ string[] atsignArray = atsigns.Split(',');
+ for (int i = 0; i < atsignArray.Length; i++)
+ {
+ atsignArray[i] = NormalizeAtsign(atsignArray[i]);
+ }
+ return string.Join(',', atsignArray);
+ }
+
+ private static string NormalizePermittedPorts(string ports)
+ {
+ string[] portArray = ports.Split(',');
+ for (int i = 0; i < portArray.Length; i++)
+ {
+ if (!portArray[i].Contains(':'))
+ {
+ portArray[i] = "localhost:" + portArray[i];
+ }
+ }
+ return string.Join(',', portArray);
+ }
+
+ public string NormalizeDeviceName(string device)
+ {
+ if (device.Contains(' ') || device.Contains('-'))
+ {
+ return device.ToLower().Replace(" ", "_");
+ }
+ return device.ToLower();
+ }
+
+ public void VerifyInstall()
+ {
+ List dirs = ["at_activate.exe"];
+ if(InstallType == InstallType.Client || InstallType == InstallType.Device )
+ {
+ dirs.Add("srv.exe");
+ }
+ if (InstallType == InstallType.Client)
+ {
+ dirs.Add("sshnp.exe");
+ dirs.Add("npt.exe");
+ }
+ if (InstallType == InstallType.Device)
+ {
+ dirs.Add("sshnpd.exe");
+ dirs.Add("Sshnpd.exe");
+ }
+ foreach (var dir in dirs)
+ {
+ string sourceFilePath = Path.Combine(InstallDirectory, dir);
+ if (!File.Exists(sourceFilePath))
+ {
+ throw new Exception("File not found: " + sourceFilePath);
+ }
+ }
+ }
+
+ private bool KeysInstalled()
+ {
+ return File.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), @".atsign\keys", DeviceAtsign + "_key.atKeys"));
+ }
+
+ private static void LogEnvironment()
+ {
+ InstallLogger.Log("Environment Variables:");
+ InstallLogger.Log(
+ $"User Home: {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}"
+ );
+ InstallLogger.Log(
+ $"Program Files: {Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)}"
+ );
+ InstallLogger.Log(
+ $"Local App Data: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}"
+ );
+ InstallLogger.Log(
+ $"LocalService: {Environment.ExpandEnvironmentVariables("%systemroot%") + @"\ServiceProfiles\LocalService\"}"
+ );
+ try
+ {
+ Directory.CreateDirectory(
+ Environment.ExpandEnvironmentVariables("%systemroot%") + @"\ServiceProfiles\LocalService\" + @"\.atsign"
+ );
+ InstallLogger.Log("Created .atsign directory in LocalService.");
+ }
+ catch
+ {
+ InstallLogger.Log("Failed to create .atsign directory in LocalService Account.");
+ }
+
+ InstallLogger.Log("User Information:");
+ InstallLogger.Log($"Username: {Environment.UserName}");
+ InstallLogger.Log($"Domain: {Environment.UserDomainName}");
+
+ InstallLogger.Log("Operating System Information:");
+ InstallLogger.Log($"OS Version: {Environment.OSVersion}");
+ InstallLogger.Log($"Machine Name: {Environment.MachineName}");
+ InstallLogger.Log($"System Directory: {Environment.SystemDirectory}");
+ InstallLogger.Log($"Is 64-bit Operating System: {Environment.Is64BitOperatingSystem}");
+ InstallLogger.Log($"Is 64-bit Process: {Environment.Is64BitProcess}");
+
+ InstallLogger.Log("Process Information:");
+ InstallLogger.Log($"Process Path: {Environment.ProcessPath}");
+ InstallLogger.Log($"Process is run as Admin: {Environment.IsPrivilegedProcess}");
+ InstallLogger.Log($"Runtime Version:{Environment.Version}");
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/IAccessRules.cs b/tools/windows-installer/NoPortsInstaller/IAccessRules.cs
new file mode 100644
index 000000000..a8488c2df
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/IAccessRules.cs
@@ -0,0 +1,25 @@
+namespace NoPortsInstaller
+{
+ public enum AccessType
+ {
+ Manager,
+ Policy,
+ }
+
+ public interface IAccessEntry
+ {
+ string atSign { get; set; }
+ AccessType type { get; set; }
+ }
+
+ public interface IAccessRules
+ {
+ List Entries { get; set; }
+ List Managers { get; }
+ IAccessEntry? Policy { get; }
+
+ bool IsValid { get; }
+
+ void SetEntryType(string atSign, AccessType type);
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/InstallLogger.cs b/tools/windows-installer/NoPortsInstaller/InstallLogger.cs
new file mode 100644
index 000000000..6d61344f0
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/InstallLogger.cs
@@ -0,0 +1,33 @@
+using System.IO;
+
+namespace NoPortsInstaller
+{
+ internal class InstallLogger
+ {
+ private static readonly string LogDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "NoPortsInstaller");
+ private static readonly string LogFile = Path.Combine(LogDirectory, "install.log");
+ private static readonly List LogMessages = [];
+
+ public static void Log(string message)
+ {
+ LogMessages.Add($"{DateTime.Now} - {message}");
+ }
+
+ public static void DumpLog()
+ {
+ if (!Directory.Exists(LogDirectory))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(LogFile)!);
+ }
+ if (File.Exists(LogFile))
+ {
+ File.Delete(LogFile);
+ }
+ using StreamWriter stream = new(File.Create(LogFile));
+ foreach (string message in LogMessages)
+ {
+ stream.WriteLine(message);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/windows-installer/NoPortsInstaller/InstallType.cs b/tools/windows-installer/NoPortsInstaller/InstallType.cs
new file mode 100644
index 000000000..5a6ebe0fe
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/InstallType.cs
@@ -0,0 +1,11 @@
+public enum InstallType
+{
+ Home,
+ Device,
+ Client,
+ Onboard,
+ Enroll,
+ Approve,
+ Uninstall
+}
+
diff --git a/tools/windows-installer/NoPortsInstaller/NoPortsInstaller.csproj b/tools/windows-installer/NoPortsInstaller/NoPortsInstaller.csproj
new file mode 100644
index 000000000..ab64d8b62
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/NoPortsInstaller.csproj
@@ -0,0 +1,83 @@
+
+
+
+ WinExe
+ {FCF07AC8-119A-4DD0-A45D-3D765D138A85}
+ 0.1.0.0
+ net8.0-windows
+ enable
+ enable
+ true
+ embedded
+ win-x64
+ app.manifest
+ true
+ true
+ true
+ true
+ copyused
+ Assets\noports-stacked-dark.ico
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+ PreserveNewest
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/Approve.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Approve.xaml
new file mode 100644
index 000000000..3ba08a796
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Approve.xaml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/Approve.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Approve.xaml.cs
new file mode 100644
index 000000000..bb85bbdcf
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Approve.xaml.cs
@@ -0,0 +1,108 @@
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using static NoPortsInstaller.ActivateController;
+
+namespace NoPortsInstaller.Pages.Activate
+{
+
+ ///
+ /// Interaction logic for Approve.xaml
+ ///
+ public partial class Approve : Page
+ {
+ private readonly Controller _controller = App.ControllerInstance;
+ private string response = "";
+ IEnumerable enrollments = new List();
+ public Approve()
+ {
+ InitializeComponent();
+ Header.Content = $"Generate atKeys for {_controller.DeviceAtsign}";
+ FillEnrollmentRequests();
+ }
+
+ private void ApproveButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender != null && sender is Button)
+ {
+ Button button = (Button)sender;
+ EnrollmentRecord record = (EnrollmentRecord)button.CommandParameter;
+ RemoveEnrollment(record.Id);
+ ActivateController.Approve(record.Id);
+ FillEnrollmentRequests();
+ }
+ }
+
+ private void DenyButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender != null && sender is Button)
+ {
+ Button button = (Button)sender;
+ EnrollmentRecord record = (EnrollmentRecord)button.CommandParameter;
+ RemoveEnrollment(record.Id);
+ ActivateController.Deny(record.Id);
+ FillEnrollmentRequests();
+ }
+ }
+
+ private void RefreshButton_Click(object sender, RoutedEventArgs e)
+ {
+ FillEnrollmentRequests();
+ }
+
+ private void RemoveEnrollment(string Id)
+ {
+ enrollments = enrollments.Where(delegate (EnrollmentRecord enrollmentRecord)
+ {
+ return enrollmentRecord.Id != Id;
+ });
+ RedrawEnrollments();
+ }
+
+ private void RedrawEnrollments()
+ {
+ icEnrollments.ItemsSource = enrollments;
+ icEnrollments.UpdateLayout();
+ }
+
+ private void FillEnrollmentRequests()
+ {
+ try
+ {
+ enrollments = ActivateController.ListEnrollments();
+ }
+ catch (Exception e)
+ {
+ InstallLogger.Log("Failed to find any active Enrollments");
+ InstallLogger.Log(e.Message);
+ return;
+ }
+ RedrawEnrollments();
+ }
+
+ private void BackPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.PreviousPage();
+ }
+
+ private void Home_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Home);
+ }
+
+ private void NewOtpButton_Click(object sender, RoutedEventArgs e)
+ {
+ response = ActivateController.GenerateOTP();
+ char[] chars = response.ToCharArray();
+ if (chars.Length < 6) return;
+ OtpBox1.Text = chars[0].ToString();
+ OtpBox2.Text = chars[1].ToString();
+ OtpBox3.Text = chars[2].ToString();
+ OtpBox4.Text = chars[3].ToString();
+ OtpBox5.Text = chars[4].ToString();
+ OtpBox6.Text = chars[5].ToString();
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/AtsignApprove.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Activate/AtsignApprove.xaml
new file mode 100644
index 000000000..5d180ab20
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/AtsignApprove.xaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/AtsignApprove.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Activate/AtsignApprove.xaml.cs
new file mode 100644
index 000000000..b238d3d20
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/AtsignApprove.xaml.cs
@@ -0,0 +1,64 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Activate
+{
+ ///
+ /// Interaction logic for EnrollDevice.xaml
+ ///
+ public partial class AtsignApprove : Page
+ {
+ private readonly Controller _controller = App.ControllerInstance;
+ public AtsignApprove()
+ {
+ InitializeComponent();
+ }
+
+
+ private void BackPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Home);
+ }
+
+ private void AtsignCombo_Initialized(object sender, EventArgs e)
+ {
+ ComboBox comboBox = (ComboBox)sender;
+ _controller.PopulateAtsigns(comboBox);
+ }
+
+ private void Next_Click(object sender, RoutedEventArgs e)
+ {
+ if (AtsignCombo.Text != "")
+ {
+ var atsign = _controller.NormalizeAtsign(AtsignCombo.Text);
+ bool isMpkam = false;
+ try
+ {
+ isMpkam = ActivateController.CheckIfMPKAM(atsign);
+ }
+ catch (Exception ex)
+ {
+ _controller.LoadError(ex);
+ }
+
+ if (isMpkam)
+ {
+ _controller.DeviceAtsign = atsign;
+ _controller.Pages.Add(new Approve());
+ _controller.Pages.Add(new FinishGeneratingKeys());
+ _controller.NextPage();
+ }
+ else
+ {
+ AtsignResponseCheckText.Visibility = Visibility.Visible;
+ }
+ }
+ }
+
+ private void AtsignCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ AtsignResponseCheckText.Visibility = Visibility.Hidden;
+ }
+ }
+
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/Enroll.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Enroll.xaml
new file mode 100644
index 000000000..61c1cc9c2
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Enroll.xaml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/Enroll.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Enroll.xaml.cs
new file mode 100644
index 000000000..fc6c7e8e8
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Enroll.xaml.cs
@@ -0,0 +1,145 @@
+using NoPortsInstaller.Pages.Install;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Animation;
+
+namespace NoPortsInstaller.Pages.Activate
+{
+ ///
+ /// Interaction logic for EnrollDevice.xaml
+ ///
+ public partial class Enroll : Page
+ {
+ private readonly Controller _controller = App.ControllerInstance;
+ public Enroll()
+ {
+ InitializeComponent();
+ StartLoadingAnimation();
+ Header.Content = $"Generate atKeys for {_controller.DeviceAtsign}";
+ OTPLabel.Content = $"at__activate otp -a \"{_controller.DeviceAtsign}\"";
+ ApproveLabel.Content = $"at__activate approve -a \"{_controller.DeviceAtsign}\" -i [enrollmentId]";
+ }
+
+ // Event handler for TextChanged
+ private void OtpBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (sender is TextBox textBox)
+ {
+ if (textBox.Text.Length == 1)
+ {
+ // Move focus to the next TextBox.
+ switch (textBox.Name)
+ {
+ case "OtpBox1":
+ OtpBox2.Focus();
+ break;
+ case "OtpBox2":
+ OtpBox3.Focus();
+ break;
+ case "OtpBox3":
+ OtpBox4.Focus();
+ break;
+ case "OtpBox4":
+ OtpBox5.Focus();
+ break;
+ case "OtpBox5":
+ OtpBox6.Focus();
+ break;
+ case "OtpBox6":
+ break;
+ }
+ }
+
+ // Handle backspace (if the user tries to clear a box)
+ if (textBox.Text.Length == 0)
+ {
+ switch (textBox.Name)
+ {
+ case "OtpBox6":
+ OtpBox5.Focus();
+ break;
+ case "OtpBox5":
+ OtpBox4.Focus();
+ break;
+ case "OtpBox4":
+ OtpBox3.Focus();
+ break;
+ case "OtpBox3":
+ OtpBox2.Focus();
+ break;
+ case "OtpBox2":
+ OtpBox1.Focus();
+ break;
+ }
+ }
+
+ bool isOtpComplete = OtpBox1.Text.Length == 1 &&
+ OtpBox2.Text.Length == 1 &&
+ OtpBox3.Text.Length == 1 &&
+ OtpBox4.Text.Length == 1 &&
+ OtpBox5.Text.Length == 1 &&
+ OtpBox6.Text.Length == 1;
+
+ if (isOtpComplete)
+ {
+ Generate.IsEnabled = true;
+ }
+ else
+ {
+ Generate.IsEnabled = false;
+ }
+ }
+
+ }
+
+ private void BackPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.PreviousPage();
+ }
+
+ private async void Generate_Click(object sender, RoutedEventArgs e)
+ {
+ EnrollResponse.Content = "";
+ string otp = $"{OtpBox1.Text}{OtpBox2.Text}{OtpBox3.Text}{OtpBox4.Text}{OtpBox5.Text}{OtpBox6.Text}".ToUpper();
+ if (!_controller.InstallType.Equals(InstallType.Device))
+ {
+ _controller.DeviceName = _controller.NormalizeDeviceName($"client_{otp}");
+ }
+ Loading.Visibility = Visibility.Visible;
+ bool value = await Task.Run(() => ActivateController.Enroll(otp));
+ Loading.Visibility = Visibility.Hidden;
+ if (value)
+ {
+ if (_controller.InstallType.Equals(InstallType.Device))
+ {
+ _controller.Pages.Add(new InstallService());
+ _controller.Pages.Add(new FinishInstall());
+ }
+ _controller.NextPage();
+ }
+ else
+ {
+ EnrollResponse.Content = "Invalid OTP, Enrollment failed. Please make sure you have a valid otp and try again.";
+ }
+ }
+
+ private void StartLoadingAnimation()
+ {
+ // Create a DoubleAnimation for rotation
+ // Create a smooth DoubleAnimation for rotation
+ DoubleAnimation rotationAnimation = new()
+ {
+ From = 0,
+ To = 360,
+ Duration = new Duration(TimeSpan.FromSeconds(1)),
+ RepeatBehavior = RepeatBehavior.Forever,
+ EasingFunction = new SineEase { EasingMode = EasingMode.EaseInOut }
+ };
+
+ // Apply the animation to the RotateTransform
+ LoadingCircleTransform.BeginAnimation(System.Windows.Media.RotateTransform.AngleProperty, rotationAnimation);
+ }
+ }
+
+}
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/FinishGeneratingKeys.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Activate/FinishGeneratingKeys.xaml
new file mode 100644
index 000000000..a4c9c74aa
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/FinishGeneratingKeys.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/FinishGeneratingKeys.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Activate/FinishGeneratingKeys.xaml.cs
new file mode 100644
index 000000000..a027edff6
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/FinishGeneratingKeys.xaml.cs
@@ -0,0 +1,24 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Activate
+{
+ ///
+ /// Interaction logic for FinishGeneratingKeys.xaml
+ ///
+ public partial class FinishGeneratingKeys : Page
+ {
+ private readonly Controller _controller = App.ControllerInstance;
+ public FinishGeneratingKeys()
+ {
+ InitializeComponent();
+ }
+
+ private void HomeButton_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Home);
+ _controller.NextPage();
+ }
+
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/Onboard.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Onboard.xaml
new file mode 100644
index 000000000..eba097997
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Onboard.xaml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/Onboard.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Onboard.xaml.cs
new file mode 100644
index 000000000..c240395ad
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/Onboard.xaml.cs
@@ -0,0 +1,171 @@
+using System.Diagnostics;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using static NoPortsInstaller.ActivateController;
+
+namespace NoPortsInstaller.Pages.Activate
+{
+ ///
+ /// Interaction logic for Onboard.xaml
+ ///
+ public partial class Onboard : Page
+ {
+ private string atSign { get; set; }
+ private readonly Controller _controller = App.ControllerInstance;
+ private readonly Process at_activate = new();
+ public Onboard()
+ {
+ InitializeComponent();
+ at_activate.StartInfo.FileName = Path.Combine(_controller.InstallDirectory, "at_activate.exe");
+ at_activate.StartInfo.Arguments = "";
+ at_activate.StartInfo.UseShellExecute = false;
+ at_activate.StartInfo.RedirectStandardOutput = true;
+ at_activate.StartInfo.RedirectStandardInput = true;
+ at_activate.StartInfo.RedirectStandardError = true;
+ at_activate.StartInfo.CreateNoWindow = true;
+ atSign = "";
+ }
+ private void BackPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.PreviousPage();
+ _controller.LoadPages(InstallType.Home);
+ }
+ private void OtpBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (sender is TextBox textBox)
+ {
+ if (textBox.Text.Length == 1)
+ {
+ // Move focus to the next TextBox
+ switch (textBox.Name)
+ {
+ case "OtpBox1":
+ OtpBox2.Focus();
+ break;
+ case "OtpBox2":
+ OtpBox3.Focus();
+ break;
+ case "OtpBox3":
+ OtpBox4.Focus();
+ break;
+ }
+ }
+
+ // Handle backspace (if the user tries to clear a box)
+ if (textBox.Text.Length == 0)
+ {
+ switch (textBox.Name)
+ {
+ case "OtpBox4":
+ OtpBox3.Focus();
+ break;
+ case "OtpBox3":
+ OtpBox2.Focus();
+ break;
+ case "OtpBox2":
+ OtpBox1.Focus();
+ break;
+ }
+ }
+
+ if (OtpBox1.Text.Length == 1 && OtpBox2.Text.Length == 1 && OtpBox3.Text.Length == 1 && OtpBox4.Text.Length == 1)
+ {
+ Generate.IsEnabled = true;
+ }
+ else
+ {
+ Generate.IsEnabled = false;
+ }
+ }
+
+ }
+
+ private void Start_AtActivate(string atsign)
+ {
+ at_activate.StartInfo.Arguments = $"onboard -a {atsign}";
+ at_activate.Start();
+ while (!at_activate.HasExited && !IsProcessReady(at_activate))
+ {
+ Thread.Sleep(100); // Sleep for a short interval before checking again
+ }
+
+ if (at_activate.HasExited)
+ {
+ throw new IOException("Failed to start at_activate process.");
+ }
+ else
+ {
+ OtpInput.Visibility = Visibility.Visible;
+ }
+ }
+
+ private static bool IsProcessReady(Process process)
+ {
+ // Check if the process has started and is ready to receive input
+ return process.StandardInput.BaseStream.CanWrite;
+ }
+
+
+ private void Submit_Click(object sender, RoutedEventArgs e)
+ {
+ ActivateResponseText.Visibility = Visibility.Hidden;
+ ActivateResponseText.Content = "";
+ atSign = _controller.NormalizeAtsign(Atsign.Text);
+ var response = ActivateController.Status(atSign);
+ if (response == AtsignStatus.NotActivated)
+ {
+ ActivateResponseText.Content = "Check the email that was used to create the atsign \n \n Enter One Time Password (OTP):";
+ ActivateResponseText.Visibility = Visibility.Visible;
+ Start_AtActivate(atSign);
+ Submit.IsEnabled = false;
+ }
+ else if (response == AtsignStatus.Activated)
+ {
+ ActivateResponseText.Content = "This atsign is already activated.";
+ ActivateResponseText.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ ActivateResponseText.Content = "Make sure to enter an atSign you own and \n make sure to press 'Activate' on your dashboard.";
+ ActivateResponseText.Visibility = Visibility.Visible;
+ }
+ }
+
+ private void Generate_Click(object sender, RoutedEventArgs e)
+ {
+ string otp = $"{OtpBox1.Text}{OtpBox2.Text}{OtpBox3.Text}{OtpBox4.Text}".ToUpper();
+ if (!at_activate.HasExited)
+ {
+ InstallLogger.Log($"Entering OTP {otp}");
+ at_activate.StandardInput.WriteLine(otp);
+ at_activate.WaitForExit();
+
+ if (at_activate.ExitCode == 0)
+ {
+ _controller.NextPage();
+ }
+ else
+ {
+ InstallLogger.Log("Output from at_activate onboard (stdout):");
+ InstallLogger.Log(at_activate.StandardOutput.ToString() ?? "");
+ InstallLogger.Log("Output from at_activate onboard (stderr):");
+ InstallLogger.Log(at_activate.StandardError.ToString() ?? "");
+ InstallLogger.DumpLog();
+ ActivateResponseText.Content = "Invalid OTP, please try again.";
+ at_activate.Kill();
+ at_activate.Start();
+ }
+ }
+ }
+
+
+
+ private void Atsign_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ActivateResponseText.Visibility = Visibility.Hidden;
+ OtpInput.Visibility = Visibility.Hidden;
+ Submit.IsEnabled = true;
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/PreEnroll.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Activate/PreEnroll.xaml
new file mode 100644
index 000000000..542b89173
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/PreEnroll.xaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Activate/PreEnroll.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Activate/PreEnroll.xaml.cs
new file mode 100644
index 000000000..db07b8882
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Activate/PreEnroll.xaml.cs
@@ -0,0 +1,76 @@
+using System.Diagnostics;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using static NoPortsInstaller.ActivateController;
+
+namespace NoPortsInstaller.Pages.Activate
+{
+ ///
+ /// Interaction logic for PreEnroll.xaml
+ ///
+ public partial class PreEnroll : Page
+ {
+ private readonly Controller _controller = App.ControllerInstance;
+ private readonly Process at_activate = new();
+ public PreEnroll()
+ {
+ InitializeComponent();
+ at_activate.StartInfo.FileName = Path.Combine(_controller.InstallDirectory, "at_activate.exe");
+ at_activate.StartInfo.Arguments = $"onboard -a ";
+ at_activate.StartInfo.UseShellExecute = false;
+ at_activate.StartInfo.RedirectStandardOutput = true;
+ at_activate.StartInfo.RedirectStandardInput = true;
+ at_activate.StartInfo.RedirectStandardError = true;
+ at_activate.StartInfo.CreateNoWindow = true;
+ }
+
+ private void BackPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.PreviousPage();
+ _controller.LoadPages(InstallType.Home);
+ }
+ // Event handler for TextChanged
+
+
+ private static bool IsProcessReady(Process process)
+ {
+ // Check if the process has started and is ready to receive input
+ return process.StandardInput.BaseStream.CanWrite;
+ }
+
+
+ private void Next_Click(object sender, RoutedEventArgs e)
+ {
+ ActivateResponseText.Visibility = Visibility.Hidden;
+ ActivateResponseText.Content = "";
+
+ _controller.DeviceAtsign = _controller.NormalizeAtsign(Atsign.Text);
+ var response = ActivateController.Status(_controller.DeviceAtsign);
+ if (response != AtsignStatus.NotActivated && response != AtsignStatus.Activated)
+ {
+ ActivateResponseText.Content = "This atsign does not exist.";
+ ActivateResponseText.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ _controller.Pages.Add(new Enroll());
+ _controller.Pages.Add(new FinishGeneratingKeys());
+ _controller.NextPage();
+ }
+ }
+
+ private void Atsign_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ActivateResponseText.Visibility = Visibility.Hidden;
+ if (Atsign.Text.Length > 0)
+ {
+ Next.IsEnabled = true;
+ }
+ else
+ {
+ Next.IsEnabled = false;
+ }
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Client/ClientConfig.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Client/ClientConfig.xaml
new file mode 100644
index 000000000..bfbef6b2c
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Client/ClientConfig.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Client/ClientConfig.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Client/ClientConfig.xaml.cs
new file mode 100644
index 000000000..43396292a
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Client/ClientConfig.xaml.cs
@@ -0,0 +1,28 @@
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Install
+{
+ ///
+ /// Interaction logic for Page2.xaml
+ ///
+ public partial class ClientConfig : Page
+ {
+ private readonly Controller _controller;
+ public ClientConfig()
+ {
+ _controller = App.ControllerInstance;
+ InitializeComponent();
+ }
+
+ private void BackPageButton_Click(object sender, System.Windows.RoutedEventArgs e)
+ {
+ _controller.PreviousPage();
+ }
+
+ private void NextPageButton_Click(object sender, System.Windows.RoutedEventArgs e)
+ {
+ _controller.Pages.Add(new Download());
+ _controller.NextPage();
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig1.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig1.xaml
new file mode 100644
index 000000000..d0a4fba35
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig1.xaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig1.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig1.xaml.cs
new file mode 100644
index 000000000..4fabb7672
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig1.xaml.cs
@@ -0,0 +1,59 @@
+using System.IO;
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages
+{
+ public partial class DeviceConfig1 : Page
+ {
+ private readonly Controller _controller;
+ public DeviceConfig1()
+ {
+ _controller = App.ControllerInstance;
+ if (!Directory.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), @".atsign\keys")))
+ {
+ Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), @".atsign\keys"));
+ }
+ InitializeComponent();
+ }
+
+ private void BackPageButton_Click(object sender, System.Windows.RoutedEventArgs e)
+ {
+ _controller.PreviousPage();
+ }
+
+ private void NextPageButton_Click(object sender, System.Windows.RoutedEventArgs e)
+ {
+ _controller.DeviceAtsign = _controller.NormalizeAtsign(DeviceCombo.Text);
+ _controller.ClientAtsign = _controller.NormalizeAtsign(ClientCombo.Text);
+ _controller.DeviceName = _controller.NormalizeDeviceName(DeviceNameText.Text);
+ _controller.NextPage();
+ }
+
+ private void ClientCombo_Initialized(object sender, EventArgs e)
+ {
+ ComboBox comboBox = (ComboBox)sender;
+ _controller.PopulateAtsigns(comboBox);
+ }
+
+ private void DeviceCombo_Initialized(object sender, EventArgs e)
+ {
+ ComboBox comboBox = (ComboBox)sender;
+ _controller.PopulateAtsigns(comboBox);
+ }
+
+
+ private void ValidateInputs(object sender, dynamic e)
+ {
+ if (ClientCombo.Text == "" || DeviceCombo.Text == "" || DeviceNameText.Text == "")
+ {
+ NextPageButton.IsEnabled = false;
+ }
+ else
+ {
+ NextPageButton.IsEnabled = true;
+ }
+ }
+
+ }
+}
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig2.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig2.xaml
new file mode 100644
index 000000000..212b1410c
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig2.xaml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig2.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig2.xaml.cs
new file mode 100644
index 000000000..24352af9e
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Device/DeviceConfig2.xaml.cs
@@ -0,0 +1,28 @@
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Install
+{
+ ///
+ /// Interaction logic for Page2.xaml
+ ///
+ public partial class DeviceConfig2 : Page
+ {
+ private readonly Controller _controller = App.ControllerInstance;
+ public DeviceConfig2()
+ {
+ InitializeComponent();
+ }
+
+ private void BackPageButton_Click(object sender, System.Windows.RoutedEventArgs e)
+ {
+ _controller.PreviousPage();
+ }
+
+ private void NextPageButton_Click(object sender, System.Windows.RoutedEventArgs e)
+ {
+ _controller.AdditionalArgs = _controller.NormalizeArgs(AdditionalArgs.Text);
+ _controller.Pages.Add(new Download());
+ _controller.NextPage();
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Device/InstallService.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Device/InstallService.xaml
new file mode 100644
index 000000000..c1c416678
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Device/InstallService.xaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Device/InstallService.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Device/InstallService.xaml.cs
new file mode 100644
index 000000000..68d584160
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Device/InstallService.xaml.cs
@@ -0,0 +1,18 @@
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Install
+{
+ ///
+ /// Interaction logic for Page1.xaml
+ ///
+ public partial class InstallService : Page
+ {
+ private readonly Controller _controller;
+ public InstallService()
+ {
+ InitializeComponent();
+ _controller = App.ControllerInstance;
+ _ = _controller.InstallService(InstallProgress, Status);
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/MainWindow.xaml b/tools/windows-installer/NoPortsInstaller/Pages/MainWindow.xaml
new file mode 100644
index 000000000..9aef0c184
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/MainWindow.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/MainWindow.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/MainWindow.xaml.cs
new file mode 100644
index 000000000..55938a107
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/MainWindow.xaml.cs
@@ -0,0 +1,18 @@
+using System.Windows;
+
+namespace NoPortsInstaller.Pages;
+
+///
+/// Interaction logic for MainWindow.xaml
+///
+public partial class MainWindow : Window
+{
+ private readonly Controller _controller;
+ public MainWindow()
+ {
+ InitializeComponent();
+ _controller = App.ControllerInstance;
+ _controller.Window = this;
+ _controller.LoadPages(InstallType.Home);
+ }
+}
\ No newline at end of file
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/ServiceErrorPage.xaml b/tools/windows-installer/NoPortsInstaller/Pages/ServiceErrorPage.xaml
new file mode 100644
index 000000000..97b05bb9f
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/ServiceErrorPage.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/ServiceErrorPage.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/ServiceErrorPage.xaml.cs
new file mode 100644
index 000000000..9b13539f6
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/ServiceErrorPage.xaml.cs
@@ -0,0 +1,22 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages
+{
+ ///
+ /// Interaction logic for ServiceErrorPage.xaml
+ ///
+ public partial class ServiceErrorPage : Page
+ {
+ public ServiceErrorPage(string errorMsg = "")
+ {
+ InitializeComponent();
+ ErrorBox.Text = errorMsg;
+ }
+
+ private void NextPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ Application.Current.Shutdown();
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Setup.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Setup.xaml
new file mode 100644
index 000000000..34554cb28
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Setup.xaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Setup.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Setup.xaml.cs
new file mode 100644
index 000000000..dd2262b77
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Setup.xaml.cs
@@ -0,0 +1,72 @@
+using Microsoft.Win32;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages
+{
+ ///
+ /// Interaction logic for Setup.xaml
+ ///
+ public partial class Setup : Page
+ {
+ private readonly Controller _controller;
+
+ public Setup()
+ {
+ InitializeComponent();
+ _controller = App.ControllerInstance;
+ AccessGroupText.Text = _controller.AccessGroup;
+ }
+
+ private void OpenDialogButton_Click(object sender, RoutedEventArgs e)
+ {
+ OpenFolderDialog dialog = new();
+ dialog.InitialDirectory = _controller.InstallDirectory;
+
+ // Process save file dialog box results
+ if (dialog.ShowDialog() == true)
+ {
+ _controller.InstallDirectory = dialog.FolderName + "\\NoPorts";
+ }
+ Directory.Text = _controller.InstallDirectory;
+ }
+
+ private void DeviceInstall_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Device);
+ _controller.NextPage();
+ }
+
+ private void ClientInstall_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Client);
+ _controller.NextPage();
+ }
+
+ private void Onboard_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Onboard);
+ _controller.NextPage();
+ }
+
+ private void Enroll_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Enroll);
+ _controller.NextPage();
+ }
+
+ private void Otp_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Approve);
+ _controller.NextPage();
+ }
+
+ private void AccessGroupText_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ InstallLogger.Log(AccessGroupText.Text);
+ InstallLogger.DumpLog();
+ _controller.AccessGroup = AccessGroupText.Text;
+ }
+ }
+}
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Shared/Download.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Shared/Download.xaml
new file mode 100644
index 000000000..404d21e3b
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Shared/Download.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Shared/Download.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Shared/Download.xaml.cs
new file mode 100644
index 000000000..cd98d26f4
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Shared/Download.xaml.cs
@@ -0,0 +1,34 @@
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Install
+{
+ ///
+ /// Interaction logic for Page1.xaml
+ ///
+ public partial class Download : Page
+ {
+ private readonly Controller _controller;
+ public Download()
+ {
+ InitializeComponent();
+ _controller = App.ControllerInstance;
+ DoInstall();
+ }
+
+ private async void DoInstall()
+ {
+ await _controller.Install(InstallProgress, Status);
+ NextPageButton.IsEnabled = true;
+ }
+
+ private void BackPageButton_Click(object sender, System.Windows.RoutedEventArgs e)
+ {
+ _controller.LoadPages(InstallType.Home);
+ }
+
+ private void NextPageButton_Click(object sender, System.Windows.RoutedEventArgs e)
+ {
+ _controller.NextPage();
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Shared/FinishInstall.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Shared/FinishInstall.xaml
new file mode 100644
index 000000000..28f23c6f0
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Shared/FinishInstall.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Shared/FinishInstall.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Shared/FinishInstall.xaml.cs
new file mode 100644
index 000000000..e94c99abe
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Shared/FinishInstall.xaml.cs
@@ -0,0 +1,22 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Install
+{
+ ///
+ /// Interaction logic for FinishInstall.xaml
+ ///
+ public partial class FinishInstall : Page
+ {
+ public FinishInstall()
+ {
+ InitializeComponent();
+ }
+
+ private void NextPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ InstallLogger.DumpLog();
+ Application.Current.Shutdown();
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/FinishUninstall.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/FinishUninstall.xaml
new file mode 100644
index 000000000..d49f8c7e2
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/FinishUninstall.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/FinishUninstall.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/FinishUninstall.xaml.cs
new file mode 100644
index 000000000..04dc69ac5
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/FinishUninstall.xaml.cs
@@ -0,0 +1,21 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Update
+{
+ ///
+ /// Interaction logic for FinishInstall.xaml
+ ///
+ public partial class FinishUninstall : Page
+ {
+ public FinishUninstall()
+ {
+ InitializeComponent();
+ }
+
+ private void NextPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ Application.Current.Shutdown();
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/Uninstall.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/Uninstall.xaml
new file mode 100644
index 000000000..c2fcc4f90
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/Uninstall.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/Uninstall.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/Uninstall.xaml.cs
new file mode 100644
index 000000000..86a59f94b
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/Uninstall.xaml.cs
@@ -0,0 +1,26 @@
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Update
+{
+ ///
+ /// Interaction logic for Page1.xaml
+ ///
+ public partial class Uninstall : Page
+ {
+ private readonly Controller _controller;
+ public Uninstall()
+ {
+ InitializeComponent();
+ _controller = App.ControllerInstance;
+ _controller.Uninstall(UninstallProgress);
+ }
+
+ private void UninstallProgress_ValueChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs e)
+ {
+ if (UninstallProgress.Value == 100)
+ {
+ _controller.NextPage();
+ }
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/UninstallPage.xaml b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/UninstallPage.xaml
new file mode 100644
index 000000000..c8134cd01
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/UninstallPage.xaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/UninstallPage.xaml.cs b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/UninstallPage.xaml.cs
new file mode 100644
index 000000000..c68f326a1
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Pages/Uninstall/UninstallPage.xaml.cs
@@ -0,0 +1,25 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace NoPortsInstaller.Pages.Update
+{
+ ///
+ /// Interaction logic for UninstallPage.xaml
+ ///
+ public partial class UninstallPage : Page
+ {
+ private readonly Controller _controller;
+ public UninstallPage()
+ {
+ InitializeComponent();
+ _controller = App.ControllerInstance;
+ }
+
+ private void NextPageButton_Click(object sender, RoutedEventArgs e)
+ {
+ _controller.Pages.Add(new Uninstall());
+ _controller.Pages.Add(new FinishUninstall());
+ _controller.NextPage();
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Properties/PublishProfiles/FolderProfile.pubxml b/tools/windows-installer/NoPortsInstaller/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 000000000..45c7a9f17
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Properties/PublishProfiles/FolderProfile.pubxml
@@ -0,0 +1,17 @@
+
+
+
+
+ Release
+ Any CPU
+ bin\Release\win-x64\publish\
+ FileSystem
+ <_TargetId>Folder
+ net8.0-windows
+ win-x64
+ true
+ false
+
+
\ No newline at end of file
diff --git a/tools/windows-installer/NoPortsInstaller/Properties/Resources.Designer.cs b/tools/windows-installer/NoPortsInstaller/Properties/Resources.Designer.cs
new file mode 100644
index 000000000..e315c5eb7
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Properties/Resources.Designer.cs
@@ -0,0 +1,123 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace NoPortsInstaller.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NoPortsInstaller.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] at_activate {
+ get {
+ object obj = ResourceManager.GetObject("at_activate", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] npt {
+ get {
+ object obj = ResourceManager.GetObject("npt", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] srv {
+ get {
+ object obj = ResourceManager.GetObject("srv", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] sshnp {
+ get {
+ object obj = ResourceManager.GetObject("sshnp", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] sshnpd {
+ get {
+ object obj = ResourceManager.GetObject("sshnpd", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] SshnpdService {
+ get {
+ object obj = ResourceManager.GetObject("SshnpdService", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/Properties/Resources.resx b/tools/windows-installer/NoPortsInstaller/Properties/Resources.resx
new file mode 100644
index 000000000..adab1a97f
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/Properties/Resources.resx
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ ..\Resources\at_activate.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ ..\Resources\npt.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ ..\Resources\srv.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ ..\Resources\sshnp.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ ..\Resources\sshnpd.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ ..\Resources\SshnpdService.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/tools/windows-installer/NoPortsInstaller/ServiceController.cs b/tools/windows-installer/NoPortsInstaller/ServiceController.cs
new file mode 100644
index 000000000..95074fe1b
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/ServiceController.cs
@@ -0,0 +1,543 @@
+using Microsoft.Win32;
+using System.Diagnostics;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+namespace NoPortsInstaller
+{
+ public static class ServiceController
+ {
+ private const int STANDARD_RIGHTS_REQUIRED = 0xF0000;
+ private const int SERVICE_WIN32_OWN_PROCESS = 0x00000010;
+
+ [StructLayout(LayoutKind.Sequential)]
+ private class SERVICE_STATUS
+ {
+ public int dwServiceType = 0;
+ public ServiceState dwCurrentState = 0;
+ public int dwControlsAccepted = 0;
+ public int dwWin32ExitCode = 0;
+ public int dwServiceSpecificExitCode = 0;
+ public int dwCheckPoint = 0;
+ public int dwWaitHint = 0;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private class SERVICE_FAILURE_ACTIONS
+ {
+ public int dwResetPeriod = 0;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string lpRebootMsg = null;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string lpCommand = null;
+ public int cActions = 0;
+ public SC_ACTION[] lpsaActions = [];
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private class SC_ACTION
+ {
+ public int type;
+ public int delay;
+ }
+
+ #region OpenSCManager
+ [DllImport("advapi32.dll", EntryPoint = "OpenSCManagerW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
+ static extern IntPtr OpenSCManager(string machineName, string databaseName, ScmAccessRights dwDesiredAccess);
+ #endregion
+
+ #region OpenService
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, ServiceAccessRights dwDesiredAccess);
+ #endregion
+
+ #region CreateService
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern IntPtr CreateService(IntPtr hSCManager, string lpServiceName, string lpDisplayName, ServiceAccessRights dwDesiredAccess, int dwServiceType, ServiceBootFlag dwStartType, ServiceError dwErrorControl, string lpBinaryPathName, string lpLoadOrderGroup, IntPtr lpdwTagId, string lpDependencies, string lp, string lpPassword);
+ #endregion
+
+ #region CloseServiceHandle
+ [DllImport("advapi32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ static extern bool CloseServiceHandle(IntPtr hSCObject);
+ #endregion
+
+ #region QueryServiceStatus
+ [DllImport("advapi32.dll")]
+ private static extern int QueryServiceStatus(IntPtr hService, SERVICE_STATUS lpServiceStatus);
+ #endregion
+
+ #region QueryServiceConfig
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern int ChangeServiceConfig(IntPtr hService, int dwInfoLevel, IntPtr lpInfo);
+ #endregion
+
+ #region DeleteService
+ [DllImport("advapi32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool DeleteService(IntPtr hService);
+ #endregion
+
+ #region ControlService
+ [DllImport("advapi32.dll")]
+ private static extern int ControlService(IntPtr hService, ServiceControl dwControl, SERVICE_STATUS lpServiceStatus);
+ #endregion
+
+ #region StartService
+ [DllImport("advapi32.dll", SetLastError = true)]
+ private static extern int StartService(IntPtr hService, int dwNumServiceArgs, int lpServiceArgVectors);
+ #endregion
+
+ public static void Uninstall(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.AllAccess);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.AllAccess);
+ if (service == IntPtr.Zero)
+ {
+ throw new ApplicationException("Service not installed.");
+ }
+
+ try
+ {
+ StopService(service);
+ if (!DeleteService(service))
+ {
+ throw new ApplicationException("Could not delete service " + Marshal.GetLastWin32Error());
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ public static bool ServiceIsInstalled(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.Connect);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.QueryStatus);
+
+ if (service == IntPtr.Zero)
+ {
+ return false;
+ }
+
+ CloseServiceHandle(service);
+ return true;
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ public static void InstallAndStart(string serviceName, string displayName, string fileName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.AllAccess);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.AllAccess);
+
+ if (service == IntPtr.Zero)
+ {
+
+ service = CreateService(scm, serviceName, displayName, ServiceAccessRights.AllAccess, SERVICE_WIN32_OWN_PROCESS, ServiceBootFlag.AutoStart, ServiceError.Normal, fileName, null, IntPtr.Zero, null, @"NT AUTHORITY\LocalService", null);
+ }
+
+ if (service == IntPtr.Zero)
+ {
+ throw new ApplicationException("Failed to install service.");
+ }
+
+ try
+ {
+ StartService(service);
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ public static void StartService(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.Connect);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.QueryStatus | ServiceAccessRights.Start);
+ if (service == IntPtr.Zero)
+ {
+ throw new ApplicationException("Could not open service.");
+ }
+
+ try
+ {
+ StartService(service);
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ public static void StopService(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.Connect);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.QueryStatus | ServiceAccessRights.Stop);
+ if (service == IntPtr.Zero)
+ {
+ throw new ApplicationException("Could not open service.");
+ }
+
+ try
+ {
+ StopService(service);
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ private static void StartService(IntPtr service)
+ {
+ SERVICE_STATUS status = new();
+ StartService(service, 0, 0);
+ var changedStatus = WaitForServiceStatus(service, ServiceState.StartPending, ServiceState.Running);
+ if (!changedStatus)
+ {
+ throw new ApplicationException("Unable to start service");
+ }
+ }
+
+ private static void StopService(IntPtr service)
+ {
+ SERVICE_STATUS status = new();
+ ControlService(service, ServiceControl.Stop, status);
+ var changedStatus = WaitForServiceStatus(service, ServiceState.StopPending, ServiceState.Stopped);
+ if (!changedStatus)
+ {
+ throw new ApplicationException("Unable to stop service");
+ }
+ }
+
+ public static ServiceState GetServiceStatus(string serviceName)
+ {
+ IntPtr scm = OpenSCManager(ScmAccessRights.Connect);
+
+ try
+ {
+ IntPtr service = OpenService(scm, serviceName, ServiceAccessRights.QueryStatus);
+ if (service == IntPtr.Zero)
+ {
+ return ServiceState.NotFound;
+ }
+
+ try
+ {
+ return GetServiceStatus(service);
+ }
+ finally
+ {
+ CloseServiceHandle(service);
+ }
+ }
+ finally
+ {
+ CloseServiceHandle(scm);
+ }
+ }
+
+ private static ServiceState GetServiceStatus(IntPtr service)
+ {
+ SERVICE_STATUS status = new();
+
+ if (QueryServiceStatus(service, status) == 0)
+ {
+ throw new ApplicationException("Failed to query service status.");
+ }
+
+ return status.dwCurrentState;
+ }
+
+ public static void SetRecoveryOptions(string serviceName)
+ {
+ int exitCode;
+ using (var process = new Process())
+ {
+ var startInfo = process.StartInfo;
+ startInfo.FileName = "sc";
+ startInfo.WindowStyle = ProcessWindowStyle.Hidden;
+
+ // tell Windows that the service should restart if it fails
+ startInfo.Arguments = string.Format("failure \"{0}\" reset= 0 actions= restart/3000/restart/60000/", serviceName);
+
+ process.Start();
+ process.WaitForExit();
+
+ exitCode = process.ExitCode;
+ }
+
+ if (exitCode != 0)
+ {
+ throw new InvalidOperationException();
+ }
+ }
+
+ private static bool WaitForServiceStatus(IntPtr service, ServiceState waitStatus, ServiceState desiredStatus)
+ {
+ SERVICE_STATUS status = new();
+
+ QueryServiceStatus(service, status);
+ if (status.dwCurrentState == desiredStatus)
+ {
+ return true;
+ }
+
+ int dwStartTickCount = Environment.TickCount;
+ int dwOldCheckPoint = status.dwCheckPoint;
+
+ while (status.dwCurrentState == waitStatus)
+ {
+ // Do not wait longer than the wait hint. A good interval is
+ // one tenth the wait hint, but no less than 1 second and no
+ // more than 10 seconds.
+
+ int dwWaitTime = status.dwWaitHint / 10;
+
+ if (dwWaitTime < 1000)
+ {
+ dwWaitTime = 1000;
+ }
+ else if (dwWaitTime > 10000)
+ {
+ dwWaitTime = 10000;
+ }
+
+ Thread.Sleep(dwWaitTime);
+
+ // Check the status again.
+
+ if (QueryServiceStatus(service, status) == 0)
+ {
+ break;
+ }
+
+ if (status.dwCheckPoint > dwOldCheckPoint)
+ {
+ // The service is making progress.
+ dwStartTickCount = Environment.TickCount;
+ dwOldCheckPoint = status.dwCheckPoint;
+ }
+ else
+ {
+ if (Environment.TickCount - dwStartTickCount > status.dwWaitHint)
+ {
+ // No progress made within the wait hint
+ break;
+ }
+ }
+ }
+ return (status.dwCurrentState == desiredStatus);
+ }
+
+ private static IntPtr OpenSCManager(ScmAccessRights rights)
+ {
+ IntPtr scm = OpenSCManager(null, null, rights);
+ if (scm == IntPtr.Zero)
+ {
+ throw new ApplicationException("Could not connect to service control manager.");
+ }
+
+ return scm;
+ }
+
+ public static void CreateUninstaller(string binPath)
+ {
+ string UninstallRegKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
+ Assembly assembly = Assembly.GetExecutingAssembly();
+ using RegistryKey? parent = Registry.LocalMachine.OpenSubKey(UninstallRegKeyPath, true);
+ if (parent == null)
+ {
+ throw new Exception("Uninstall registry key not found.");
+ }
+
+ try
+ {
+ RegistryKey? key = null;
+ try
+ {
+ string keyName = "NoPorts";
+ key = parent.OpenSubKey(keyName, true) ?? parent.CreateSubKey(keyName);
+
+ if (key == null)
+ {
+ throw new Exception($"Unable to create uninstaller '{UninstallRegKeyPath}\\{keyName}'");
+ }
+
+ Version v = assembly.GetName().Version!;
+
+ key.SetValue("DisplayName", "NoPorts");
+ key.SetValue("ApplicationVersion", v.ToString());
+ key.SetValue("Publisher", "Atsign Inc");
+ key.SetValue("DisplayVersion", v.ToString(2));
+ key.SetValue("URLInfoAbout", "http://www.noports.com");
+ key.SetValue("Contact", "info@atsign.com");
+ key.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
+ key.SetValue("UninstallString", $"{binPath} ");
+
+ }
+ finally
+ {
+ key?.Close();
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new Exception(
+ "An error occurred writing uninstall information to the registry. The service is fully installed but can only be uninstalled manually through the command line.",
+ ex);
+ }
+ }
+
+ public static async Task TryUninstall(string service)
+ {
+ if (ServiceController.ServiceIsInstalled(service))
+ {
+ int maxAttempts = 3;
+ for (int i = 0; i < maxAttempts; i++)
+ {
+ try
+ {
+ StopService(service);
+ Uninstall(service);
+ return;
+ }
+ catch (Exception ex)
+ {
+ if (i == maxAttempts - 1)
+ {
+ throw new Exception("Failed to uninstall service", ex);
+ }
+ await Task.Delay(500);
+ }
+ }
+ }
+ }
+ }
+
+ public enum ServiceState
+ {
+ Unknown = -1, // The state cannot be (has not been) retrieved.
+ NotFound = 0, // The service is not known on the host server.
+ Stopped = 1,
+ StartPending = 2,
+ StopPending = 3,
+ Running = 4,
+ ContinuePending = 5,
+ PausePending = 6,
+ Paused = 7
+ }
+
+ [Flags]
+ public enum ScmAccessRights
+ {
+ Connect = 0x0001,
+ CreateService = 0x0002,
+ EnumerateService = 0x0004,
+ Lock = 0x0008,
+ QueryLockStatus = 0x0010,
+ ModifyBootConfig = 0x0020,
+ StandardRightsRequired = 0xF0000,
+ AllAccess = (StandardRightsRequired | Connect | CreateService |
+ EnumerateService | Lock | QueryLockStatus | ModifyBootConfig)
+ }
+
+ [Flags]
+ public enum ServiceAccessRights
+ {
+ QueryConfig = 0x1,
+ ChangeConfig = 0x2,
+ QueryStatus = 0x4,
+ EnumerateDependants = 0x8,
+ Start = 0x10,
+ Stop = 0x20,
+ PauseContinue = 0x40,
+ Interrogate = 0x80,
+ UserDefinedControl = 0x100,
+ Delete = 0x00010000,
+ StandardRightsRequired = 0xF0000,
+ AllAccess = (StandardRightsRequired | QueryConfig | ChangeConfig |
+ QueryStatus | EnumerateDependants | Start | Stop | PauseContinue |
+ Interrogate | UserDefinedControl)
+ }
+
+ public enum ServiceBootFlag
+ {
+ Start = 0x00000000,
+ SystemStart = 0x00000001,
+ AutoStart = 0x00000002,
+ DemandStart = 0x00000003,
+ Disabled = 0x00000004
+ }
+
+ public enum ServiceControl
+ {
+ Stop = 0x00000001,
+ Pause = 0x00000002,
+ Continue = 0x00000003,
+ Interrogate = 0x00000004,
+ Shutdown = 0x00000005,
+ ParamChange = 0x00000006,
+ NetBindAdd = 0x00000007,
+ NetBindRemove = 0x00000008,
+ NetBindEnable = 0x00000009,
+ NetBindDisable = 0x0000000A
+ }
+
+ public enum ServiceError
+ {
+ Ignore = 0x00000000,
+ Normal = 0x00000001,
+ Severe = 0x00000002,
+ Critical = 0x00000003
+ }
+
+ public enum ServiceAction
+ {
+ None = 0,
+ Restart = 1,
+ Reboot = 2,
+ RunCommand = 3
+ }
+}
diff --git a/tools/windows-installer/NoPortsInstaller/app.manifest b/tools/windows-installer/NoPortsInstaller/app.manifest
new file mode 100644
index 000000000..67e3559f4
--- /dev/null
+++ b/tools/windows-installer/NoPortsInstaller/app.manifest
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/NoPortsWindows.sln b/tools/windows-installer/NoPortsWindows.sln
new file mode 100644
index 000000000..77b55dbc9
--- /dev/null
+++ b/tools/windows-installer/NoPortsWindows.sln
@@ -0,0 +1,64 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35013.160
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SshnpdService", "SshnpdService\SshnpdService.csproj", "{82FC9C48-3A1E-44D1-B463-8032A3249E21}"
+ ProjectSection(ProjectDependencies) = postProject
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F} = {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoPortsInstaller", "NoPortsInstaller\NoPortsInstaller.csproj", "{BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Debug|x64.Build.0 = Debug|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Debug|x86.Build.0 = Debug|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Release|Any CPU.Build.0 = Release|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Release|ARM64.Build.0 = Release|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Release|x64.ActiveCfg = Release|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Release|x64.Build.0 = Release|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Release|x86.ActiveCfg = Release|Any CPU
+ {82FC9C48-3A1E-44D1-B463-8032A3249E21}.Release|x86.Build.0 = Release|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Debug|x64.Build.0 = Debug|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Debug|x86.Build.0 = Debug|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Release|ARM64.Build.0 = Release|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Release|x64.ActiveCfg = Release|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Release|x64.Build.0 = Release|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Release|x86.ActiveCfg = Release|Any CPU
+ {BDD7E6B8-0CEE-443C-BEA2-F42053934B4F}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1FEB3DA0-06A9-4D74-8C61-246B05C969E6}
+ EndGlobalSection
+EndGlobal
diff --git a/tools/windows-installer/SshnpdService/GlobalSuppressions.cs b/tools/windows-installer/SshnpdService/GlobalSuppressions.cs
new file mode 100644
index 000000000..47700344f
--- /dev/null
+++ b/tools/windows-installer/SshnpdService/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Windows Only")]
diff --git a/tools/windows-installer/SshnpdService/Program.cs b/tools/windows-installer/SshnpdService/Program.cs
new file mode 100644
index 000000000..90f64f54c
--- /dev/null
+++ b/tools/windows-installer/SshnpdService/Program.cs
@@ -0,0 +1,25 @@
+using Microsoft.Extensions.Logging.Configuration;
+using Microsoft.Extensions.Logging.EventLog;
+using SshnpdService;
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Services.AddWindowsService(options =>
+{
+ options.ServiceName = "sshnpd";
+});
+
+LoggerProviderOptions.RegisterProviderOptions<
+ EventLogSettings, EventLogLoggerProvider>(services: builder.Services);
+
+builder.Services.AddLogging(loggingBuilder => loggingBuilder
+ .AddEventLog()
+ .AddFilter("Microsoft", LogLevel.Warning)
+ .AddFilter("System", LogLevel.Warning)
+ .AddFilter("NoPorts", LogLevel.Information));
+
+builder.Services.AddSingleton();
+builder.Services.AddHostedService();
+
+
+var host = builder.Build();
+host.Run();
diff --git a/tools/windows-installer/SshnpdService/Properties/PublishProfiles/FolderProfile.pubxml b/tools/windows-installer/SshnpdService/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 000000000..3feaebaf8
--- /dev/null
+++ b/tools/windows-installer/SshnpdService/Properties/PublishProfiles/FolderProfile.pubxml
@@ -0,0 +1,16 @@
+
+
+
+
+ Release
+ x64
+ bin\Release\net8.0\publish\
+ FileSystem
+ <_TargetId>Folder
+ net8.0-windows10.0.22621.0
+ win-x64
+ true
+
+
\ No newline at end of file
diff --git a/tools/windows-installer/SshnpdService/Sshnpd.cs b/tools/windows-installer/SshnpdService/Sshnpd.cs
new file mode 100644
index 000000000..353749b8b
--- /dev/null
+++ b/tools/windows-installer/SshnpdService/Sshnpd.cs
@@ -0,0 +1,163 @@
+using Microsoft.Win32;
+using System.Diagnostics;
+using System.Text;
+
+namespace SshnpdService
+{
+ ///
+ /// This class is responsible for the creation and termination of the sshnpd process.
+ ///
+ public sealed class Sshnpd
+ {
+ ///
+ /// Gets the arguments for the sshnpd process from the registry.
+ ///
+
+ private string GetArgs()
+ {
+ try
+ {
+ RegistryKey? registryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\NoPorts");
+ if (registryKey != null)
+ {
+ object? deviceArgs = registryKey.GetValue("DeviceArgs");
+ if (deviceArgs != null)
+ {
+ return deviceArgs.ToString()!;
+ }
+ }
+ }
+ catch
+ {
+ throw;
+ }
+ return string.Empty;
+ }
+
+ ///
+ /// Gets the path to the sshnpd executable from the registry.
+ ///
+
+ private string GetBinPath()
+ {
+ try
+ {
+ RegistryKey? registryKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\NoPorts");
+ if (registryKey != null)
+ {
+ object? binPath = registryKey.GetValue("BinPath");
+ if (binPath != null)
+ {
+ return binPath.ToString() + @"\sshnpd.exe";
+ }
+ }
+ }
+ catch
+ {
+ throw;
+ }
+ return string.Empty;
+ }
+
+ ///
+ /// Closes the sshnpd process gracefully or forcefully terminates it.
+ ///
+ public void Close()
+ {
+ try
+ {
+ Process[] processes = Process.GetProcessesByName("sshnpd");
+ foreach (Process process in processes)
+ {
+ try
+ {
+ // Attempt to close the process gracefully
+ process.CloseMainWindow();
+ process.WaitForExit(3000); // Wait for 3 seconds for the process to exit
+ if (!process.HasExited)
+ {
+ // Forcefully terminate the process if it does not close gracefully
+ process.Kill();
+ }
+ process.Dispose();
+ }
+ catch
+ {
+ throw;
+ }
+ }
+ }
+ catch
+ {
+ // Log any exceptions that occur while retrieving the processes
+ throw;
+ }
+ }
+
+ ///
+ /// Runs the sshnpd process asynchronously.
+ ///
+ /// The cancellation token to stop the process.
+ public async Task Run(CancellationToken cancellationToken)
+ {
+ var args = GetArgs();
+ // Use ProcessStartInfo class
+ ProcessStartInfo startInfo = new();
+ startInfo.CreateNoWindow = false;
+ startInfo.UseShellExecute = false;
+ startInfo.FileName = GetBinPath();
+ startInfo.WindowStyle = ProcessWindowStyle.Hidden;
+ startInfo.RedirectStandardOutput = true;
+ startInfo.RedirectStandardError = true;
+
+ startInfo.Arguments = GetArgs();
+
+ StringBuilder stdout = new();
+ StringBuilder stderr = new();
+
+ try
+ {
+ using Process exeProcess = Process.Start(startInfo)!;
+ //Event Log has a default limit of 32KB per entry
+ int bufferLimit = 16384;
+ int outLines = 0;
+ int errLines = 0;
+ exeProcess.OutputDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ stdout.AppendLine(e.Data);
+ if (stdout.Length >= bufferLimit || outLines >= 5)
+ {
+ EventLog.WriteEntry("NoPorts", stdout.ToString(), EventLogEntryType.Information);
+ stdout.Clear();
+ }
+ outLines++;
+ }
+ };
+ exeProcess.ErrorDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ stderr.AppendLine(e.Data);
+ if (stderr.Length >= bufferLimit || errLines >= 5)
+ {
+ EventLog.WriteEntry("NoPorts", stderr.ToString(), EventLogEntryType.Information);
+ stderr.Clear();
+ }
+ errLines++;
+ }
+ };
+
+ exeProcess.BeginOutputReadLine();
+ exeProcess.BeginErrorReadLine();
+ await exeProcess.WaitForExitAsync(cancellationToken);
+ return;
+ }
+ catch
+ {
+ throw;
+ }
+ }
+ }
+}
diff --git a/tools/windows-installer/SshnpdService/SshnpdService.csproj b/tools/windows-installer/SshnpdService/SshnpdService.csproj
new file mode 100644
index 000000000..afd83cc7b
--- /dev/null
+++ b/tools/windows-installer/SshnpdService/SshnpdService.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0-windows10.0.22621.0
+ enable
+ enable
+ dotnet-SshnpdService-d2f561af-aaf6-4e52-869c-084e7d933422
+ exe
+ true
+ true
+ win-x64
+ true
+ true
+ embedded
+ x64
+ True
+
+
+
+
+
+
+
diff --git a/tools/windows-installer/SshnpdService/SshnpdWindowsService.cs b/tools/windows-installer/SshnpdService/SshnpdWindowsService.cs
new file mode 100644
index 000000000..87b88a810
--- /dev/null
+++ b/tools/windows-installer/SshnpdService/SshnpdWindowsService.cs
@@ -0,0 +1,60 @@
+namespace SshnpdService
+{
+ ///
+ /// Represents a Windows service for the Sshnpd application.
+ ///
+ public class SshnpdWindowsService : BackgroundService
+ {
+ private readonly ILogger _logger;
+ private readonly Sshnpd _sshnpdService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Sshnpd service instance.
+ /// The logger instance.
+ public SshnpdWindowsService(Sshnpd sshnpdService, ILogger logger)
+ {
+ _sshnpdService = sshnpdService;
+ _logger = logger;
+ }
+
+ ///
+ /// Executes the Sshnpd service asynchronously.
+ ///
+ /// The cancellation token to stop the service.
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ await _sshnpdService.Run(stoppingToken);
+ _logger.LogInformation("Sshnpd service is restarting...");
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // _sshnpdService.Close();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "{Message}", ex.Message);
+
+ // Terminates this process and returns an exit code to the operating system.
+ // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
+ // performs one of two scenarios:
+ // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
+ // 2. When set to "StopHost": will cleanly stop the host, and log errors.
+ //
+ // In order for the Windows Service Management system to leverage configured
+ // recovery options, we need to terminate the process with a non-zero exit code.
+ Environment.Exit(1);
+ }
+ }
+ _sshnpdService.Close();
+ }
+ }
+}
diff --git a/tools/windows-installer/SshnpdService/appsettings.Development.json b/tools/windows-installer/SshnpdService/appsettings.Development.json
new file mode 100644
index 000000000..690176464
--- /dev/null
+++ b/tools/windows-installer/SshnpdService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/tools/windows-installer/SshnpdService/appsettings.json b/tools/windows-installer/SshnpdService/appsettings.json
new file mode 100644
index 000000000..690176464
--- /dev/null
+++ b/tools/windows-installer/SshnpdService/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}