Make the Console<->Launcher system source-built and modern now that the console is under our control and the WinXP-era pods are gone. Contract extraction (Contract/Tesla.Contract.csproj): - One multi-targeted (net48;net8.0-windows) source project for the RPC contract, replacing the vendored TeslaConsoleLaunchLib.dll and the hand-synced Tesla.Net replica in Launcher/LaunchModels_Shared.cs. Emits assembly TeslaConsoleLaunchLib. SecureConfig extraction (SecureConfig/Tesla.SecureConfig.csproj): - net48 source of the first-boot provisioning protocol (UDP beacons, OFB crypto, RSA key exchange), replacing the vendored TeslaSecureConfiguration.dll. Remove BinaryFormatter from the wire (RCE sink + the reason net6 was pinned): - Console<->Launcher RPC is now length-prefixed System.Text.Json frames (Contract/PodRpcProtocol.cs) over the unchanged OFB transport; dispatch by method name. Deleted the SerializationBinder / MethodInfoProxy machinery. - Console-local BinaryFormatter (Site config, mission replays) intentionally retained: local net48 file I/O, not the network surface. Runtime modernization: - Launcher Service + Agent: net6 -> net8, win-x86 -> win-x64 (all pods are 64-bit Win10). Kept the SHA1-default PBKDF2 (Console key-derivation compat) with SYSLIB0041 suppressed and documented. Tests: differential suite now 73 green. Added SecureConfigCompatTests (OFB ciphertext byte-identical to the vendored DLL) and PodRpcProtocolTests (JSON round-trip of every request/response shape); removed the now-obsolete BinaryFormatter byte-identity guard. Build hygiene: per-project obj dirs (Launcher/Directory.Build.props) fix a NuGet restore collision between the two Launcher projects sharing one folder. NOT runtime-verified against a live pod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
277 lines
10 KiB
C#
277 lines
10 KiB
C#
// =============================================================================
|
|
// Tesla.Contract — Console-side RPC client (net48 only)
|
|
// =============================================================================
|
|
// Opens an OFB-encrypted TCP connection to the pod (port 53290) and dispatches
|
|
// ILauncherService calls as framed JSON RpcRequest / RpcResponse pairs (see
|
|
// PodRpcProtocol.cs). Replaces the original serialized-MethodBase + BinaryFormatter
|
|
// scheme; the OFB transport (PodConfigurationServer.NegotiateCryptoStreams) is
|
|
// unchanged.
|
|
//
|
|
// Depends on Tesla.PodConfigurationServer (Tesla.SecureConfig) for the crypto
|
|
// handshake, so it is compiled for net48 only. The Launcher is the server end.
|
|
// =============================================================================
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Reflection;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
|
|
namespace Tesla.Net
|
|
{
|
|
public class PodManagerConnection : IPodManagerConnection, ILauncherService, IDisposable
|
|
{
|
|
private TcpClient mOpenClient;
|
|
|
|
private object mCommSyncRoot = new object();
|
|
|
|
protected Stream mUnderlyingOutStream;
|
|
|
|
private BufferedStream mReader;
|
|
|
|
private BufferedStream mWriter;
|
|
|
|
private IPEndPoint mEndPoint;
|
|
|
|
private byte[] mKey;
|
|
|
|
public bool IsOpen
|
|
{
|
|
get
|
|
{
|
|
if (mOpenClient == null || mOpenClient.Client == null || mWriter == null || mReader == null)
|
|
{
|
|
return false;
|
|
}
|
|
return mOpenClient.Connected;
|
|
}
|
|
}
|
|
|
|
protected object SyncRoot => mCommSyncRoot;
|
|
|
|
public float VolumeLevel
|
|
{
|
|
get => Invoke<float>("get_VolumeLevel");
|
|
set => InvokeVoid("set_VolumeLevel", value);
|
|
}
|
|
|
|
public void Open(IPEndPoint remoteEndPoint, byte[] secureKey)
|
|
{
|
|
mEndPoint = remoteEndPoint;
|
|
mKey = secureKey;
|
|
mOpenClient = new TcpClient();
|
|
mOpenClient.SendTimeout = 10000;
|
|
mOpenClient.ReceiveTimeout = 10000;
|
|
mOpenClient.Connect(remoteEndPoint);
|
|
if (!PodConfigurationServer.NegotiateCryptoStreams(mOpenClient.GetStream(), secureKey, out var outStream, out var inStream))
|
|
{
|
|
mOpenClient.Close();
|
|
throw new IOException("Error connecting to pod.");
|
|
}
|
|
mUnderlyingOutStream = outStream;
|
|
mWriter = new BufferedStream(outStream);
|
|
mReader = new BufferedStream(inStream);
|
|
}
|
|
|
|
public void Close()
|
|
{
|
|
try
|
|
{
|
|
mWriter.Close();
|
|
mReader.Close();
|
|
mOpenClient.Client.Close();
|
|
mOpenClient = null;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
public Guid InstallProduct(string filepath)
|
|
{
|
|
if (!File.Exists(filepath))
|
|
{
|
|
return Guid.Empty;
|
|
}
|
|
FileStream outOfBandData = File.OpenRead(filepath);
|
|
Guid result = Guid.Empty;
|
|
try
|
|
{
|
|
result = OutOfBandInvocation.Invoke(mEndPoint, mKey, "InitiateInstallProduct", new object[0], outOfBandData);
|
|
return result;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// ── RPC core ─────────────────────────────────────────────────────────
|
|
// Writes a framed JSON request and (optionally) reads the framed response,
|
|
// deserializing its Result into the caller's expected type.
|
|
|
|
protected object InvokeCore(string method, bool waitForResponse, Type resultType, object[] parameters)
|
|
{
|
|
try
|
|
{
|
|
lock (mCommSyncRoot)
|
|
{
|
|
PodRpc.WriteRequest(mWriter, method, parameters);
|
|
if (!waitForResponse)
|
|
{
|
|
return null;
|
|
}
|
|
RpcResponse response = PodRpc.ReadResponse(mReader);
|
|
if (response.Error != null)
|
|
{
|
|
throw new Exception("Server function threw an exception: " + response.Error);
|
|
}
|
|
if (resultType == null
|
|
|| response.Result.ValueKind == JsonValueKind.Null
|
|
|| response.Result.ValueKind == JsonValueKind.Undefined)
|
|
{
|
|
return resultType != null && resultType.IsValueType
|
|
? Activator.CreateInstance(resultType)
|
|
: null;
|
|
}
|
|
return response.Result.Deserialize(resultType, PodRpc.JsonOptions);
|
|
}
|
|
}
|
|
catch (IOException innerException)
|
|
{
|
|
throw new TeslaConsole.ConnectionLostException("Connection Lost", innerException);
|
|
}
|
|
}
|
|
|
|
protected T Invoke<T>(string method, params object[] parameters)
|
|
=> (T)InvokeCore(method, waitForResponse: true, typeof(T), parameters);
|
|
|
|
protected void InvokeVoid(string method, params object[] parameters)
|
|
=> InvokeCore(method, waitForResponse: true, null, parameters);
|
|
|
|
protected void InvokeOneWay(string method, params object[] parameters)
|
|
=> InvokeCore(method, waitForResponse: false, null, parameters);
|
|
|
|
// ── ILauncherService ─────────────────────────────────────────────────
|
|
|
|
public void ClearStore() => InvokeOneWay("ClearStore");
|
|
|
|
public DateTime Ping(DateTime now) => Invoke<DateTime>("Ping", now);
|
|
|
|
public int LaunchApp(Guid launchKey) => Invoke<int>("LaunchApp", launchKey);
|
|
|
|
public LaunchPair[] GetLaunchableApps() => Invoke<LaunchPair[]>("GetLaunchableApps");
|
|
|
|
public Guid InitiateInstallProduct() => Invoke<Guid>("InitiateInstallProduct");
|
|
|
|
public OutOfBandProgress GetOutOfBandProgress(Guid installId)
|
|
=> Invoke<OutOfBandProgress>("GetOutOfBandProgress", installId);
|
|
|
|
public void KillApp(Guid launchKey, int processId)
|
|
=> InvokeVoid("KillApp", launchKey, processId);
|
|
|
|
public void KillAllOfType(Guid launchKey) => InvokeVoid("KillAllOfType", launchKey);
|
|
|
|
public void KillAllApps() => InvokeVoid("KillAllApps");
|
|
|
|
public void Shutdown(bool doRestart) => InvokeVoid("Shutdown", doRestart);
|
|
|
|
public LaunchedAppData[] GetLaunchedApps() => Invoke<LaunchedAppData[]>("GetLaunchedApps");
|
|
|
|
public LaunchData[] GetInstalledApps() => Invoke<LaunchData[]>("GetInstalledApps");
|
|
|
|
public void RemoveApp(Guid index) => InvokeVoid("RemoveApp", index);
|
|
|
|
public void InstallApp(LaunchData data) => InvokeVoid("InstallApp", data);
|
|
|
|
public void UninstallApp(Guid launchKey) => InvokeVoid("UninstallApp", launchKey);
|
|
|
|
public FullUpdateData FullUpdate() => Invoke<FullUpdateData>("FullUpdate");
|
|
|
|
public void Dispose()
|
|
{
|
|
Close();
|
|
}
|
|
}
|
|
|
|
public class OutOfBandInvocation : PodManagerConnection
|
|
{
|
|
private Stream mOutOfBandData;
|
|
|
|
public static Guid Invoke(IPEndPoint target, byte[] key, string methodName, object[] parameters, Stream outOfBandData)
|
|
{
|
|
return Invoke(target, key, methodName, parameters, outOfBandData, null);
|
|
}
|
|
|
|
public static Guid Invoke(IPEndPoint target, byte[] key, MethodBase method, object[] parameters, Stream outofBandData)
|
|
{
|
|
return Invoke(target, key, method.Name, parameters, outofBandData, null);
|
|
}
|
|
|
|
public static Guid Invoke(IPEndPoint target, byte[] key, MethodBase method, object[] parameters, Stream outOfBandData, OutOfBandProgressChanged progressCallback)
|
|
{
|
|
return Invoke(target, key, method.Name, parameters, outOfBandData, progressCallback);
|
|
}
|
|
|
|
public static Guid Invoke(IPEndPoint target, byte[] key, string methodName, object[] parameters, Stream outOfBandData, OutOfBandProgressChanged progressCallback)
|
|
{
|
|
OutOfBandInvocation outOfBandInvocation = new OutOfBandInvocation(target, key);
|
|
// progressCallback is vestigial: install progress is polled via
|
|
// GetOutOfBandProgress, never pushed. Kept on the signature for surface parity.
|
|
_ = progressCallback;
|
|
if (outOfBandData != null)
|
|
{
|
|
outOfBandInvocation.mOutOfBandData = outOfBandData;
|
|
}
|
|
Guid result = outOfBandInvocation.Invoke<Guid>(methodName, parameters ?? new object[0]);
|
|
ThreadPool.QueueUserWorkItem(AppendOutOfBandData, outOfBandInvocation);
|
|
return result;
|
|
}
|
|
|
|
protected static void AppendOutOfBandData(object state)
|
|
{
|
|
OutOfBandInvocation outOfBandInvocation = (OutOfBandInvocation)state;
|
|
using (outOfBandInvocation)
|
|
{
|
|
try
|
|
{
|
|
outOfBandInvocation.WriteOutOfBandData();
|
|
}
|
|
catch (IOException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
protected OutOfBandInvocation(IPEndPoint target, byte[] key)
|
|
{
|
|
Open(target, key);
|
|
}
|
|
|
|
protected void WriteOutOfBandData()
|
|
{
|
|
lock (base.SyncRoot)
|
|
{
|
|
int num = 8192;
|
|
byte[] buffer = new byte[num];
|
|
long num2 = mOutOfBandData.Length;
|
|
mUnderlyingOutStream.Write(BitConverter.GetBytes(num2), 0, 8);
|
|
while (num2 > num)
|
|
{
|
|
mOutOfBandData.Read(buffer, 0, num);
|
|
mUnderlyingOutStream.Write(buffer, 0, num);
|
|
num2 -= num;
|
|
}
|
|
if (num2 > 0)
|
|
{
|
|
mOutOfBandData.Read(buffer, 0, (int)num2);
|
|
mUnderlyingOutStream.Write(buffer, 0, (int)num2);
|
|
}
|
|
mUnderlyingOutStream.Flush();
|
|
}
|
|
}
|
|
}
|
|
}
|