130 lines
5.4 KiB
C#
130 lines
5.4 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Fleet.Core;
|
|
using MoShiro.Protocol;
|
|
|
|
namespace MoShiro.Measurement;
|
|
|
|
/// <summary>
|
|
/// 节点进程和宿主机更新器之间的文件交换(容器里 <c>/etc/moshiro</c>,也就是数据目录):
|
|
/// 节点把面板发来的清单和签名写进 <c>update/</c>,把「更新」请求写成 <c>update/apply.request</c>;更新器验签并更新,状态由宿主更新器写在只读挂载的 <c>update-status/status.json</c>。
|
|
/// 节点进程不执行任何命令,也不信任清单的内容(验签在宿主机上)。没有安装更新器时目录写不了或没人读,调用只是返回 false。
|
|
/// </summary>
|
|
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<string, byte[]> _write;
|
|
private readonly INodeLog _log;
|
|
|
|
/// <param name="write">写数据目录里的文件(相对路径,原子替换):由节点传入 <c>ConfigStore.Write</c>——常驻进程里只有它可以写文件。</param>
|
|
public UpdateBridge(string dataDirectory, Action<string, byte[]> write, INodeLog log)
|
|
{
|
|
_updateDirectory = Path.Combine(dataDirectory, "update");
|
|
_statusFile = Path.Combine(dataDirectory, "update-status", "status.json");
|
|
_write = write;
|
|
_log = log;
|
|
}
|
|
|
|
/// <summary>放下面板发来的清单和签名(和已有的一样就不动,免得每次重连都惊动更新器)。先写清单、后写签名:更新器监视的是签名文件。</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>请求更新:写一个空的 <c>apply.request</c>(更新器验签通过、清单更新才会真的动手)。</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>更新器的状态;没有安装更新器、文件不存在或内容不对时返回 null。</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 握手时自述的二进制:正在运行的可执行文件的哈希,加上宿主机更新器验过的、安装这个版本时用的 CI 签名清单(更新器写在只读的 <c>update-status/</c>)。
|
|
/// 容器节点以外(没有更新器)没有清单,只有哈希;读不到可执行文件时返回 null(不做绑定)。
|
|
/// </summary>
|
|
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);
|
|
}
|