JJServiceManagerRelay/server/RelayHandler.cs

142 lines
5.5 KiB
C#

using System.Buffers;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Text;
namespace JJServiceManagerRelay;
/// <summary>/relay/{세션ID}?token=…&amp;side=manager|agent — 두 연결을 이어 붙여 바이트를 그대로 넘김</summary>
public sealed class RelayHandler
{
private readonly Hub _hub;
private static readonly TimeSpan PairWait = TimeSpan.FromSeconds(30);
public RelayHandler(Hub hub) => _hub = hub;
public async Task RunAsync(HttpContext ctx, string sessionId)
{
var side = ctx.Request.Query["side"].ToString();
var token = ctx.Request.Query["token"].ToString();
if (!Guid.TryParse(sessionId, out var sid) || !_hub.Sessions.TryGetValue(sid, out var s) || (side != "manager" && side != "agent"))
{
ctx.Response.StatusCode = 404;
return;
}
var expect = side == "manager" ? s.ManagerToken : s.AgentToken;
bool tokenOk = CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(token), Encoding.UTF8.GetBytes(expect));
lock (s.Lock)
{
bool used = side == "manager" ? s.ManagerUsed : s.AgentUsed;
if (!tokenOk || used || s.Finished || s.Direct)
{
ctx.Response.StatusCode = 403;
return;
}
if (side == "manager") s.ManagerUsed = true; else s.AgentUsed = true;
}
using var ws = await ctx.WebSockets.AcceptWebSocketAsync();
WebSocket other = null;
lock (s.Lock)
{
if (side == "manager") s.ManagerWs = ws; else s.AgentWs = ws;
if (s.ManagerWs != null && s.AgentWs != null && !s.Paired)
{
s.Paired = true;
other = side == "manager" ? s.AgentWs : s.ManagerWs;
}
}
if (other == null)
{
// 상대를 기다림 (상대 쪽 요청이 이어 붙이기를 맡음)
await Task.WhenAny(s.Done.Task, s.Wake.Task, Task.Delay(PairWait, ctx.RequestAborted).ContinueWith(_ => { }));
bool paired;
lock (s.Lock) paired = s.Paired;
if (!paired)
{
await CloseAsync(ws, s.Direct ? "직접 연결됨" : "상대가 연결되지 않았습니다.");
if (!s.Direct) await _hub.FinishSessionAsync(s, "failed", $"중계: {(side == "manager" ? "대상 PC" : "관리 PC")}가 30초 안에 붙지 않음");
return;
}
await s.Done.Task; // 이어 붙인 쪽이 끝낼 때까지 이 요청을 살려 둠
return;
}
// 두 번째로 붙은 쪽: 양방향으로 넘김
await _hub.Db.ConnectionModeAsync(s.Id, "relay");
var managerWs = side == "manager" ? ws : other;
var agentWs = side == "manager" ? other : ws;
_hub.Log.Info($"중계 시작: {s.Username} → {s.DeviceName} ({s.Id})");
string reason = "정상 종료";
try
{
var t1 = PumpAsync(managerWs, agentWs, n => Interlocked.Add(ref s.BytesToDevice, n));
var t2 = PumpAsync(agentWs, managerWs, n => Interlocked.Add(ref s.BytesFromDevice, n));
var first = await Task.WhenAny(t1, t2);
reason = await first;
// 한쪽이 끝나면 다른 쪽도 닫음
await CloseAsync(managerWs, reason);
await CloseAsync(agentWs, reason);
// 상대가 닫기 응답을 안 하면 2초 뒤 강제로 끊음
await Task.WhenAny(Task.WhenAll(t1, t2), Task.Delay(2000));
try { managerWs.Abort(); } catch { }
try { agentWs.Abort(); } catch { }
}
catch (Exception ex)
{
reason = "중계 오류: " + ex.Message;
}
finally
{
_hub.Log.Info($"중계 끝: {s.Username} → {s.DeviceName} ({reason}, 보냄 {s.BytesToDevice}B / 받음 {s.BytesFromDevice}B)");
await _hub.FinishSessionAsync(s, "relay", reason);
}
}
/// <summary>from에서 받은 프레임을 to로 그대로 보냄. 끝난 사유를 돌려줌</summary>
private static async Task<string> PumpAsync(WebSocket from, WebSocket to, Action<long> count)
{
var buf = ArrayPool<byte>.Shared.Rent(64 * 1024);
try
{
while (true)
{
var r = await from.ReceiveAsync(buf, CancellationToken.None);
if (r.MessageType == WebSocketMessageType.Close) return "상대가 연결을 닫음";
count(r.Count);
await to.SendAsync(new ArraySegment<byte>(buf, 0, r.Count), r.MessageType, r.EndOfMessage, CancellationToken.None);
}
}
catch (WebSocketException)
{
return "연결 끊김";
}
catch (ObjectDisposedException)
{
return "연결 끊김";
}
catch (OperationCanceledException)
{
return "연결 끊김";
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
private static async Task CloseAsync(WebSocket ws, string reason)
{
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
var b = reason ?? "";
while (Encoding.UTF8.GetByteCount(b) > 120) b = b[..^1];
if (ws.State == WebSocketState.Open || ws.State == WebSocketState.CloseReceived)
await ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, b, cts.Token);
}
catch { }
}
}