using System.Text; using System.Text.Json; using Fleet.Core; using MoShiro.Protocol; namespace MoShiro.Measurement; /// /// 节点进程和宿主机更新器之间的文件交换(容器里 /etc/moshiro,也就是数据目录): /// 节点把面板发来的清单和签名写进 update/,把「更新」请求写成 update/apply.request;更新器验签并更新,状态由宿主更新器写在只读挂载的 update-status/status.json。 /// 节点进程不执行任何命令,也不信任清单的内容(验签在宿主机上)。没有安装更新器时目录写不了或没人读,调用只是返回 false。 /// public sealed class UpdateBridge { private const int MaxManifestBytes = 4096; private const int MaxSignatureBytes = 512; private const int MaxStatusBytes = 4096; private readonly string _updateDirectory; private readonly string _statusFile; private readonly Action _write; private readonly INodeLog _log; /// 写数据目录里的文件(相对路径,原子替换):由节点传入 ConfigStore.Write——常驻进程里只有它可以写文件。 public UpdateBridge(string dataDirectory, Action write, INodeLog log) { _updateDirectory = Path.Combine(dataDirectory, "update"); _statusFile = Path.Combine(dataDirectory, "update-status", "status.json"); _write = write; _log = log; } /// 放下面板发来的清单和签名(和已有的一样就不动,免得每次重连都惊动更新器)。先写清单、后写签名:更新器监视的是签名文件。 public bool WriteOffer(UpdateOfferParams offer) { byte[] manifest = Encoding.UTF8.GetBytes(offer.Manifest); byte[] signature = Encoding.UTF8.GetBytes(offer.Signature); if (manifest.Length is 0 or > MaxManifestBytes || signature.Length is 0 or > MaxSignatureBytes) { _log.Warn("ignoring an update offer of an unexpected size"); return false; } try { string manifestPath = Path.Combine(_updateDirectory, "offer.manifest"); string signaturePath = Path.Combine(_updateDirectory, "offer.sig"); if (Same(manifestPath, manifest) && Same(signaturePath, signature)) { return true; } _write("update/offer.manifest", manifest); _write("update/offer.sig", signature); _log.Info("update offer received"); return true; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { _log.Warn("could not store the update offer (is the updater installed?)", ("err", ex.Message)); return false; } } /// 请求更新:写一个空的 apply.request(更新器验签通过、清单更新才会真的动手)。 public bool RequestApply() { try { _write("update/apply.request", []); _log.Info("update requested"); return true; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { _log.Warn("could not request the update (is the updater installed?)", ("err", ex.Message)); return false; } } /// 更新器的状态;没有安装更新器、文件不存在或内容不对时返回 null。 public UpdateStatus? ReadStatus() { try { if (!File.Exists(_statusFile) || new FileInfo(_statusFile).Length > MaxStatusBytes) { return null; } return JsonSerializer.Deserialize(File.ReadAllBytes(_statusFile), MoShiroJsonContext.Default.UpdateStatus); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException) { return null; } } /// /// 握手时自述的二进制:正在运行的可执行文件的哈希,加上宿主机更新器验过的、安装这个版本时用的 CI 签名清单(更新器写在只读的 update-status/)。 /// 容器节点以外(没有更新器)没有清单,只有哈希;读不到可执行文件时返回 null(不做绑定)。 /// public BinaryAttestation? Attest() { if (NodeBinary.Sha256Hex is not { } hash) { return null; } string? manifest = ReadSmall("installed.manifest", MaxManifestBytes); string? signature = ReadSmall("installed.sig", MaxSignatureBytes); return manifest is not null && signature is not null ? new BinaryAttestation(hash, manifest, signature) : new BinaryAttestation(hash, null, null); } private string? ReadSmall(string name, int maxBytes) { try { string path = Path.Combine(Path.GetDirectoryName(_statusFile)!, name); return File.Exists(path) && new FileInfo(path).Length <= maxBytes ? File.ReadAllText(path, Encoding.UTF8) : null; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { return null; } } private static bool Same(string path, byte[] content) => File.Exists(path) && File.ReadAllBytes(path).AsSpan().SequenceEqual(content); }