Compare commits
3 Commits
update-nix
...
remove-dep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b37fa18f1b | ||
|
|
f539d00e9a | ||
|
|
2d22eecd32 |
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"type": "age"
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{ ... }:
|
||||
|
||||
let
|
||||
error = builtins.throw ''
|
||||
|
||||
###############################################################################
|
||||
# #
|
||||
# Clan modules (clanModules) have been deprecated and removed in favor of #
|
||||
# Clan services! #
|
||||
# #
|
||||
# Refer to https://docs.clan.lol/guides/migrations/migrate-inventory-services #
|
||||
# for migration instructions. #
|
||||
# #
|
||||
###############################################################################
|
||||
|
||||
'';
|
||||
|
||||
modnames = [
|
||||
"admin"
|
||||
"borgbackup"
|
||||
"borgbackup-static"
|
||||
"deltachat"
|
||||
"disk-id"
|
||||
"dyndns"
|
||||
"ergochat"
|
||||
"garage"
|
||||
"heisenbridge"
|
||||
"iwd"
|
||||
"localbackup"
|
||||
"localsend"
|
||||
"matrix-synapse"
|
||||
"moonlight"
|
||||
"mumble"
|
||||
"nginx"
|
||||
"packages"
|
||||
"postgresql"
|
||||
"root-password"
|
||||
"single-disk"
|
||||
"sshd"
|
||||
"state-version"
|
||||
"static-hosts"
|
||||
"sunshine"
|
||||
"syncthing"
|
||||
"syncthing-static-peers"
|
||||
"thelounge"
|
||||
"trusted-nix-caches"
|
||||
"user-password"
|
||||
"vaultwarden"
|
||||
"xfce"
|
||||
"zerotier-static-peers"
|
||||
"zt-tcp-relay"
|
||||
];
|
||||
in
|
||||
|
||||
{
|
||||
flake.clanModules = builtins.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = error;
|
||||
}) modnames
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
# We don't have a way of specifying dependencies between clanServices for now.
|
||||
# When it get's added this file should be removed and the users module used instead.
|
||||
{
|
||||
roles.default.perInstance =
|
||||
{ ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
users.mutableUsers = false;
|
||||
users.users.root.hashedPasswordFile =
|
||||
config.clan.core.vars.generators.root-password.files.password-hash.path;
|
||||
|
||||
clan.core.vars.generators.root-password = {
|
||||
files.password-hash.neededFor = "users";
|
||||
|
||||
files.password.deploy = false;
|
||||
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.mkpasswd
|
||||
pkgs.xkcdpass
|
||||
];
|
||||
|
||||
prompts.password.display = {
|
||||
group = "Root User";
|
||||
label = "Password";
|
||||
required = false;
|
||||
helperText = ''
|
||||
Your password will be encrypted and stored securely using the secret store you've configured.
|
||||
'';
|
||||
};
|
||||
|
||||
prompts.password.type = "hidden";
|
||||
prompts.password.persist = true;
|
||||
prompts.password.description = "Leave empty to generate automatically";
|
||||
|
||||
script = ''
|
||||
prompt_value="$(cat "$prompts"/password)"
|
||||
if [[ -n "''${prompt_value-}" ]]; then
|
||||
echo "$prompt_value" | tr -d "\n" > "$out"/password
|
||||
else
|
||||
xkcdpass --numwords 5 --delimiter - --count 1 | tr -d "\n" > "$out"/password
|
||||
fi
|
||||
mkpasswd -s -m sha-512 < "$out"/password | tr -d "\n" > "$out"/password-hash
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -18,4 +18,11 @@
|
||||
imports = map (name: ./. + "/${name}/flake-module.nix") validModuleDirs;
|
||||
in
|
||||
imports;
|
||||
|
||||
flake.clanModules = builtins.throw ''
|
||||
clanModules have been removed!
|
||||
|
||||
Refer to https://docs.clan.lol/guides/migrations/migrate-inventory-services for migration.
|
||||
'';
|
||||
|
||||
}
|
||||
|
||||
@@ -46,8 +46,6 @@
|
||||
"checks/lib/ssh/privkey"
|
||||
"checks/lib/ssh/pubkey"
|
||||
"checks/matrix-synapse/synapse-registration_shared_secret"
|
||||
"checks/mumble/machines/peer1/facts/mumble-cert"
|
||||
"checks/mumble/machines/peer2/facts/mumble-cert"
|
||||
"checks/secrets/clan-secrets"
|
||||
"checks/secrets/sops/groups/group/machines/machine"
|
||||
"checks/syncthing/introducer/introducer_device_id"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
clan.nixosTests.machine-id = {
|
||||
|
||||
name = "service-machine-id";
|
||||
name = "machine-id";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
|
||||
cfg = config.clan.core.postgresql;
|
||||
|
||||
createDatabaseState =
|
||||
db:
|
||||
let
|
||||
folder = "/var/backup/postgres/${db.name}";
|
||||
current = "${folder}/pg-dump";
|
||||
compression = lib.optionalString (lib.versionAtLeast config.services.postgresql.package.version "16") "--compress=zstd";
|
||||
in
|
||||
{
|
||||
folders = [ folder ];
|
||||
preBackupScript = ''
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
config.services.postgresql.package
|
||||
config.systemd.package
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.zstd
|
||||
]
|
||||
}
|
||||
while [[ "$(systemctl is-active postgresql)" == activating ]]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
mkdir -p "${folder}"
|
||||
runuser -u postgres -- pg_dump ${compression} --dbname=${db.name} -Fc -c > "${current}.tmp"
|
||||
mv "${current}.tmp" ${current}
|
||||
'';
|
||||
postRestoreScript = ''
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
config.services.postgresql.package
|
||||
config.systemd.package
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.zstd
|
||||
pkgs.gnugrep
|
||||
]
|
||||
}
|
||||
while [[ "$(systemctl is-active postgresql)" == activating ]]; do
|
||||
sleep 1
|
||||
done
|
||||
echo "Waiting for postgres to be ready..."
|
||||
while ! runuser -u postgres -- psql --port=${builtins.toString config.services.postgresql.settings.port} -d postgres -c "" ; do
|
||||
if ! systemctl is-active postgresql; then exit 1; fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [[ -e "${current}" ]]; then
|
||||
(
|
||||
systemctl stop ${lib.concatStringsSep " " db.restore.stopOnRestore}
|
||||
trap "systemctl start ${lib.concatStringsSep " " db.restore.stopOnRestore}" EXIT
|
||||
|
||||
mkdir -p "${folder}"
|
||||
if runuser -u postgres -- psql -d postgres -c "SELECT 1 FROM pg_database WHERE datname = '${db.name}'" | grep -q 1; then
|
||||
runuser -u postgres -- dropdb "${db.name}"
|
||||
fi
|
||||
runuser -u postgres -- pg_restore -C -d postgres "${current}"
|
||||
)
|
||||
else
|
||||
echo No database backup found, skipping restore
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
createDatabase = db: ''
|
||||
CREATE DATABASE "${db.name}" ${
|
||||
lib.concatStringsSep " " (
|
||||
lib.mapAttrsToList (name: value: "${name} = '${value}'") db.create.options
|
||||
)
|
||||
}
|
||||
'';
|
||||
|
||||
userClauses = lib.mapAttrsToList (
|
||||
_: user:
|
||||
''$PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"' ''
|
||||
) cfg.users;
|
||||
databaseClauses = lib.mapAttrsToList (
|
||||
name: db:
|
||||
lib.optionalString db.create.enable ''$PSQL -d postgres -c "SELECT 1 FROM pg_database WHERE datname = '${name}'" | grep -q 1 || $PSQL -d postgres -c ${lib.escapeShellArg (createDatabase db)} ''
|
||||
) cfg.databases;
|
||||
in
|
||||
{
|
||||
options.clan.core.postgresql = {
|
||||
|
||||
enable = lib.mkEnableOption "Whether to enable PostgreSQL Server";
|
||||
|
||||
# we are reimplemeting ensureDatabase and ensureUser options here to allow to create databases with options
|
||||
databases = lib.mkOption {
|
||||
description = "Databases to create";
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
description = "Database name.";
|
||||
};
|
||||
service = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
description = "Service name that we associate with the database.";
|
||||
};
|
||||
# set to false, in case the upstream module uses ensureDatabase option
|
||||
create.enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Create the database if it does not exist.";
|
||||
};
|
||||
create.options = lib.mkOption {
|
||||
description = "Options to pass to the CREATE DATABASE command.";
|
||||
type = lib.types.lazyAttrsOf lib.types.str;
|
||||
default = { };
|
||||
example = {
|
||||
TEMPLATE = "template0";
|
||||
LC_COLLATE = "C";
|
||||
LC_CTYPE = "C";
|
||||
ENCODING = "UTF8";
|
||||
OWNER = "foo";
|
||||
};
|
||||
};
|
||||
restore.stopOnRestore = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "List of systemd services to stop before restoring the database.";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
users = lib.mkOption {
|
||||
description = "Users to create";
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options.name = lib.mkOption {
|
||||
description = "User name";
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (config.clan.core.postgresql.enable) {
|
||||
|
||||
clan.core.settings.state-version.enable = true;
|
||||
|
||||
# services.postgresql.package = lib.mkDefault pkgs.postgresql_16;
|
||||
|
||||
services.postgresql.enable = true;
|
||||
|
||||
services.postgresql.settings = {
|
||||
wal_level = "replica";
|
||||
max_wal_senders = 3;
|
||||
};
|
||||
|
||||
# We are duplicating a bit the upstream module but allow to create databases with options
|
||||
systemd.services.postgresql.postStart = ''
|
||||
PSQL="psql --port=${builtins.toString config.services.postgresql.settings.port}"
|
||||
|
||||
while ! $PSQL -d postgres -c "" 2> /dev/null; do
|
||||
if ! kill -0 "$MAINPID"; then exit 1; fi
|
||||
sleep 0.1
|
||||
done
|
||||
${lib.concatStringsSep "\n" userClauses}
|
||||
${lib.concatStringsSep "\n" databaseClauses}
|
||||
'';
|
||||
|
||||
clan.core.state = lib.mapAttrs' (
|
||||
_: db: lib.nameValuePair db.service (createDatabaseState db)
|
||||
) config.clan.core.postgresql.databases;
|
||||
|
||||
environment.systemPackages = builtins.map (
|
||||
db:
|
||||
let
|
||||
folder = "/var/backup/postgres/${db.name}";
|
||||
current = "${folder}/pg-dump";
|
||||
in
|
||||
pkgs.writeShellScriptBin "postgres-db-restore-command-${db.name}" ''
|
||||
export PATH=${
|
||||
lib.makeBinPath [
|
||||
config.services.postgresql.package
|
||||
config.systemd.package
|
||||
pkgs.coreutils
|
||||
pkgs.util-linux
|
||||
pkgs.zstd
|
||||
pkgs.gnugrep
|
||||
]
|
||||
}
|
||||
while [[ "$(systemctl is-active postgresql)" == activating ]]; do
|
||||
sleep 1
|
||||
done
|
||||
echo "Waiting for postgres to be ready..."
|
||||
while ! runuser -u postgres -- psql --port=${builtins.toString config.services.postgresql.settings.port} -d postgres -c "" ; do
|
||||
if ! systemctl is-active postgresql; then exit 1; fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [[ -e "${current}" ]]; then
|
||||
(
|
||||
${lib.optionalString (db.restore.stopOnRestore != [ ]) ''
|
||||
systemctl stop ${builtins.toString db.restore.stopOnRestore}
|
||||
trap "systemctl start ${builtins.toString db.restore.stopOnRestore}" EXIT
|
||||
''}
|
||||
|
||||
mkdir -p "${folder}"
|
||||
if runuser -u postgres -- psql -d postgres -c "SELECT 1 FROM pg_database WHERE datname = '${db.name}'" | grep -q 1; then
|
||||
runuser -u postgres -- dropdb "${db.name}"
|
||||
fi
|
||||
runuser -u postgres -- pg_restore -C -d postgres "${current}"
|
||||
)
|
||||
else
|
||||
echo No database backup found, skipping restore
|
||||
fi
|
||||
''
|
||||
) (builtins.attrValues config.clan.core.postgresql.databases);
|
||||
};
|
||||
}
|
||||
@@ -173,7 +173,6 @@ class ClanFlake:
|
||||
"git+https://git.clan.lol/clan/clan-core": clan_core_replacement,
|
||||
"https://git.clan.lol/clan/clan-core/archive/main.tar.gz": clan_core_replacement,
|
||||
}
|
||||
self.clan_modules: list[str] = []
|
||||
self.temporary_home = temporary_home
|
||||
self.path = temporary_home / "flake"
|
||||
if not suppress_tmp_home_warning:
|
||||
@@ -235,9 +234,6 @@ class ClanFlake:
|
||||
if self.inventory:
|
||||
inventory_path = self.path / "inventory.json"
|
||||
inventory_path.write_text(json.dumps(self.inventory, indent=2))
|
||||
imports = "\n".join(
|
||||
[f"clan-core.clanModules.{module}" for module in self.clan_modules]
|
||||
)
|
||||
for machine_name, machine_config in self.machines.items():
|
||||
configuration_nix = (
|
||||
self.path / "machines" / machine_name / "configuration.nix"
|
||||
@@ -249,7 +245,6 @@ class ClanFlake:
|
||||
{{
|
||||
imports = [
|
||||
(builtins.fromJSON (builtins.readFile ./configuration.json))
|
||||
{imports}
|
||||
];
|
||||
}}
|
||||
"""
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
inputs':
|
||||
let
|
||||
# fake clan-core input
|
||||
fake-clan-core = {
|
||||
clanModules.fake-module = ./fake-module.nix;
|
||||
};
|
||||
inputs = inputs' // {
|
||||
clan-core = fake-clan-core;
|
||||
};
|
||||
# TODO should this be removed as well?
|
||||
# fake-clan-core = {
|
||||
# clanModules.fake-module = ./fake-module.nix;
|
||||
# };
|
||||
inputs = inputs';
|
||||
# inputs = inputs' // {
|
||||
# clan-core = fake-clan-core;
|
||||
# };
|
||||
lib = inputs.nixpkgs.lib;
|
||||
clan_attrs_json =
|
||||
if lib.pathExists ./clan_attrs.json then
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
clanCoreWithVendoredDeps = self'.packages.clan-core-flake.override {
|
||||
clanCore = self.filter {
|
||||
include = [
|
||||
"clanModules"
|
||||
"flakeModules"
|
||||
"lib"
|
||||
"nixosModules"
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
agit = pkgs.callPackage ./agit { };
|
||||
tea-create-pr = pkgs.callPackage ./tea-create-pr { };
|
||||
zerotier-members = pkgs.callPackage ./zerotier-members { };
|
||||
moonlight-sunshine-accept = pkgs.callPackage ./moonlight-sunshine-accept { };
|
||||
merge-after-ci = pkgs.callPackage ./merge-after-ci { inherit (config.packages) tea-create-pr; };
|
||||
minifakeroot = pkgs.callPackage ./minifakeroot { };
|
||||
pending-reviews = pkgs.callPackage ./pending-reviews { };
|
||||
editor = pkgs.callPackage ./editor/clan-edit-codium.nix { };
|
||||
classgen = pkgs.callPackage ./classgen { };
|
||||
zerotierone = pkgs.callPackage ./zerotierone { };
|
||||
update-clan-core-for-checks = pkgs.callPackage ./update-clan-core-for-checks { };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# shellcheck shell=bash
|
||||
use flake .#moonlight-sunshine-accept
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
python3Packages,
|
||||
makeDesktopItem,
|
||||
copyDesktopItems,
|
||||
}:
|
||||
let
|
||||
desktop-file = makeDesktopItem {
|
||||
name = "org.clan.moonlight-sunset-accept";
|
||||
exec = "moonlight-sunshine-accept moonlight join %u";
|
||||
desktopName = "moonlight-handler";
|
||||
startupWMClass = "moonlight-handler";
|
||||
mimeTypes = [ "x-scheme-handler/moonlight" ];
|
||||
};
|
||||
in
|
||||
python3Packages.buildPythonApplication {
|
||||
name = "moonlight-sunshine-accept";
|
||||
|
||||
src = ./.;
|
||||
|
||||
format = "pyproject";
|
||||
|
||||
propagatedBuildInputs = [ python3Packages.cryptography ];
|
||||
nativeBuildInputs = [
|
||||
python3Packages.setuptools
|
||||
copyDesktopItems
|
||||
];
|
||||
|
||||
desktopItems = [ desktop-file ];
|
||||
|
||||
meta = with lib; {
|
||||
description = "Moonlight Sunshine Bridge";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ a-kenji ];
|
||||
mainProgram = "moonlight-sunshine-accept";
|
||||
};
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
|
||||
from . import moonlight, sunshine
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="moonlight-sunshine-accept",
|
||||
description="Manage moonlight machines",
|
||||
)
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
parser_sunshine = subparsers.add_parser(
|
||||
"sunshine",
|
||||
aliases=["sun"],
|
||||
description="Sunshine configuration",
|
||||
help="Sunshine configuration",
|
||||
)
|
||||
sunshine.register_parser(parser_sunshine)
|
||||
|
||||
parser_moonlight = subparsers.add_parser(
|
||||
"moonlight",
|
||||
aliases=["moon"],
|
||||
description="Moonlight configuration",
|
||||
help="Moonlight configuration",
|
||||
)
|
||||
moonlight.register_parser(parser_moonlight)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not hasattr(args, "func"):
|
||||
parser.print_help()
|
||||
else:
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,2 +0,0 @@
|
||||
class Error(Exception):
|
||||
pass
|
||||
@@ -1,37 +0,0 @@
|
||||
import argparse
|
||||
|
||||
from .init_certificates import register_initialization_parser
|
||||
from .init_config import register_config_initialization_parser
|
||||
from .join import register_join_parser
|
||||
|
||||
|
||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
subparser = parser.add_subparsers(
|
||||
title="command",
|
||||
description="the command to run",
|
||||
help="the command to run",
|
||||
required=True,
|
||||
)
|
||||
|
||||
initialization_parser = subparser.add_parser(
|
||||
"init",
|
||||
aliases=["i"],
|
||||
description="Initialize the moonlight credentials",
|
||||
help="Initialize the moonlight credentials",
|
||||
)
|
||||
register_initialization_parser(initialization_parser)
|
||||
|
||||
config_initialization_parser = subparser.add_parser(
|
||||
"init-config",
|
||||
description="Initialize the moonlight configuration",
|
||||
help="Initialize the moonlight configuration",
|
||||
)
|
||||
register_config_initialization_parser(config_initialization_parser)
|
||||
|
||||
join_parser = subparser.add_parser(
|
||||
"join",
|
||||
aliases=["j"],
|
||||
description="Join a sunshine host",
|
||||
help="Join a sunshine host",
|
||||
)
|
||||
register_join_parser(join_parser)
|
||||
@@ -1,74 +0,0 @@
|
||||
import argparse
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography import hazmat, x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.x509.oid import NameOID
|
||||
|
||||
|
||||
def generate_private_key() -> rsa.RSAPrivateKey:
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=2048, backend=hazmat.backends.default_backend()
|
||||
)
|
||||
return private_key
|
||||
|
||||
|
||||
def generate_certificate(private_key: rsa.RSAPrivateKey) -> bytes:
|
||||
subject = issuer = x509.Name(
|
||||
[
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, "NVIDIA GameStream Client"),
|
||||
]
|
||||
)
|
||||
cert_builder = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(issuer)
|
||||
.public_key(private_key.public_key())
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.datetime.now(tz=datetime.UTC))
|
||||
.not_valid_after(
|
||||
datetime.datetime.now(tz=datetime.UTC) + timedelta(days=365 * 20)
|
||||
)
|
||||
.add_extension(
|
||||
x509.SubjectAlternativeName([x509.DNSName("localhost")]),
|
||||
critical=False,
|
||||
)
|
||||
.sign(private_key, hashes.SHA256(), default_backend())
|
||||
)
|
||||
pem_certificate = cert_builder.public_bytes(serialization.Encoding.PEM)
|
||||
return pem_certificate
|
||||
|
||||
|
||||
def private_key_to_pem(private_key: rsa.RSAPrivateKey) -> bytes:
|
||||
pem_private_key = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
# format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
return pem_private_key
|
||||
|
||||
|
||||
def init_credentials() -> tuple[bytes, bytes]:
|
||||
private_key = generate_private_key()
|
||||
certificate = generate_certificate(private_key)
|
||||
private_key_pem = private_key_to_pem(private_key)
|
||||
return certificate, private_key_pem
|
||||
|
||||
|
||||
def write_credentials(_args: argparse.Namespace) -> None:
|
||||
pem_certificate, pem_private_key = init_credentials()
|
||||
credentials_path = Path.cwd() / "credentials"
|
||||
Path(credentials_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
(credentials_path / "cacert.pem").write_bytes(pem_certificate)
|
||||
(credentials_path / "cakey.pem").write_bytes(pem_private_key)
|
||||
print("Finished writing moonlight credentials")
|
||||
|
||||
|
||||
def register_initialization_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.set_defaults(func=write_credentials)
|
||||
@@ -1,15 +0,0 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from .state import init_state
|
||||
|
||||
|
||||
def init_config(args: argparse.Namespace) -> None:
|
||||
init_state(args.certificate.read_text(), args.key.read_text())
|
||||
print("Finished initializing moonlight state.")
|
||||
|
||||
|
||||
def register_config_initialization_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--certificate", type=Path)
|
||||
parser.add_argument("--key", type=Path)
|
||||
parser.set_defaults(func=init_config)
|
||||
@@ -1,131 +0,0 @@
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
|
||||
from .run import MoonlightPairing
|
||||
from .state import add_sunshine_host, gen_pin, get_moonlight_certificate
|
||||
from .uri import parse_moonlight_uri
|
||||
|
||||
|
||||
def send_join_request(host: str, port: int, cert: str) -> bool:
|
||||
max_tries = 3
|
||||
response = False
|
||||
for _tries in range(max_tries):
|
||||
response = send_join_request_api(host, port)
|
||||
if response:
|
||||
return response
|
||||
return bool(send_join_request_native(host, port, cert))
|
||||
|
||||
|
||||
# This is the preferred join method, but sunshines pin mechanism
|
||||
# seems to be somewhat brittle in repeated testing, retry then fallback to native
|
||||
def send_join_request_api(host: str, port: int) -> bool:
|
||||
moonlight = MoonlightPairing()
|
||||
# is_paired = moonlight.check(host)
|
||||
is_paired = False
|
||||
if is_paired:
|
||||
print(f"Moonlight is already paired with this host: {host}")
|
||||
return True
|
||||
pin = gen_pin()
|
||||
moonlight.init_pairing(host, pin)
|
||||
moonlight.wait_until_started()
|
||||
|
||||
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
json_body = json.dumps({"type": "api", "pin": pin})
|
||||
request = (
|
||||
f"POST / HTTP/1.1\r\n"
|
||||
f"Content-Type: application/json\r\n"
|
||||
f"Content-Length: {len(json_body)}\r\n"
|
||||
f"Connection: close\r\n\r\n"
|
||||
f"{json_body}"
|
||||
)
|
||||
try:
|
||||
s.sendall(request.encode("utf-8"))
|
||||
response = s.recv(16384).decode("utf-8")
|
||||
print(response)
|
||||
body = response.split("\n")[-1]
|
||||
print(body)
|
||||
moonlight.terminate()
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
moonlight.terminate()
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def send_join_request_native(host: str, port: int, cert: str) -> bool:
|
||||
# This is the hardcoded UUID for the moonlight client
|
||||
uuid = "123456789ABCD"
|
||||
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
|
||||
s.connect((host, port))
|
||||
encoded_cert = base64.urlsafe_b64encode(cert.encode("utf-8")).decode("utf-8")
|
||||
json_body_str = json.dumps(
|
||||
{"type": "native", "uuid": uuid, "cert": encoded_cert}
|
||||
)
|
||||
request = (
|
||||
f"POST / HTTP/1.1\r\n"
|
||||
f"Content-Type: application/json\r\n"
|
||||
f"Content-Length: {len(json_body_str)}\r\n"
|
||||
f"Connection: close\r\n\r\n"
|
||||
f"{json_body_str}"
|
||||
)
|
||||
try:
|
||||
s.sendall(request.encode("utf-8"))
|
||||
response = s.recv(16384).decode("utf-8")
|
||||
print(response)
|
||||
lines = response.split("\n")
|
||||
body = "\n".join(lines[2:])[2:]
|
||||
print(body)
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
else:
|
||||
return True
|
||||
# TODO: fix
|
||||
try:
|
||||
print(f"response: {response}")
|
||||
data = json.loads(response)
|
||||
print(f"Data: {data}")
|
||||
print(f"Host uuid: {data['uuid']}")
|
||||
print(f"Host certificate: {data['cert']}")
|
||||
print("Joining sunshine host")
|
||||
cert = data["cert"]
|
||||
cert = base64.urlsafe_b64decode(cert).decode("utf-8")
|
||||
uuid = data["uuid"]
|
||||
hostname = data["hostname"]
|
||||
add_sunshine_host(hostname, host, cert, uuid)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Failed to decode JSON: {e}")
|
||||
pos = e.pos
|
||||
print(f"Failed to decode JSON: unexpected character {response[pos]}")
|
||||
return False
|
||||
|
||||
|
||||
def join(args: argparse.Namespace) -> None:
|
||||
if args.url:
|
||||
(host, port) = parse_moonlight_uri(args.url)
|
||||
if port is None:
|
||||
port = 48011
|
||||
else:
|
||||
port = args.port
|
||||
host = args.host
|
||||
|
||||
print(f"Host: {host}, port: {port}")
|
||||
# TODO: If cert is not provided parse from config
|
||||
# cert = args.cert
|
||||
cert = get_moonlight_certificate()
|
||||
assert port is not None
|
||||
if send_join_request(host, port, cert):
|
||||
print(f"Successfully joined sunshine host: {host}")
|
||||
else:
|
||||
print(f"Failed to join sunshine host: {host}")
|
||||
|
||||
|
||||
def register_join_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("url", nargs="?")
|
||||
parser.add_argument("--port", type=int, default=48011)
|
||||
parser.add_argument("--host")
|
||||
parser.add_argument("--cert")
|
||||
parser.set_defaults(func=join)
|
||||
@@ -1,63 +0,0 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
|
||||
class MoonlightPairing:
|
||||
def __init__(self) -> None:
|
||||
self.process: subprocess.Popen | None = None
|
||||
self.output = ""
|
||||
self.found = threading.Event()
|
||||
|
||||
def init_pairing(self, host: str, pin: str) -> bool:
|
||||
# host = f"[{host}]"
|
||||
args = ["moonlight", "pair", host, "--pin", pin]
|
||||
print("Trying to pair")
|
||||
try:
|
||||
print(f"Running command: {args}")
|
||||
self.process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
print("Pairing initiated")
|
||||
thread = threading.Thread(
|
||||
target=self.stream_output,
|
||||
args=('Latest supported GFE server: "99.99.99.99"',),
|
||||
)
|
||||
thread.start()
|
||||
print("Thread started")
|
||||
except Exception as e:
|
||||
print(
|
||||
"Error occurred while starting the process: ", str(e), file=sys.stderr
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def check(self, host: str) -> bool:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["moonlight", "list", "localhost", host], check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
else:
|
||||
return result.returncode == 0
|
||||
|
||||
def terminate(self) -> None:
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
|
||||
def stream_output(self, target_string: str) -> None:
|
||||
assert self.process is not None
|
||||
assert self.process.stdout is not None
|
||||
for line in iter(self.process.stdout.readline, b""):
|
||||
line = line.decode()
|
||||
self.output += line
|
||||
if target_string in line:
|
||||
self.found.set()
|
||||
break
|
||||
|
||||
def wait_until_started(self, timeout: int = 10) -> None:
|
||||
if self.found.wait(timeout):
|
||||
print("Started up.")
|
||||
else:
|
||||
print("Starting up took took too long. Terminated the process.")
|
||||
@@ -1,149 +0,0 @@
|
||||
import contextlib
|
||||
import random
|
||||
import string
|
||||
from configparser import ConfigParser, DuplicateSectionError, NoOptionError
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def moonlight_config_dir() -> Path:
|
||||
return Path.home() / ".config" / "Moonlight Game Streaming Project"
|
||||
|
||||
|
||||
def moonlight_state_file() -> Path:
|
||||
return moonlight_config_dir() / "Moonlight.conf"
|
||||
|
||||
|
||||
def load_state() -> ConfigParser | None:
|
||||
try:
|
||||
with moonlight_state_file().open() as file:
|
||||
config = ConfigParser()
|
||||
config.read_file(file)
|
||||
print(config.sections())
|
||||
return config
|
||||
except FileNotFoundError:
|
||||
print("Sunshine state file not found.")
|
||||
return None
|
||||
|
||||
|
||||
# prepare the string for the config file
|
||||
# this is how qt handles byte arrays
|
||||
def convert_string_to_bytearray(data: str) -> str:
|
||||
byte_array = '"@ByteArray('
|
||||
byte_array += data.replace("\n", "\\n")
|
||||
byte_array += ')"'
|
||||
return byte_array
|
||||
|
||||
|
||||
def convert_bytearray_to_string(byte_array: str) -> str:
|
||||
if byte_array.startswith('"@ByteArray(') and byte_array.endswith(')"'):
|
||||
byte_array = byte_array[12:-2]
|
||||
return byte_array.replace("\\n", "\n")
|
||||
return byte_array
|
||||
|
||||
|
||||
# this must be created before moonlight is first run
|
||||
def init_state(certificate: str, key: str) -> None:
|
||||
print("Initializing moonlight state.")
|
||||
moonlight_config_dir().mkdir(parents=True, exist_ok=True)
|
||||
print("Initialized moonlight config directory.")
|
||||
|
||||
print("Writing moonlight state file.")
|
||||
# write the initial bootstrap config file
|
||||
with moonlight_state_file().open("w") as file:
|
||||
config = ConfigParser()
|
||||
# bytearray objects are not supported by ConfigParser,
|
||||
# so we need to adjust them ourselves
|
||||
config.add_section("General")
|
||||
config.set("General", "certificate", convert_string_to_bytearray(certificate))
|
||||
config.set("General", "key", convert_string_to_bytearray(key))
|
||||
config.set("General", "latestsupportedversion-v1", "99.99.99.99")
|
||||
config.add_section("gcmapping")
|
||||
config.set("gcmapping", "size", "0")
|
||||
|
||||
config.write(file)
|
||||
|
||||
|
||||
def write_state(data: ConfigParser) -> None:
|
||||
with moonlight_state_file().open("w") as file:
|
||||
data.write(file)
|
||||
|
||||
|
||||
def add_sunshine_host_to_parser(
|
||||
config: ConfigParser, hostname: str, manual_host: str, certificate: str, uuid: str
|
||||
) -> bool:
|
||||
with contextlib.suppress(DuplicateSectionError):
|
||||
config.add_section("hosts")
|
||||
|
||||
# amount of hosts
|
||||
try:
|
||||
no_hosts = int(config.get("hosts", "size"))
|
||||
except NoOptionError:
|
||||
no_hosts = 0
|
||||
|
||||
new_host = no_hosts + 1
|
||||
|
||||
config.set(
|
||||
"hosts", f"{new_host}\\srvcert", convert_string_to_bytearray(certificate)
|
||||
)
|
||||
config.set("hosts", "size", str(new_host))
|
||||
config.set("hosts", f"{new_host}\\uuid", uuid)
|
||||
config.set("hosts", f"{new_host}\\hostname", hostname)
|
||||
config.set("hosts", f"{new_host}\\nvidiasv", "false")
|
||||
config.set("hosts", f"{new_host}\\customname", "false")
|
||||
config.set("hosts", f"{new_host}\\manualaddress", manual_host)
|
||||
config.set("hosts", f"{new_host}\\manualport", "47989")
|
||||
config.set("hosts", f"{new_host}\\remoteport", "0")
|
||||
config.set("hosts", f"{new_host}\\remoteaddress", "")
|
||||
config.set("hosts", f"{new_host}\\localaddress", "")
|
||||
config.set("hosts", f"{new_host}\\localport", "0")
|
||||
config.set("hosts", f"{new_host}\\ipv6port", "0")
|
||||
config.set("hosts", f"{new_host}\\ipv6address", "")
|
||||
config.set(
|
||||
"hosts", f"{new_host}\\mac", convert_string_to_bytearray("\\xceop\\x8d\\xfc{")
|
||||
)
|
||||
add_app(config, "Desktop", new_host, 1, 881448767)
|
||||
add_app(config, "Low Res Desktop", new_host, 2, 303580669)
|
||||
add_app(config, "Steam Big Picture", new_host, 3, 1093255277)
|
||||
|
||||
print(config.items("hosts"))
|
||||
return True
|
||||
|
||||
|
||||
# set default apps for the host for now
|
||||
# TODO: do this dynamically
|
||||
def add_app(
|
||||
config: ConfigParser, name: str, host_id: int, app_id: int, app_no: int
|
||||
) -> None:
|
||||
identifier = f"{host_id}\\apps\\{app_id}\\"
|
||||
config.set("hosts", f"{identifier}appcollector", "false")
|
||||
config.set("hosts", f"{identifier}directlaunch", "false")
|
||||
config.set("hosts", f"{identifier}hdr", "false")
|
||||
config.set("hosts", f"{identifier}hidden", "false")
|
||||
config.set("hosts", f"{identifier}id", f"{app_no}")
|
||||
config.set("hosts", f"{identifier}name", f"{name}")
|
||||
|
||||
|
||||
def get_moonlight_certificate() -> str:
|
||||
config = load_state()
|
||||
if config is None:
|
||||
msg = "Moonlight state file not found."
|
||||
raise FileNotFoundError(msg)
|
||||
certificate = config.get("General", "certificate")
|
||||
certificate = convert_bytearray_to_string(certificate)
|
||||
return certificate
|
||||
|
||||
|
||||
def gen_pin() -> str:
|
||||
return "".join(random.choice(string.digits) for _ in range(4))
|
||||
|
||||
|
||||
def add_sunshine_host(
|
||||
hostname: str, manual_host: str, certificate: str, uuid: str
|
||||
) -> bool:
|
||||
config = load_state()
|
||||
if config is None:
|
||||
return False
|
||||
hostname = "test"
|
||||
add_sunshine_host_to_parser(config, hostname, manual_host, certificate, uuid)
|
||||
write_state(config)
|
||||
return True
|
||||
@@ -1,22 +0,0 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from moonlight_sunshine_accept.errors import Error
|
||||
|
||||
|
||||
def parse_moonlight_uri(uri: str) -> tuple[str, int | None]:
|
||||
print(uri)
|
||||
if uri.startswith("moonlight:"):
|
||||
# Fixes a bug where moonlight:// is not parsed correctly
|
||||
uri = uri[10:]
|
||||
uri = "moonlight://" + uri
|
||||
print(uri)
|
||||
parsed = urlparse(uri)
|
||||
if parsed.scheme != "moonlight":
|
||||
msg = f"Invalid moonlight URI: {uri}"
|
||||
raise Error(msg)
|
||||
hostname = parsed.hostname
|
||||
if hostname is None:
|
||||
msg = f"Invalid moonlight URI: {uri}"
|
||||
raise Error(msg)
|
||||
port = parsed.port
|
||||
return (hostname, port)
|
||||
@@ -1,63 +0,0 @@
|
||||
import argparse
|
||||
|
||||
from .init_certificates import register_initialization_parser
|
||||
from .init_state import register_state_initialization_parser
|
||||
from .listen import register_socket_listener
|
||||
|
||||
|
||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
subparser = parser.add_subparsers(
|
||||
title="command",
|
||||
description="the command to run",
|
||||
help="the command to run",
|
||||
required=True,
|
||||
)
|
||||
|
||||
subparser.add_parser(
|
||||
"generate",
|
||||
# title="command",
|
||||
aliases=["gen"],
|
||||
description="Generate a shareable link",
|
||||
help="Generate a shareable link",
|
||||
)
|
||||
# TODO: add a timeout for the link
|
||||
# generate.add_argument(
|
||||
# "--timeout",
|
||||
# default="10",
|
||||
# )
|
||||
# copy = subparsers.add_parser("copy", description="Copy the link to the clipboard")
|
||||
|
||||
initialization_parser = subparser.add_parser(
|
||||
"init",
|
||||
aliases=["i"],
|
||||
description="Initialize the sunshine credentials",
|
||||
help="Initialize the sunshine credentials",
|
||||
)
|
||||
register_initialization_parser(initialization_parser)
|
||||
|
||||
state_initialization_parser = subparser.add_parser(
|
||||
"init-state",
|
||||
description="Initialize the sunshine state file",
|
||||
help="Initialize the sunshine state file",
|
||||
)
|
||||
register_state_initialization_parser(state_initialization_parser)
|
||||
|
||||
listen_parser = subparser.add_parser(
|
||||
"listen",
|
||||
description="Listen for incoming connections",
|
||||
help="Listen for incoming connections",
|
||||
)
|
||||
register_socket_listener(listen_parser)
|
||||
|
||||
# TODO: Add a machine directly <- useful when using dependent secrets
|
||||
# sunshine_add = subparser.add_parser(
|
||||
# "add",
|
||||
# aliases=["a"],
|
||||
# description="Add a new moonlight machine to sunshine",
|
||||
# help="Add a new moonlight machine to sunshine",
|
||||
# )
|
||||
# sunshine_add.add_argument("--url", type=str, help="URL of the moonlight machine")
|
||||
# sunshine_add.add_argument(
|
||||
# "--cert", type=str, help="Certificate of the moonlight machine"
|
||||
# )
|
||||
# sunshine_add.add_argument("--uuid", type=str, help="UUID of the moonlight machine")
|
||||
@@ -1,63 +0,0 @@
|
||||
import base64
|
||||
import http.client
|
||||
import json
|
||||
|
||||
|
||||
def get_context() -> http.client.ssl.SSLContext: # type: ignore
|
||||
# context = http.client.ssl.create_default_context()
|
||||
# # context.load_cert_chain(
|
||||
# # certfile="/var/lib/sunshine/sunshine.cert", keyfile="/var/lib/sunshine/sunshine.key"
|
||||
# # )
|
||||
# context.load_cert_chain(
|
||||
# certfile="/home/kenji/.config/sunshine/credentials/cacert.pem",
|
||||
# keyfile="/home/kenji/.config/sunshine/credentials/cakey.pem",
|
||||
# )
|
||||
return http.client.ssl._create_unverified_context() # type: ignore # noqa: SLF001
|
||||
|
||||
|
||||
def pair(pin: str) -> str:
|
||||
conn = http.client.HTTPSConnection("localhost", 47990, context=get_context())
|
||||
|
||||
# TODO: dynamic username and password
|
||||
user_and_pass = base64.b64encode(b"sunshine:sunshine").decode("ascii")
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Basic {user_and_pass}",
|
||||
}
|
||||
|
||||
# Define the parameters
|
||||
params = json.dumps({"pin": f"{pin}"})
|
||||
|
||||
# Make the request
|
||||
conn.request("POST", "/api/pin", params, headers)
|
||||
|
||||
# Get and print the response
|
||||
res = conn.getresponse()
|
||||
data = res.read()
|
||||
|
||||
print(data.decode("utf-8"))
|
||||
return data.decode("utf-8")
|
||||
|
||||
|
||||
def restart() -> None:
|
||||
# Define the connection
|
||||
conn = http.client.HTTPSConnection(
|
||||
"localhost",
|
||||
47990,
|
||||
context=http.client.ssl._create_unverified_context(), # type: ignore # noqa: SLF001
|
||||
)
|
||||
user_and_pass = base64.b64encode(b"sunshine:sunshine").decode("ascii")
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Basic {user_and_pass}",
|
||||
}
|
||||
|
||||
# Make the request
|
||||
conn.request("POST", "/api/restart", {}, headers)
|
||||
|
||||
# Get and print the response
|
||||
# There wont be a response, because it is restarted
|
||||
res = conn.getresponse()
|
||||
data = res.read()
|
||||
|
||||
print(data.decode("utf-8"))
|
||||
@@ -1,49 +0,0 @@
|
||||
import configparser
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
# address_family = both
|
||||
# channels = 5
|
||||
# pkey = /var/lib/sunshine/sunshine.key
|
||||
# cert = /var/lib/sunshine/sunshine.cert
|
||||
# file_state = /var/lib/sunshine/state.json
|
||||
# credentials_file = /var/lib/sunshine/credentials.json
|
||||
|
||||
PSEUDO_SECTION = "DEFAULT"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
config: configparser.ConfigParser
|
||||
config_location: Path
|
||||
_instance = None
|
||||
|
||||
def __new__(cls, config_location: Path | None = None) -> "Config":
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.config = configparser.ConfigParser()
|
||||
config = config_location or cls._instance.default_sunshine_config_file()
|
||||
cls._instance.config_location = config
|
||||
with config.open() as f:
|
||||
config_string = f"[{PSEUDO_SECTION}]\n" + f.read()
|
||||
print(config_string)
|
||||
cls._instance.config.read_string(config_string)
|
||||
return cls._instance
|
||||
|
||||
def default_sunshine_config_dir(self) -> Path:
|
||||
return Path.home() / ".config" / "sunshine"
|
||||
|
||||
def default_sunshine_config_file(self) -> Path:
|
||||
return self.default_sunshine_config_dir() / "sunshine.conf"
|
||||
|
||||
def get_private_key(self) -> str:
|
||||
return self.config.get(PSEUDO_SECTION, "pkey")
|
||||
|
||||
def get_certificate(self) -> str:
|
||||
return self.config.get(PSEUDO_SECTION, "cert")
|
||||
|
||||
def get_state_file(self) -> str:
|
||||
return self.config.get(PSEUDO_SECTION, "file_state")
|
||||
|
||||
def get_credentials_file(self) -> str:
|
||||
return self.config.get(PSEUDO_SECTION, "credentials_file")
|
||||
@@ -1,76 +0,0 @@
|
||||
import argparse
|
||||
import datetime
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography import hazmat, x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.x509.oid import NameOID
|
||||
|
||||
|
||||
def generate_private_key() -> rsa.RSAPrivateKey:
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=2048, backend=hazmat.backends.default_backend()
|
||||
)
|
||||
return private_key
|
||||
|
||||
|
||||
def generate_certificate(private_key: rsa.RSAPrivateKey) -> bytes:
|
||||
subject = issuer = x509.Name(
|
||||
[
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, "Sunshine Gamestream Host"),
|
||||
]
|
||||
)
|
||||
cert_builder = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(issuer)
|
||||
.public_key(private_key.public_key())
|
||||
.serial_number(61093384576940497812448570031200738505731293357)
|
||||
.not_valid_before(datetime.datetime(2024, 2, 27, tzinfo=datetime.UTC))
|
||||
.not_valid_after(datetime.datetime(2044, 2, 22, tzinfo=datetime.UTC))
|
||||
.add_extension(
|
||||
x509.SubjectAlternativeName([x509.DNSName("localhost")]),
|
||||
critical=False,
|
||||
)
|
||||
.sign(private_key, hashes.SHA256(), default_backend())
|
||||
)
|
||||
pem_certificate = cert_builder.public_bytes(serialization.Encoding.PEM)
|
||||
return pem_certificate
|
||||
|
||||
|
||||
def private_key_to_pem(private_key: rsa.RSAPrivateKey) -> bytes:
|
||||
pem_private_key = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
return pem_private_key
|
||||
|
||||
|
||||
def init_credentials() -> tuple[bytes, bytes]:
|
||||
private_key = generate_private_key()
|
||||
certificate = generate_certificate(private_key)
|
||||
private_key_pem = private_key_to_pem(private_key)
|
||||
return certificate, private_key_pem
|
||||
|
||||
|
||||
def uniqueid() -> str:
|
||||
return str(uuid.uuid4()).upper()
|
||||
|
||||
|
||||
def write_credentials(_args: argparse.Namespace) -> None:
|
||||
print("Writing sunshine credentials")
|
||||
pem_certificate, pem_private_key = init_credentials()
|
||||
credentials_dir = Path("credentials")
|
||||
credentials_dir.mkdir(parents=True, exist_ok=True)
|
||||
(credentials_dir / "cacert.pem").write_bytes(pem_certificate)
|
||||
(credentials_dir / "cakey.pem").write_bytes(pem_private_key)
|
||||
print("Generating sunshine UUID")
|
||||
Path("uuid").write_text(uniqueid())
|
||||
|
||||
|
||||
def register_initialization_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.set_defaults(func=write_credentials)
|
||||
@@ -1,16 +0,0 @@
|
||||
import argparse
|
||||
|
||||
from .state import init_state
|
||||
|
||||
|
||||
def init_state_file(args: argparse.Namespace) -> None:
|
||||
uuid = args.uuid
|
||||
state_file = args.state_file
|
||||
init_state(uuid, state_file)
|
||||
print("Finished initializing sunshine state file.")
|
||||
|
||||
|
||||
def register_state_initialization_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--uuid")
|
||||
parser.add_argument("--state-file")
|
||||
parser.set_defaults(func=init_state_file)
|
||||
@@ -1,90 +0,0 @@
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
import traceback
|
||||
|
||||
from .api import pair
|
||||
from .state import default_sunshine_state_file
|
||||
|
||||
|
||||
# listen on a specific port for information from the moonlight side
|
||||
def listen(port: int, cert: str, uuid: str, state_file: str) -> bool:
|
||||
host = ""
|
||||
# Create a socket object with dual-stack support
|
||||
server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
# Enable dual-stack support
|
||||
server_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
||||
# Bind the socket to the host and port
|
||||
server_socket.bind((host, port))
|
||||
# Listen for incoming connections (accept up to 5)
|
||||
server_socket.listen(5)
|
||||
|
||||
while True:
|
||||
# Accept incoming connection
|
||||
client_socket, addr = server_socket.accept()
|
||||
|
||||
print(f"Connection accepted from {addr}")
|
||||
|
||||
# Receive data from the client
|
||||
|
||||
data = client_socket.recv(16384)
|
||||
|
||||
try:
|
||||
request = data.decode("utf-8")
|
||||
raw_body = request.split("\n")[-1]
|
||||
print(raw_body)
|
||||
body = json.loads(raw_body)
|
||||
|
||||
pair_type = body.get("type", "")
|
||||
|
||||
if pair_type == "api":
|
||||
print("Api request")
|
||||
status = pair(body.get("pin", ""))
|
||||
status = json.dumps(status)
|
||||
response = f"HTTP/1.1 200 OK\r\nContent-Type:application/json\r\n\r\\{status}\r\n"
|
||||
client_socket.sendall(response.encode("utf-8"))
|
||||
|
||||
if pair_type == "native":
|
||||
# url = unquote(data_str.split()[1])
|
||||
# rec_uuid = parse_qs(urlparse(url).query).get("uuid", [""])[0]
|
||||
# rec_cert = parse_qs(urlparse(url).query).get("cert", [""])[0]
|
||||
# decoded_cert = base64.urlsafe_b64decode(rec_cert).decode("utf-8")
|
||||
# print(f"Received URL: {url}")
|
||||
# print(f"Extracted UUID: {rec_uuid}")
|
||||
# print(f"Extracted Cert: {decoded_cert}")
|
||||
encoded_cert = base64.urlsafe_b64encode(cert.encode("utf-8")).decode(
|
||||
"utf-8"
|
||||
)
|
||||
json_data = {}
|
||||
json_data["uuid"] = uuid
|
||||
json_data["cert"] = encoded_cert
|
||||
json_data["hostname"] = socket.gethostname()
|
||||
json_body = json.dumps(json_data)
|
||||
response = f"HTTP/1.1 200 OK\r\nContent-Type:application/json\r\n\r\\{json_body}\r\n"
|
||||
client_socket.sendall(response.encode("utf-8"))
|
||||
# add_moonlight_client(decoded_cert, state_file, rec_uuid)
|
||||
|
||||
except UnicodeDecodeError:
|
||||
print(f"UnicodeDecodeError: Cannot decode byte {data[8]}")
|
||||
traceback.print_exc()
|
||||
|
||||
client_socket.close()
|
||||
|
||||
|
||||
def init_listener(args: argparse.Namespace) -> None:
|
||||
port = args.port
|
||||
cert = args.cert
|
||||
uuid = args.uuid
|
||||
state = args.state
|
||||
listen(port, cert, uuid, state)
|
||||
|
||||
|
||||
def register_socket_listener(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--port", default=48011, type=int)
|
||||
parser.add_argument("--cert")
|
||||
parser.add_argument("--uuid")
|
||||
parser.add_argument("--state", default=default_sunshine_state_file())
|
||||
# TODO: auto accept
|
||||
# parser.add_argument("--auto-accept")
|
||||
parser.set_defaults(func=init_listener)
|
||||
@@ -1,67 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def default_sunshine_config_dir() -> Path:
|
||||
return Path.home() / ".config" / "sunshine"
|
||||
|
||||
|
||||
def default_sunshine_state_file() -> Path:
|
||||
return default_sunshine_config_dir() / "sunshine_state.json"
|
||||
|
||||
|
||||
def load_state(sunshine_state_path: Path) -> str | None:
|
||||
sunshine_state_path = sunshine_state_path or default_sunshine_state_file()
|
||||
print(f"Loading sunshine state from {sunshine_state_path}")
|
||||
try:
|
||||
return json.loads(sunshine_state_path.read_text())
|
||||
except FileNotFoundError:
|
||||
print("Sunshine state file not found.")
|
||||
return None
|
||||
|
||||
|
||||
# this needs to be created before sunshine is first run
|
||||
def init_state(uuid: str, sunshine_state_path: Path) -> None:
|
||||
print("Initializing sunshine state.")
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
data["root"] = {}
|
||||
data["root"]["uniqueid"] = uuid
|
||||
data["root"]["devices"] = []
|
||||
|
||||
# write the initial bootstrap config file
|
||||
write_state(data, sunshine_state_path)
|
||||
|
||||
|
||||
def write_state(data: dict[str, Any], sunshine_state_path: Path) -> None:
|
||||
sunshine_state_path = sunshine_state_path or default_sunshine_state_file()
|
||||
with sunshine_state_path.open("w") as file:
|
||||
json.dump(data, file, indent=4)
|
||||
|
||||
|
||||
# this is used by moonlight-qt
|
||||
def pseudo_uuid() -> str:
|
||||
return "0123456789ABCDEF"
|
||||
|
||||
|
||||
# TODO: finish this function
|
||||
def add_moonlight_client(
|
||||
certificate: str, sunshine_state_path: Path, uuid: str
|
||||
) -> None:
|
||||
print("Adding moonlight client to sunshine state.")
|
||||
raw_state = load_state(sunshine_state_path)
|
||||
if raw_state:
|
||||
state = json.loads(raw_state)
|
||||
|
||||
if not state["root"]["devices"]:
|
||||
state["root"]["devices"].append(
|
||||
{"uniqueid": pseudo_uuid(), "certs": [certificate]}
|
||||
)
|
||||
write_state(state, sunshine_state_path)
|
||||
if certificate not in state["root"]["devices"][0]["certs"]:
|
||||
state["root"]["devices"][0]["certs"].append(certificate)
|
||||
state["root"]["devices"][0]["uniqueid"] = pseudo_uuid()
|
||||
write_state(state, sunshine_state_path)
|
||||
else:
|
||||
print("Moonlight certificate already added.")
|
||||
@@ -1,9 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "moonlight-sunshine-accept"
|
||||
description = "Moonlight Sunshine Bridge"
|
||||
dynamic = ["version"]
|
||||
scripts = { moonlight-sunshine-accept = "moonlight_sunshine_accept:main" }
|
||||
Reference in New Issue
Block a user