Files
Migu2.0/MiGu.Server/Controllers/OtaReceiveController.cs
T
wei.wu 1f72488a8d 新增车辆表及OTA权限与逻辑优化
新增车辆任务与报警表,完善实体与DbContext配置。细化OTA权限校验,增强回传会话IP安全。优化OTA上传与设置面板,调度器支持重启恢复。报警采集逻辑支持历史分段。
2026-07-27 15:12:14 +08:00

122 lines
4.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MiGu.Server.Ota;
namespace MiGu.Server.Controllers;
/// <summary>
/// WatchDog 回传包接收端。
/// WatchDog 写死 POST 到 http://{config.serverIP}:8000/upload-mdcs/{routeKey}
/// 必须与参考 Electron Express :8000 路径一致;/api/ota/receive/* 仅作兼容别名。
/// 会话校验使用 TCP 对端 IP(见 Program 中 TcpRemoteIp),忽略可伪造的 X-Forwarded-For。
/// </summary>
[ApiController]
[AllowAnonymous]
public class OtaReceiveController : ControllerBase
{
public const string TcpRemoteIpItemKey = "TcpRemoteIp";
private readonly OtaStore _store;
private readonly ILogger<OtaReceiveController> _log;
public OtaReceiveController(OtaStore store, ILogger<OtaReceiveController> log)
{
_store = store;
_log = log;
}
[HttpGet("/hello")]
[HttpGet("/api/ota/receive/hello")]
public IActionResult Hello() => Ok("ok");
/// <summary>WatchDog 官方路径:/upload-mdcs/{Medullaexe|...}</summary>
[HttpPost("/upload-mdcs/{routeKey}")]
[HttpPost("/api/ota/receive/upload-mdcs/{routeKey}")]
[HttpPost("/api/ota/receive/upload-mdcs{routeKey}")]
[RequestSizeLimit(512_000_000)]
public Task<IActionResult> UploadMdcs(string routeKey, CancellationToken ct)
=> SaveAsync(routeKey, ct);
[HttpPost("/upload-history/{routeKey}")]
[HttpPost("/api/ota/receive/upload-history/{routeKey}")]
[HttpPost("/api/ota/receive/upload-history{routeKey}")]
[RequestSizeLimit(512_000_000)]
public async Task<IActionResult> UploadHistory(string routeKey, CancellationToken ct)
{
var ip = ResolveTcpRemoteIp();
if (!_store.TryGetActivePullId(ip, out _))
{
_log.LogWarning("OTA history rejected without active pull session from {Ip}", ip ?? "unknown");
return BadRequest("no active pull session");
}
var day = DateTime.Now.ToString("yyyy-MM-dd");
var dir = Path.Combine(_store.HistoryDir, day, SafeFileName(ip ?? "unknown", "unknown"));
Directory.CreateDirectory(dir);
var file = await ReadFirstFileAsync(ct);
if (file == null || file.Length == 0) return BadRequest("empty");
// 净化文件名:routeKey / FileName 都可能含路径分隔符,必须 GetFileName 防穿越。
var rawName = string.IsNullOrWhiteSpace(file.FileName) ? routeKey : file.FileName;
var safeName = SafeFileName(rawName, "unnamed");
var path = Path.Combine(dir, safeName);
await using var fs = System.IO.File.Create(path);
await file.CopyToAsync(fs, ct);
_store.NotePullReceive(ip);
_log.LogInformation("OTA history receive {Route} -> {Path} ({Len})", routeKey, path, file.Length);
return Ok(new { ok = true });
}
private async Task<IActionResult> SaveAsync(string routeKey, CancellationToken ct)
{
try
{
var clientIp = ResolveTcpRemoteIp();
if (!_store.TryGetActivePullId(clientIp, out _))
{
_log.LogWarning("OTA mdcs rejected without active pull session from {Ip}", clientIp ?? "unknown");
return BadRequest("no active pull session");
}
var file = await ReadFirstFileAsync(ct);
if (file == null || file.Length == 0) return BadRequest("empty file");
var dest = _store.ResolveReceivePath(routeKey, clientIp);
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
await using (var fs = System.IO.File.Create(dest))
await file.CopyToAsync(fs, ct);
_store.NotePullReceive(clientIp);
_log.LogInformation("OTA mdcs receive {Route} -> {Dest} ({Len})", routeKey, dest, file.Length);
return Ok(new { ok = true });
}
catch (Exception ex)
{
_log.LogWarning(ex, "OTA receive failed {Route}", routeKey);
return BadRequest(ex.Message);
}
}
/// <summary>优先取 ForwardedHeaders 之前写入的 TCP 对端 IP,避免 X-Forwarded-For 投毒。</summary>
private string? ResolveTcpRemoteIp()
{
if (HttpContext.Items.TryGetValue(TcpRemoteIpItemKey, out var boxed) && boxed is string s && !string.IsNullOrWhiteSpace(s))
return s;
return HttpContext.Connection.RemoteIpAddress?.ToString();
}
private async Task<IFormFile?> ReadFirstFileAsync(CancellationToken ct)
{
if (!Request.HasFormContentType) return null;
var form = await Request.ReadFormAsync(ct);
return form.Files.FirstOrDefault();
}
private static string SafeFileName(string raw, string fallback)
{
var safe = Path.GetFileName(raw);
if (string.IsNullOrWhiteSpace(safe)) safe = fallback;
foreach (var ch in Path.GetInvalidFileNameChars())
safe = safe.Replace(ch, '_');
return string.IsNullOrWhiteSpace(safe) ? fallback : safe;
}
}