// ============================================================================= // Tesla.Contract — Pod RPC protocol (Console <-> Launcher Service) // ============================================================================= // The framed, JSON-based replacement for the old OFB-encrypted BinaryFormatter // RPC. Runs ON TOP of the same OFB-encrypted stream (NegotiateCryptoStreams is // unchanged); only the per-message serialization changed. // // Wire format, after the OFB/CONF handshake: // request : [4-byte little-endian length][UTF-8 JSON RpcRequest] // response: [4-byte little-endian length][UTF-8 JSON RpcResponse] // The InitiateInstallProduct out-of-band file transfer (8-byte length + raw // bytes) is unchanged and still follows its response frame on the same stream. // // Dispatch is by method NAME (RpcRequest.Method) — the old serialized-MethodBase // + SerializationBinder + MethodInfoProxy machinery is gone. Both ends share this // one file, so the request/response shape cannot drift. // // Serializer: Newtonsoft.Json — System.Text.Json has no net40 target, and since // XP11 the whole suite (Console, Launcher, vPOD) is net40. The protocol briefly // had an STJ leg for the net48 Console era; it wrote shape-identical JSON // (PascalCase member names, fields included, Guids as strings, ISO-8601 dates), // so anything that captured wire traffic then still matches what this writes. // ============================================================================= using System; using System.Collections.Generic; using System.IO; using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Tesla.Net { /// One RPC call: a method name plus its arguments as JSON tokens. public sealed class RpcRequest { public string Method { get; set; } public List Args { get; set; } } /// One RPC result: the return value as JSON, or an error message. public sealed class RpcResponse { public JToken Result { get; set; } // JSON null for void / null public string Error { get; set; } // null on success } /// Framing + (de)serialization shared by the Console client and the /// Launcher Service. Synchronous to match the existing stream usage. public static class PodRpc { /// Upper bound on a single JSON frame (the bulk install payload is /// streamed out-of-band, not framed), guarding against hostile lengths. public const int MaxFrameBytes = 16 * 1024 * 1024; // Newtonsoft serializes public fields of the wire types by default, and // writes ISO-8601 dates / string Guids — no special options needed. public static readonly JsonSerializer JsonOptions = JsonSerializer.CreateDefault(); // ── Framing ────────────────────────────────────────────────────────── public static void WriteFrame(Stream stream, byte[] payload) { if (payload.Length > MaxFrameBytes) throw new IOException($"RPC frame too large ({payload.Length} bytes)."); var len = BitConverter.GetBytes(payload.Length); // little-endian on x86/x64 stream.Write(len, 0, 4); stream.Write(payload, 0, payload.Length); stream.Flush(); } public static byte[] ReadFrame(Stream stream) { var lenBuf = ReadExact(stream, 4); int len = BitConverter.ToInt32(lenBuf, 0); if (len < 0 || len > MaxFrameBytes) throw new IOException($"RPC frame length out of range ({len})."); return ReadExact(stream, len); } private static byte[] ReadExact(Stream stream, int count) { var buf = new byte[count]; int off = 0; while (off < count) { int n = stream.Read(buf, off, count - off); if (n == 0) throw new EndOfStreamException("Connection closed mid-frame."); off += n; } return buf; } // ── Request ────────────────────────────────────────────────────────── public static void WriteRequest(Stream stream, string method, object[] args) { var req = new RpcRequest { Method = method, Args = new List() }; if (args != null) foreach (var a in args) req.Args.Add(a == null ? JValue.CreateNull() : JToken.FromObject(a, JsonOptions)); WriteFrame(stream, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req))); } public static RpcRequest ReadRequest(Stream stream) => JsonConvert.DeserializeObject( Encoding.UTF8.GetString(ReadFrame(stream)), ReadSettings); // Keep date-looking strings as raw strings so an echoed argument (Ping) // goes back byte-identical instead of reformatted through DateTime. private static readonly JsonSerializerSettings ReadSettings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }; // ── Response ───────────────────────────────────────────────────────── public static void WriteResponse(Stream stream, object result, string error) { object payload = error == null ? result : null; var resp = new RpcResponse { Result = payload == null ? JValue.CreateNull() : JToken.FromObject(payload, JsonOptions), Error = error, }; WriteFrame(stream, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(resp))); } public static RpcResponse ReadResponse(Stream stream) => JsonConvert.DeserializeObject( Encoding.UTF8.GetString(ReadFrame(stream)), ReadSettings); } }