覆盖包库回传、任务下发、CDM 任务同步与报警采集,并为包/任务 ID 与上传文件名加上路径安全校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
4.4 KiB
C#
109 lines
4.4 KiB
C#
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/* 仅作兼容别名。
|
||
/// </summary>
|
||
[ApiController]
|
||
[AllowAnonymous]
|
||
public class OtaReceiveController : ControllerBase
|
||
{
|
||
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 = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||
if (!_store.TryGetActivePullId(ip, out _))
|
||
{
|
||
_log.LogWarning("OTA history rejected without active pull session from {Ip}", ip);
|
||
return BadRequest("no active pull session");
|
||
}
|
||
|
||
var day = DateTime.Now.ToString("yyyy-MM-dd");
|
||
var dir = Path.Combine(_store.HistoryDir, day, SafeFileName(ip, "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);
|
||
_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 = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||
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);
|
||
_log.LogInformation("OTA mdcs receive {Route} -> {Dest} ({Len})", routeKey, dest, file.Length);
|
||
return Ok(new { ok = true, path = dest });
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.LogWarning(ex, "OTA receive failed {Route}", routeKey);
|
||
return BadRequest(ex.Message);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|