init: sunshine-moonlight-accept module
This commit is contained in:
@@ -1,4 +1,84 @@
|
|||||||
{ pkgs, ... }: {
|
{ pkgs, config, ... }:
|
||||||
|
let
|
||||||
|
ms-accept = pkgs.callPackage ../pkgs/moonlight-sunshine-accept { };
|
||||||
|
defaultPort = 48011;
|
||||||
|
in
|
||||||
|
{
|
||||||
hardware.opengl.enable = true;
|
hardware.opengl.enable = true;
|
||||||
environment.systemPackages = [ pkgs.moonlight-qt ];
|
environment.systemPackages = [
|
||||||
|
pkgs.moonlight-qt
|
||||||
|
pkgs.libnotify
|
||||||
|
ms-accept
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '/var/lib/moonlight' 0770 'user' 'users' - -"
|
||||||
|
"C '/var/lib/moonlight/moonlight.cert' 0644 'user' 'users' - ${config.clanCore.secrets.moonlight.secrets."moonlight.cert".path or ""}"
|
||||||
|
"C '/var/lib/moonlight/moonlight.key' 0644 'user' 'users' - ${config.clanCore.secrets.moonlight.secrets."moonlight.key".path or ""}"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.user.services.init-moonlight = {
|
||||||
|
enable = false;
|
||||||
|
description = "Initializes moonlight";
|
||||||
|
wantedBy = [ "graphical-session.target" ];
|
||||||
|
script = ''
|
||||||
|
${ms-accept}/bin/moonlight-sunshine-accept moonlight init-config --key /var/lib/moonlight/moonlight.key --cert /var/lib/moonlight/moonlight.cert
|
||||||
|
'';
|
||||||
|
serviceConfig = {
|
||||||
|
user = "user";
|
||||||
|
Type = "oneshot";
|
||||||
|
WorkingDirectory = "/home/user/";
|
||||||
|
RunTimeDirectory = "moonlight";
|
||||||
|
TimeoutSec = "infinity";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
ReadOnlyPaths = [
|
||||||
|
"/var/lib/moonlight/moonlight.key"
|
||||||
|
"/var/lib/moonlight/moonlight.cert"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services.moonlight-join = {
|
||||||
|
description = "Join sunshine hosts";
|
||||||
|
script = ''
|
||||||
|
${ms-accept}/bin/moonlight-sunshine-accept moonlight join --port ${builtins.toString defaultPort} --cert '${config.clanCore.secrets.moonlight.facts."moonlight.cert".value or ""}' --host fd2e:25da:6035:c98f:cd99:93e0:b9b8:9ca1'';
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
TimeoutSec = "infinity";
|
||||||
|
Restart = "on-failure";
|
||||||
|
ReadOnlyPaths = [
|
||||||
|
"/var/lib/moonlight/moonlight.key"
|
||||||
|
"/var/lib/moonlight/moonlight.cert"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
systemd.user.timers.moonlight-join = {
|
||||||
|
description = "Join sunshine hosts";
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnUnitActiveSec = "5min";
|
||||||
|
OnBootSec = "0min";
|
||||||
|
Persistent = true;
|
||||||
|
Unit = "moonlight-join.service";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
clanCore.secrets.moonlight = {
|
||||||
|
secrets."moonlight.key" = { };
|
||||||
|
secrets."moonlight.cert" = { };
|
||||||
|
facts."moonlight.cert" = { };
|
||||||
|
generator.path = [
|
||||||
|
pkgs.coreutils
|
||||||
|
ms-accept
|
||||||
|
];
|
||||||
|
generator.script = ''
|
||||||
|
moonlight-sunshine-accept moonlight init
|
||||||
|
mv credentials/cakey.pem "$secrets"/moonlight.key
|
||||||
|
cp credentials/cacert.pem "$secrets"/moonlight.cert
|
||||||
|
mv credentials/cacert.pem "$facts"/moonlight.cert
|
||||||
|
'';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
{ pkgs, config, ... }:
|
{ pkgs, config, lib, ... }:
|
||||||
|
let
|
||||||
|
ms-accept = pkgs.callPackage ../pkgs/moonlight-sunshine-accept { };
|
||||||
|
sunshineConfiguration = pkgs.writeText "sunshine.conf" ''
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
listenPort = 48011;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [
|
||||||
@@ -6,6 +18,7 @@
|
|||||||
47989
|
47989
|
||||||
47990
|
47990
|
||||||
48010
|
48010
|
||||||
|
48011
|
||||||
];
|
];
|
||||||
|
|
||||||
allowedUDPPorts = [
|
allowedUDPPorts = [
|
||||||
@@ -29,17 +42,28 @@
|
|||||||
to = 48010;
|
to = 48010;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
networking.firewall.interfaces."zt+".allowedTCPPorts = [
|
||||||
|
47984
|
||||||
|
47989
|
||||||
|
47990
|
||||||
|
48010
|
||||||
|
listenPort
|
||||||
|
];
|
||||||
|
networking.firewall.interfaces."zt+".allowedUDPPortRanges = [
|
||||||
|
{
|
||||||
|
from = 47998;
|
||||||
|
to = 48010;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
|
ms-accept
|
||||||
pkgs.sunshine
|
pkgs.sunshine
|
||||||
pkgs.avahi
|
pkgs.avahi
|
||||||
# Convenience script, until we find a better UX
|
# Convenience script, until we find a better UX
|
||||||
(pkgs.writers.writeDashBin "sun" ''
|
(pkgs.writers.writeDashBin "sun" ''
|
||||||
${pkgs.sunshine}/bin/sunshine -1 ${
|
${pkgs.sunshine}/bin/sunshine -0 ${sunshineConfiguration} "$@"
|
||||||
pkgs.writeText "sunshine.conf" ''
|
|
||||||
address_family = both
|
|
||||||
''
|
|
||||||
} "$@"
|
|
||||||
'')
|
'')
|
||||||
# Create a dummy account, for easier setup,
|
# Create a dummy account, for easier setup,
|
||||||
# don't use this account in actual production yet.
|
# don't use this account in actual production yet.
|
||||||
@@ -51,32 +75,118 @@
|
|||||||
|
|
||||||
# Required to simulate input
|
# Required to simulate input
|
||||||
boot.kernelModules = [ "uinput" ];
|
boot.kernelModules = [ "uinput" ];
|
||||||
security.rtkit.enable = true;
|
|
||||||
|
|
||||||
# services.udev.extraRules = ''
|
|
||||||
# KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"
|
|
||||||
# '';
|
|
||||||
|
|
||||||
services.udev.extraRules = ''
|
services.udev.extraRules = ''
|
||||||
KERNEL=="uinput", GROUP="input", MODE="0660" OPTIONS+="static_node=uinput"
|
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
security.wrappers.sunshine = {
|
security = {
|
||||||
|
rtkit.enable = true;
|
||||||
|
wrappers.sunshine = {
|
||||||
owner = "root";
|
owner = "root";
|
||||||
group = "root";
|
group = "root";
|
||||||
capabilities = "cap_sys_admin+p";
|
capabilities = "cap_sys_admin+p";
|
||||||
source = "${pkgs.sunshine}/bin/sunshine";
|
source = "${pkgs.sunshine}/bin/sunshine";
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '/var/lib/sunshine' 0770 'user' 'users' - -"
|
||||||
|
"C '/var/lib/sunshine/sunshine.cert' 0644 'user' 'users' - ${config.clanCore.secrets.sunshine.secrets."sunshine.cert".path or ""}"
|
||||||
|
"C '/var/lib/sunshine/sunshine.key' 0644 'user' 'users' - ${config.clanCore.secrets.sunshine.secrets."sunshine.key".path or ""}"
|
||||||
|
];
|
||||||
|
|
||||||
|
hardware.opengl.enable = true;
|
||||||
|
|
||||||
systemd.user.services.sunshine = {
|
systemd.user.services.sunshine = {
|
||||||
description = "sunshine";
|
enable = true;
|
||||||
wantedBy = [ "graphical-session.target" ];
|
description = "Sunshine self-hosted game stream host for Moonlight";
|
||||||
environment = {
|
startLimitBurst = 5;
|
||||||
DISPLAY = ":0";
|
startLimitIntervalSec = 500;
|
||||||
};
|
script = "/run/current-system/sw/bin/env /run/wrappers/bin/sunshine ${sunshineConfiguration}";
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${config.security.wrapperDir}/sunshine";
|
Restart = "on-failure";
|
||||||
|
RestartSec = "5s";
|
||||||
|
ReadWritePaths = [
|
||||||
|
"/var/lib/sunshine"
|
||||||
|
];
|
||||||
|
ReadOnlyPaths = [
|
||||||
|
(config.clanCore.secrets.sunshine.secrets."sunshine.key".path or "")
|
||||||
|
(config.clanCore.secrets.sunshine.secrets."sunshine.cert".path or "")
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
wantedBy = [ "graphical-session.target" ];
|
||||||
|
partOf = [ "graphical-session.target" ];
|
||||||
|
wants = [ "graphical-session.target" ];
|
||||||
|
after = [
|
||||||
|
"sunshine-init-state.service"
|
||||||
|
"sunshine-init-credentials.service"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services.sunshine-init-state = {
|
||||||
|
enable = true;
|
||||||
|
description = "Sunshine self-hosted game stream host for Moonlight";
|
||||||
|
startLimitBurst = 5;
|
||||||
|
startLimitIntervalSec = 500;
|
||||||
|
script = ''
|
||||||
|
${ms-accept}/bin/moonlight-sunshine-accept sunshine init-state --uuid ${config.clanCore.secrets.sunshine.facts.sunshine-uuid.value or null} --state-file /var/lib/sunshine/state.json
|
||||||
|
'';
|
||||||
|
serviceConfig = {
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "5s";
|
||||||
|
Type = "oneshot";
|
||||||
|
ReadWritePaths = [
|
||||||
|
"/var/lib/sunshine"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
wantedBy = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services.sunshine-init-credentials = {
|
||||||
|
enable = true;
|
||||||
|
description = "Sunshine self-hosted game stream host for Moonlight";
|
||||||
|
startLimitBurst = 5;
|
||||||
|
startLimitIntervalSec = 500;
|
||||||
|
script = ''
|
||||||
|
${lib.getExe pkgs.sunshine} --creds sunshine sunshine
|
||||||
|
'';
|
||||||
|
serviceConfig = {
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "5s";
|
||||||
|
Type = "oneshot";
|
||||||
|
ReadWritePaths = [
|
||||||
|
"/var/lib/sunshine"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
wantedBy = [ "graphical-session.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.user.services.sunshine-listener = {
|
||||||
|
enable = true;
|
||||||
|
description = "Sunshine self-hosted game stream host for Moonlight";
|
||||||
|
startLimitBurst = 5;
|
||||||
|
startLimitIntervalSec = 500;
|
||||||
|
script = ''
|
||||||
|
${ms-accept}/bin/moonlight-sunshine-accept sunshine listen --port ${builtins.toString listenPort} --uuid ${config.clanCore.secrets.sunshine.facts.sunshine-uuid.value or null} --state /var/lib/sunshine/state.json --cert '${config.clanCore.secrets.sunshine.facts."sunshine.cert".value or null}'
|
||||||
|
'';
|
||||||
|
serviceConfig = {
|
||||||
|
# ExecStart = lib.concatStringsSep " " (lib.flatten
|
||||||
|
# [
|
||||||
|
# (lib.getExe ms-accept) "sunshine" "listen"
|
||||||
|
# "--port" (builtins.toString listenPort)
|
||||||
|
# "--uuid" (config.clanCore.secrets.sunshine.facts."sunshine-uuid".value or "")
|
||||||
|
# "--state" "/var/lib/sunshine/state.json"
|
||||||
|
# "--cert" (config.clanCore.secrets.sunshine.facts."sunshine.cert".value or "")
|
||||||
|
# ]
|
||||||
|
# );
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = 5;
|
||||||
|
ReadWritePaths = [
|
||||||
|
"/var/lib/sunshine"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
wantedBy = [ "graphical-session.target" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
# xdg.configFile."sunshine/apps.json".text = builtins.toJSON {
|
# xdg.configFile."sunshine/apps.json".text = builtins.toJSON {
|
||||||
@@ -93,17 +203,21 @@
|
|||||||
# ];
|
# ];
|
||||||
# };
|
# };
|
||||||
|
|
||||||
services = {
|
clanCore.secrets.sunshine = {
|
||||||
avahi = {
|
secrets."sunshine.key" = { };
|
||||||
enable = true;
|
secrets."sunshine.cert" = { };
|
||||||
reflector = true;
|
facts."sunshine-uuid" = { };
|
||||||
nssmdns = true;
|
facts."sunshine.cert" = { };
|
||||||
publish = {
|
generator.path = [
|
||||||
enable = true;
|
pkgs.coreutils
|
||||||
addresses = true;
|
ms-accept
|
||||||
userServices = true;
|
];
|
||||||
workstation = true;
|
generator.script = ''
|
||||||
};
|
moonlight-sunshine-accept sunshine init
|
||||||
};
|
mv credentials/cakey.pem "$secrets"/sunshine.key
|
||||||
|
cp credentials/cacert.pem "$secrets"/sunshine.cert
|
||||||
|
mv credentials/cacert.pem "$facts"/sunshine.cert
|
||||||
|
mv uuid "$facts"/sunshine-uuid
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
tea-create-pr = pkgs.callPackage ./tea-create-pr { };
|
tea-create-pr = pkgs.callPackage ./tea-create-pr { };
|
||||||
zerotier-members = pkgs.callPackage ./zerotier-members { };
|
zerotier-members = pkgs.callPackage ./zerotier-members { };
|
||||||
zt-tcp-relay = pkgs.callPackage ./zt-tcp-relay { };
|
zt-tcp-relay = pkgs.callPackage ./zt-tcp-relay { };
|
||||||
|
moonlight-sunshine-accept = pkgs.callPackage ./moonlight-sunshine-accept { };
|
||||||
merge-after-ci = pkgs.callPackage ./merge-after-ci {
|
merge-after-ci = pkgs.callPackage ./merge-after-ci {
|
||||||
inherit (config.packages) tea-create-pr;
|
inherit (config.packages) tea-create-pr;
|
||||||
};
|
};
|
||||||
|
|||||||
39
pkgs/moonlight-sunshine-accept/default.nix
Normal file
39
pkgs/moonlight-sunshine-accept/default.nix
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{ 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";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/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()
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from datetime import datetime, 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.utcnow())
|
||||||
|
.not_valid_after(datetime.utcnow() + 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() -> (str, str):
|
||||||
|
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 = os.getcwd() + "credentials"
|
||||||
|
Path(credentials_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
cacaert_path = os.path.join(credentials_path, "cacert.pem")
|
||||||
|
with open(cacaert_path, mode="wb") as file:
|
||||||
|
file.write(pem_certificate)
|
||||||
|
cakey_path = os.path.join(credentials_path, "cakey.pem")
|
||||||
|
with open(cakey_path, mode="wb") as file:
|
||||||
|
file.write(pem_private_key)
|
||||||
|
print("Finished writing moonlight credentials")
|
||||||
|
|
||||||
|
|
||||||
|
def register_initialization_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.set_defaults(func=write_credentials)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import argparse
|
||||||
|
|
||||||
|
from .state import init_state
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(file_path: str) -> str:
|
||||||
|
with open(file_path) as file:
|
||||||
|
return file.read()
|
||||||
|
|
||||||
|
|
||||||
|
def init_config(args: argparse.Namespace) -> None:
|
||||||
|
key = read_file(args.key)
|
||||||
|
certificate = read_file(args.certificate)
|
||||||
|
|
||||||
|
init_state(certificate, key)
|
||||||
|
print("Finished initializing moonlight state.")
|
||||||
|
|
||||||
|
|
||||||
|
def register_config_initialization_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument("--certificate")
|
||||||
|
parser.add_argument("--key")
|
||||||
|
parser.set_defaults(func=init_config)
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
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:
|
||||||
|
tries = 0
|
||||||
|
max_tries = 3
|
||||||
|
response = False
|
||||||
|
for tries in range(max_tries):
|
||||||
|
response = send_join_request_api(host, port)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
if send_join_request_native(host, port, cert):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 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 = {"type": "api", "pin": pin}
|
||||||
|
json_body = json.dumps(json_body)
|
||||||
|
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()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
moonlight.terminate()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
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 = {"type": "native", "uuid": uuid, "cert": encoded_cert}
|
||||||
|
json_body = json.dumps(json_body)
|
||||||
|
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)
|
||||||
|
lines = response.split("\n")
|
||||||
|
body = "\n".join(lines[2:])[2:]
|
||||||
|
print(body)
|
||||||
|
return body
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
# 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]}")
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
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)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
class MoonlightPairing:
|
||||||
|
def __init__(self) -> "MoonlightPairing":
|
||||||
|
self.process = None
|
||||||
|
self.output = ""
|
||||||
|
self.found = threading.Event()
|
||||||
|
|
||||||
|
def init_pairing(self, host: str, pin: str) -> bool:
|
||||||
|
args = ["moonlight", "pair", host, "--pin", pin]
|
||||||
|
print("Trying to pair")
|
||||||
|
try:
|
||||||
|
self.process = subprocess.Popen(
|
||||||
|
args, stderr=subprocess.PIPE, stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
print("Pairing initiated")
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self.stream_output,
|
||||||
|
args=('Latest supported GFE server: "99.99.99.99"',),
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
"Error occurred while starting the process: ", str(e), file=sys.stderr
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self, host: str) -> bool:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["moonlight", "list", "localhost", host], check=True
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def terminate(self) -> None:
|
||||||
|
if self.process:
|
||||||
|
self.process.terminate()
|
||||||
|
self.process.wait()
|
||||||
|
|
||||||
|
def stream_output(self, target_string: str) -> 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) -> None:
|
||||||
|
self.found.wait()
|
||||||
|
print("Started up.")
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from configparser import ConfigParser, DuplicateSectionError, NoOptionError
|
||||||
|
|
||||||
|
|
||||||
|
def moonlight_config_dir() -> str:
|
||||||
|
return os.path.join(
|
||||||
|
os.path.expanduser("~"), ".config", "Moonlight Game Streaming Project"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def moonlight_state_file() -> str:
|
||||||
|
return os.path.join(moonlight_config_dir(), "Moonlight.conf")
|
||||||
|
|
||||||
|
|
||||||
|
def load_state() -> ConfigParser | None:
|
||||||
|
try:
|
||||||
|
with open(moonlight_state_file()) 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")
|
||||||
|
|
||||||
|
|
||||||
|
# this must be created before moonlight is first run
|
||||||
|
def init_state(certificate: str, key: str) -> None:
|
||||||
|
print("Initializing moonlight state.")
|
||||||
|
os.makedirs(moonlight_config_dir(), exist_ok=True)
|
||||||
|
print("Initialized moonlight config directory.")
|
||||||
|
|
||||||
|
print("Writing moonlight state file.")
|
||||||
|
# write the initial bootstrap config file
|
||||||
|
with open(moonlight_state_file(), "w") as file:
|
||||||
|
config = ConfigParser()
|
||||||
|
# bytearray ojbects 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) -> bool:
|
||||||
|
with open(moonlight_state_file(), "w") as file:
|
||||||
|
data.write(file)
|
||||||
|
|
||||||
|
|
||||||
|
def add_sunshine_host_to_parser(
|
||||||
|
config: ConfigParser, hostname: str, manual_host: str, certificate: str, uuid: str
|
||||||
|
) -> bool:
|
||||||
|
try:
|
||||||
|
config.add_section("hosts")
|
||||||
|
except DuplicateSectionError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
raise FileNotFoundError("Moonlight state file not found.")
|
||||||
|
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
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def parse_moonlight_uri(uri: str) -> (str, str):
|
||||||
|
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":
|
||||||
|
raise ValueError(f"Invalid moonlight URI: {uri}")
|
||||||
|
hostname = parsed.hostname
|
||||||
|
port = parsed.port
|
||||||
|
return (hostname, port)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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")
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import base64
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def get_context() -> http.client.ssl.SSLContext:
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
|
||||||
|
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": "Basic %s" % 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()
|
||||||
|
)
|
||||||
|
user_and_pass = base64.b64encode(b"sunshine:sunshine").decode("ascii")
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Basic %s" % user_and_pass,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define the parameters
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
# Make the request
|
||||||
|
conn.request("POST", "/api/restart", params, 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"))
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, config_location: str | 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 open(config) as f:
|
||||||
|
config_string = f"[{PSEUDO_SECTION}]\n" + f.read()
|
||||||
|
print(config_string)
|
||||||
|
cls._instance.config.read_string(config_string)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def config_location(self) -> str:
|
||||||
|
return self._config_location
|
||||||
|
|
||||||
|
def default_sunshine_config_dir(self) -> str:
|
||||||
|
return os.path.join(os.path.expanduser("~"), ".config", "sunshine")
|
||||||
|
|
||||||
|
def default_sunshine_config_file(self) -> str:
|
||||||
|
return os.path.join(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")
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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))
|
||||||
|
.not_valid_after(datetime.datetime(2044, 2, 22))
|
||||||
|
.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() -> (str, str):
|
||||||
|
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()
|
||||||
|
Path("credentials").mkdir(parents=True, exist_ok=True)
|
||||||
|
with open("credentials/cacert.pem", mode="wb") as file:
|
||||||
|
file.write(pem_certificate)
|
||||||
|
with open("credentials/cakey.pem", mode="wb") as file:
|
||||||
|
file.write(pem_private_key)
|
||||||
|
print("Generating sunshine UUID")
|
||||||
|
with open("uuid", mode="w") as file:
|
||||||
|
file.write(uniqueid())
|
||||||
|
|
||||||
|
|
||||||
|
def register_initialization_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.set_defaults(func=write_credentials)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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")
|
||||||
|
body = request.split("\n")[-1]
|
||||||
|
print(body)
|
||||||
|
body = json.loads(f"{body}")
|
||||||
|
print(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":
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 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_body = {}
|
||||||
|
json_body["uuid"] = uuid
|
||||||
|
json_body["cert"] = encoded_cert
|
||||||
|
json_body["hostname"] = socket.gethostname()
|
||||||
|
json_body = json.dumps(json_body)
|
||||||
|
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)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def default_sunshine_config_dir() -> str:
|
||||||
|
return os.path.join(os.path.expanduser("~"), ".config", "sunshine")
|
||||||
|
|
||||||
|
|
||||||
|
def default_sunshine_state_file() -> str:
|
||||||
|
return os.path.join(default_sunshine_config_dir(), "sunshine_state.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_state(sunshine_state_path: str) -> dict[str, Any] | None:
|
||||||
|
sunshine_state_path = sunshine_state_path or default_sunshine_state_file()
|
||||||
|
print(f"Loading sunshine state from {sunshine_state_path}")
|
||||||
|
try:
|
||||||
|
with open(sunshine_state_path) as file:
|
||||||
|
config = file.read()
|
||||||
|
return config
|
||||||
|
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: str) -> None:
|
||||||
|
print("Initializing sunshine state.")
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
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: str) -> None:
|
||||||
|
sunshine_state_path = sunshine_state_path or default_sunshine_state_file()
|
||||||
|
with open(sunshine_state_path, "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: str, uuid: str) -> None:
|
||||||
|
print("Adding moonlight client to sunshine state.")
|
||||||
|
state = load_state(sunshine_state_path)
|
||||||
|
if state:
|
||||||
|
state = json.loads(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.")
|
||||||
9
pkgs/moonlight-sunshine-accept/pyproject.toml
Normal file
9
pkgs/moonlight-sunshine-accept/pyproject.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[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