using System.Security.Cryptography;
using System.Text;
namespace EarthQuake.Core.EarthQuakes.P2PQuake.Client;
public class P2PEventArgs(string message) : EventArgs
{
public string Message { get; } = message.Replace("\r\n", "");
public bool IsPeerMessage = false;
}
public class P2PException(string message) : Exception(message);
public class P2PClient : TcpSocket
{
///
/// 接続可能なP2PサーバーのURL
///
public string[] Urls { get; init; } = ["p2pquake.info", "www.p2pquake.net", "p2pquake.xyz", "p2pquake.ddo.jp"];
private bool Connected { get; set; }
private const string ProtocolVersion = "0.36";
private const float SupportedServerVersion = 0.30f;
public ushort Port { get; init; } = 6911;
public short AreaCode { get; init; } = 900;
private int MaxServerConnection
{
get => Server.MaxConnection;
set => Server.MaxConnection = value;
}
public static string ClientVersion => ProtocolVersion + ":OkayuEQ:Alpha0";
public static string Format => "yyyy/MM/dd HH-mm-ss";
internal readonly Dictionary PeersConnected = [];
private TimeSpan protocolTimeSpan;
private readonly Dictionary> _buffer = [];
private readonly CancellationTokenSource cts = new();
public IReadOnlyDictionary> BufferPair => _buffer;
public readonly P2PServer Server;
public DateTime ProtocolTime => DateTime.Now + protocolTimeSpan;
public int PeersCount { private set; get; }
public int PeersConnectedCount => PeersConnected.Count(x => x.Key.ClientConnected);
public int PeerId { get; private set; }
private readonly Random random = new();
private QuakeKeys? keys;
protected P2PClient()
{
Server = new P2PServer(this);
Server.OnMessageReceived += OnReceived;
Server.OnMessageSent += OnSent;
keys = QuakeKeys.LoadFile();
}
/// P2P地震情報ネットワークに接続します。
/// 正常に接続できた場合はtrue、それ以外はfalseを返します。
/// 接続に失敗した場合に発生します。
public async Task ConnectAsync()
{
if (Connected) return true;
var url = Urls[random.Next(0, Urls.Length)];
try
{
if (!Connect(url, 6910)) throw new P2PException("Connection timeout.");
var status = WaitingFor.Connect;
while (ClientConnected)
{
Response response = new(await Read());
if (!IsCorrectStatus(status, response.Code, out var exp))
{
throw new P2PException($"Invalid status code: {response.Code} expected: {exp}");
}
switch (response.Code)
{
case 211:
// クライアントのバーションを送信
await Send("131 1 " + ClientVersion);
status = WaitingFor.ServerVersion;
break;
case 212:
// サーバーのバージョンを確認
if (IsUnSupportedVersion(response.Body?[0]))
{
await Send("192 1");
throw new P2PException("Server version is older than supported version. Aborted.");
}
await Send("113 1");
status = WaitingFor.PeerId;
break;
case 233:
// ピアIDを割り当て
if (response.Body is null)
throw new P2PException("Attempted to obtain a peer ID, but no response recieved.");
PeerId = int.Parse(response.Body[0]);
await Send($"114 1 {PeerId}:{Port}");
status = WaitingFor.PortCheck;
break;
case 234:
if (response.Body?[0] != "1")
MaxServerConnection = 0;
// ポート開放チェック
await Send($"115 1 {PeerId}");
status = WaitingFor.PeerConnection;
break;
case 235:
// ピアに接続
if (response.Body is null)
throw new P2PException("Cannot obtain peers. Please try reconnecting.");
foreach (var peerData in response.Body)
{
var args = peerData.Split(',');
P2Peer peer = new(args[0], ushort.Parse(args[1]), this) { PeerId = int.Parse(args[2]) };
((TcpSocket)peer).OnMessageReceived += OnReceived;
peer.OnMessageSent += OnSent;
var result = peer.Connect();
await Console.Out.WriteLineAsync(
$"Attempt connecting to peer #{peer.PeerId}. connected: {result}");
var task = Task.Run(peer.GetDataAsync, cts.Token);
if (result)
{
PeersConnected.Add(peer, task);
}
if (PeersConnected.Count >= 5) // 接続できたピアが5以上になったら中断
{
break;
}
}
if (PeersConnected.Count == 0)
{
throw new P2PException("Cannot connect to any peer.");
}
await Send(
$"155 1 {string.Join(':', PeersConnected.Select(x => x.Key.PeerId))}"); // 接続したピアを通知
await Task.Delay(100);
await Send(
$"116 1 {PeerId}:{6911}:{901}:{PeersConnected.Count}:{MaxServerConnection}"); // ピア本割当に移行
status = WaitingFor.AllocatePeer;
break;
case 236:
// ピアの本割当
if (response.Body is null)
throw new P2PException(
"Couldn't obtain this peer to P2P network. Please try reconnecting later.");
PeersCount = int.Parse(response.Body[0]);
await Send($"117 1 {PeerId}");
status = WaitingFor.AllocateKeys;
break;
case 237:
case 295:
// 鍵を取得
keys = QuakeKeys.Create(response);
keys?.SaveFile();
await Send("127 1");
status = WaitingFor.AreaPeers;
break;
case 247:
// 各地域のピア数を取得
// TODO: 地域ピア数をParse
await Send("118 1");
status = WaitingFor.ProtocolTime;
break;
case 238:
// プロトコル時間を取得
if (response.Body is null || response.RawBody is null)
throw new P2PException($"Cannot to read protocol time.");
protocolTimeSpan = DateTime.ParseExact(response.RawBody, Format, null) - DateTime.Now;
status = WaitingFor.DisconnectServer;
await Send("119 1");
break;
case 239:
// サーバーを切断する
Close();
break;
}
}
Connected = true;
return true;
}
catch (Exception ex)
{
OnError(ex);
return false;
}
finally
{
Close();
}
}
public async Task Broadcast(string message, int excludePeer)
{
foreach (var item in PeersConnected.Keys.Where(item => item.PeerId != excludePeer))
{
await item.Send(message);
}
foreach (var item in Server.ConnectedPeers.Where(item => item.PeerId != excludePeer))
{
await item.Send(message);
}
}
public async Task Broadcast(string message)
{
foreach (var item in PeersConnected.Keys)
{
await item.Send(message);
}
foreach (var item in Server.ConnectedPeers)
{
await item.Send(message);
}
}
public static bool IsUnSupportedVersion(string? recievedVersion) =>
(recievedVersion is null ? 0 : float.Parse(recievedVersion)) < SupportedServerVersion;
///
/// 接続を続行するためにサーバーにエコーを行います。
///
/// 正常にエコー操作を行えた場合はtrue、それ以外はfalseを返します。
///
public async Task EchoAsync()
{
if (!Connected) return true;
var url = Urls[random.Next(0, Urls.Length)];
try
{
if (!Connect(url, 6910)) throw new P2PException("Connection timeout.");
var status = WaitingFor.Connect;
while (ClientConnected)
{
Response response = new(await Read());
if (!IsCorrectStatus(status, response.Code, out var exp))
{
throw new P2PException($"Invalid status code: {response.Code} expected: {exp}");
}
switch (response.Code)
{
case 211:
// クライアントのバーションを送信
await Send("131 1 " + ClientVersion);
status = WaitingFor.ServerVersion;
break;
case 212:
// サーバーのバージョンを確認
if (IsUnSupportedVersion(response.Body?[0]))
{
await Send("192 1");
throw new P2PException("Server version is older than supported version. Aborted.");
}
await Send($"123 1 {PeerId}:{PeersConnectedCount}");
status = WaitingFor.PeerId;
break;
case 243:
// エコーを行う。
if (keys is null || (DateTime.Now - keys.InvalidationDate).TotalMinutes < 30)
{
await Send($"124 1 {PeerId}:{keys?.PrivateKey ?? "Unknown"}");
status = WaitingFor.ReallocateKeys;
}
else if (PeersConnectedCount <= 3)
{
await Send($"115 1 {PeerId}");
status = WaitingFor.PeerConnection;
}
else
{
await Send("118 1");
status = WaitingFor.ProtocolTime;
}
break;
case 244:
case 295:
// 鍵を割当
keys = QuakeKeys.Create(response);
keys?.SaveFile();
if (PeersConnectedCount <= 3)
{
await Send($"115 1 {PeerId}");
status = WaitingFor.PeerConnection;
}
else
{
await Send("118 1");
status = WaitingFor.ProtocolTime;
}
break;
case 235:
// ピアに接続
if (response.Body is null) throw new P2PException("Cannot obtain new peers.");
Dictionary newPeers = [];
foreach (var peerData in response.Body)
{
var args = peerData.Split(',');
P2Peer peer = new(args[0], ushort.Parse(args[1]), this) { PeerId = int.Parse(args[2]) };
((TcpSocket)peer).OnMessageReceived += OnReceived;
peer.OnMessageSent += OnSent;
var result = peer.Connect();
await Console.Out.WriteLineAsync(
$"Attempt connecting to peer #{peer.PeerId}. connected: {result}");
var task = Task.Run(peer.GetDataAsync);
if (result)
{
newPeers.Add(peer, task);
}
if (newPeers.Count >= 5) // 接続できたピアが5以上になったら中断
{
break;
}
}
await Send($"155 1 {string.Join(':', newPeers.Select(x => x.Key.PeerId))}"); // 接続したピアを通知
status = WaitingFor.ProtocolTime;
await Task.Delay(100);
await Send("118 1");
status = WaitingFor.ProtocolTime;
break;
case 238:
// プロトコル時間を取得
if (response.Body is null || response.RawBody is null)
throw new P2PException($"Cannot to read protocol time.");
protocolTimeSpan = DateTime.ParseExact(response.RawBody, Format, null) - DateTime.Now;
status = WaitingFor.DisconnectServer;
await Send("119 1");
break;
case 239:
// サーバーを切断する
Close();
break;
}
}
Connected = true;
return true;
}
catch (Exception ex)
{
OnError(ex);
return false;
}
finally
{
Close();
}
}
///
/// P2P地震情報ネットワークへの接続を終了する操作を行います。
///
///
public async Task DisconnectAsync()
{
if (!Connected) return true;
bool b;
do
{
var url = Urls[random.Next(0, Urls.Length)];
b = Connect(url, 6910);
} while (!b);
foreach (var peer in PeersConnected) // ピアクライアントの接続をすべて切る
{
peer.Key.Close();
}
await cts.CancelAsync();
PeersConnected.Clear();
Server.Close(); // ピアサーバーを閉じる
var status = WaitingFor.Connect;
var open = true;
while (open)
{
Response response = new(await Read());
if (!IsCorrectStatus(status, response.Code, out var exp))
{
throw new P2PException($"Invalid status code: {response.Code} expected: {exp}");
}
switch (response.Code)
{
case 211:
// クライアントのバーションを送信
await Send("131 1 " + ClientVersion);
status = WaitingFor.ServerVersion;
break;
case 212:
// サーバーのバージョンを確認
if (IsUnSupportedVersion(response.Body?[0]))
{
await Send("192 1");
throw new P2PException("Server version is older than supported version. Aborted.");
}
await Send($"128 1 {PeerId}:{keys?.PrivateKey ?? "Unknown"}"); // 鍵とピアIDを廃棄する
status = WaitingFor.FinalizeServer;
break;
case 248:
// 接続終了が完了する
await Send("119 1");
status = WaitingFor.DisconnectServer;
break;
case 239:
// サーバーを切断する
Close();
open = false;
break;
default:
throw new P2PException($"なんか知らんけどバグったンゴwwwww多分こいつのせいンゴwww{response.Code}:{response.RawBody}");
}
}
Connected = false;
return true;
}
///
/// ピアサーバーを開いて接続を受け付けます。
///
public async Task OpenServer()
{
try
{
await Server.Open(Port);
}
catch (Exception e)
{
OnError(e);
}
}
///
/// すべての接続が終了するまで待機します。
///
public async Task WaitForClose() => await Task.WhenAll(PeersConnected.Values);
public string CreateUserQuake()
{
if (keys is null) return string.Empty;
var data = $"{random.NextInt64(0, long.MaxValue)},{AreaCode}";
var validation = (DateTime.Now + protocolTimeSpan).ToString(Format);
var bytes = Encoding.ASCII.GetBytes(validation)
.Concat(MD5.HashData(BufferedNetworkStream.ShiftGis.GetBytes(data))).ToArray();
return
$"555 1 {keys.Generate(bytes)}:{validation}:{keys.PublicKey}:{keys.Signature}:{keys.InvalidationDate.ToString(Format)}:{data}";
// 「データ署名」「有効期限」「公開鍵」「鍵署名」「鍵期限」「感知情報データ」
}
private enum WaitingFor : ushort
{
Connect = 1,
ServerVersion,
PeerId,
PortCheck,
PeerConnection,
AllocatePeer,
AllocateKeys,
AreaPeers,
ProtocolTime,
DisconnectServer,
EchoServer,
ReallocateKeys,
FinalizeServer,
}
private static bool IsCorrectStatus(WaitingFor status, int code, out int expected)
{
expected = status switch
{
WaitingFor.Connect => 211,
WaitingFor.ServerVersion => 212,
WaitingFor.PeerId => 233,
WaitingFor.PortCheck => 234,
WaitingFor.PeerConnection => 235,
WaitingFor.AllocatePeer => 236,
WaitingFor.AllocateKeys => 237,
WaitingFor.AreaPeers => 247,
WaitingFor.ProtocolTime => 238,
WaitingFor.DisconnectServer => 239,
WaitingFor.EchoServer => 243,
WaitingFor.ReallocateKeys => 244,
WaitingFor.FinalizeServer => 248,
_ => 291
};
if (status is WaitingFor.AllocateKeys or WaitingFor.ReallocateKeys && code == 295)
{
return true; // 鍵がすでに割り当て済みの場合は操作を続行とする
}
return code == expected;
}
public void CheckBufferExpiration()
{
var now = DateTime.Now;
foreach (var buffer in _buffer.Where(buffer => (now - buffer.Value.Value).TotalSeconds > 60))
{
_buffer.Remove(buffer.Key);
}
}
internal void AddBuffer(Response resp, IPeerConnection sender)
{
if (resp.Body is null) return;
switch (resp.Code)
{
case 551:
case 552:
case 561:
case 555:
_buffer.Add(resp.Body[0], KeyValuePair.Create(sender.PeerId, DateTime.Now));
break;
case 615:
_buffer.Add(string.Join(':', resp.Body[0..1]), KeyValuePair.Create(sender.PeerId, DateTime.Now));
break;
case 635:
return;
default:
if (resp.RawBody is not null)
{
_buffer.Add(resp.RawBody, KeyValuePair.Create(sender.PeerId, DateTime.Now));
}
break;
}
}
internal bool IncludesBuffer(Response resp)
{
if (resp.Body is null) return false;
return resp.Code switch
{
551 or 552 or 561 or 555 => _buffer.ContainsKey(resp.Body[0]),
615 or 635 => _buffer.ContainsKey(string.Join(':', resp.Body[0..1])),
_ => resp.RawBody is not null && _buffer.ContainsKey(resp.RawBody)
};
}
}