?
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/recruit.log
|
/recruit.log
|
||||||
/Folder.DotSettings.user
|
/Folder.DotSettings.user
|
||||||
|
/bin/
|
||||||
|
/obj
|
||||||
|
|||||||
15
.idea/.idea.CatLink.dir/.idea/.gitignore
generated
vendored
Normal file
15
.idea/.idea.CatLink.dir/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Rider 忽略的文件
|
||||||
|
/modules.xml
|
||||||
|
/.idea.CatLink.iml
|
||||||
|
/contentModel.xml
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
4
.idea/.idea.CatLink.dir/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.CatLink.dir/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
8
.idea/.idea.CatLink.dir/.idea/indexLayout.xml
generated
Normal file
8
.idea/.idea.CatLink.dir/.idea/indexLayout.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders />
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.CatLink.dir/.idea/vcs.xml
generated
Normal file
6
.idea/.idea.CatLink.dir/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
30
Api/DebugController.cs
Normal file
30
Api/DebugController.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CatLink.Api
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("debug")]
|
||||||
|
public class DebugController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<DebugController> _logger;
|
||||||
|
|
||||||
|
public DebugController(ILogger<DebugController> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult GetDebugInfo()
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
uptime = Environment.TickCount64,
|
||||||
|
os = Environment.OSVersion.ToString(),
|
||||||
|
processorCount = Environment.ProcessorCount,
|
||||||
|
workingSet = Environment.WorkingSet,
|
||||||
|
gcMemory = GC.GetTotalMemory(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
189
Api/FutariLobby.cs
Normal file
189
Api/FutariLobby.cs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using CatLink.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CatLink.Api
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("recruit")]
|
||||||
|
public class FutariLobby : ControllerBase
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<uint, RecruitRecord> _recruits = new();
|
||||||
|
private static readonly object _recruitsLock = new();
|
||||||
|
private static readonly string _logFilePath = "recruit.log";
|
||||||
|
private readonly ILogger<FutariLobby> _logger;
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public FutariLobby(ILogger<FutariLobby> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("start")]
|
||||||
|
public IActionResult StartRecruit([FromBody] JsonElement json)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var record = JsonSerializer.Deserialize<RecruitRecord>(json.GetRawText(), _jsonOptions);
|
||||||
|
if (record == null)
|
||||||
|
{
|
||||||
|
throw new ApiException(400, "Invalid request body");
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
var key = record.RecruitInfo.MechaInfo.IpAddress;
|
||||||
|
|
||||||
|
lock (_recruitsLock)
|
||||||
|
{
|
||||||
|
var isNew = !_recruits.ContainsKey(key);
|
||||||
|
_recruits[key] = record;
|
||||||
|
|
||||||
|
if (isNew)
|
||||||
|
{
|
||||||
|
LogRecruit("START", record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash user IDs (commented out - client already sends numeric IDs)
|
||||||
|
// if (record.RecruitInfo.MechaInfo.UserIDs != null)
|
||||||
|
// {
|
||||||
|
// record.RecruitInfo.MechaInfo.UserIDs = record.RecruitInfo.MechaInfo.UserIDs
|
||||||
|
// .Select(HashUserId)
|
||||||
|
// .ToList();
|
||||||
|
// }
|
||||||
|
|
||||||
|
return Ok(new { success = true });
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
return StatusCode(ex.Code, ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error starting recruit");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("finish")]
|
||||||
|
public IActionResult FinishRecruit([FromBody] JsonElement json)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var record = JsonSerializer.Deserialize<RecruitRecord>(json.GetRawText(), _jsonOptions);
|
||||||
|
if (record == null)
|
||||||
|
{
|
||||||
|
throw new ApiException(400, "Invalid request body");
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = record.RecruitInfo.MechaInfo.IpAddress;
|
||||||
|
|
||||||
|
lock (_recruitsLock)
|
||||||
|
{
|
||||||
|
if (!_recruits.ContainsKey(key))
|
||||||
|
{
|
||||||
|
throw new ApiException(404, "Recruit not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = _recruits[key];
|
||||||
|
if (existing.Keychip != record.Keychip)
|
||||||
|
{
|
||||||
|
throw new ApiException(403, "Keychip mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
_recruits.Remove(key);
|
||||||
|
LogRecruit("FINISH", existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new { success = true });
|
||||||
|
}
|
||||||
|
catch (ApiException ex)
|
||||||
|
{
|
||||||
|
return StatusCode(ex.Code, ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error finishing recruit");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("list")]
|
||||||
|
public IActionResult GetRecruitList()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
var timeout = 30000; // 30 seconds
|
||||||
|
|
||||||
|
List<RecruitRecord> records;
|
||||||
|
|
||||||
|
lock (_recruitsLock)
|
||||||
|
{
|
||||||
|
// Remove expired records
|
||||||
|
var expiredKeys = _recruits
|
||||||
|
.Where(kvp => now - kvp.Value.Time > timeout)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var key in expiredKeys)
|
||||||
|
{
|
||||||
|
_recruits.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
records = _recruits.Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return NDJSON format (newline-delimited JSON)
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
foreach (var record in records)
|
||||||
|
{
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(record, new System.Text.Json.JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
sb.AppendLine(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Content(sb.ToString(), "text/plain");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting recruit list");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HashUserId(long userId)
|
||||||
|
{
|
||||||
|
var hash = MD5.HashData(Encoding.UTF8.GetBytes(userId.ToString()));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogRecruit(string action, RecruitRecord record)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var line = $"[{DateTimeOffset.UtcNow:yyyy-MM-dd HH:mm:ss}] {action} {record.RecruitInfo.MechaInfo.IpAddress} {record.Keychip} MusicID={record.RecruitInfo.MusicID}";
|
||||||
|
|
||||||
|
lock (_recruitsLock)
|
||||||
|
{
|
||||||
|
System.IO.File.AppendAllText(_logFilePath, line + Environment.NewLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(line);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error writing to recruit log");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Api/InfoController.cs
Normal file
39
Api/InfoController.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CatLink.Api
|
||||||
|
{
|
||||||
|
public class RelayInfoResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("relayHost")]
|
||||||
|
public string RelayHost { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("relayPort")]
|
||||||
|
public int RelayPort { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("info")]
|
||||||
|
public class InfoController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public InfoController(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult GetInfo()
|
||||||
|
{
|
||||||
|
var host = _configuration.GetValue<string>("Host") ?? "localhost";
|
||||||
|
var relayPort = _configuration.GetValue<int>("RelayPort");
|
||||||
|
|
||||||
|
return Ok(new RelayInfoResponse
|
||||||
|
{
|
||||||
|
RelayHost = host,
|
||||||
|
RelayPort = relayPort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
CatLink.csproj
Normal file
33
CatLink.csproj
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="obj\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Remove="obj\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Remove="obj\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="obj\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Project>
|
||||||
12
Models/ApiException.cs
Normal file
12
Models/ApiException.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace CatLink.Models
|
||||||
|
{
|
||||||
|
public class ApiException : Exception
|
||||||
|
{
|
||||||
|
public int Code { get; }
|
||||||
|
|
||||||
|
public ApiException(int code, string message) : base(message)
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Models/Command.cs
Normal file
15
Models/Command.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace CatLink.Models
|
||||||
|
{
|
||||||
|
public static class Command
|
||||||
|
{
|
||||||
|
public const int CTL_START = 1;
|
||||||
|
public const int CTL_BIND = 2;
|
||||||
|
public const int CTL_HEARTBEAT = 3;
|
||||||
|
public const int CTL_TCP_CONNECT = 4;
|
||||||
|
public const int CTL_TCP_ACCEPT = 5;
|
||||||
|
public const int CTL_TCP_ACCEPT_ACK = 6;
|
||||||
|
public const int CTL_TCP_CLOSE = 7;
|
||||||
|
public const int DATA_SEND = 21;
|
||||||
|
public const int DATA_BROADCAST = 22;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Models/MechaInfo.cs
Normal file
43
Models/MechaInfo.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CatLink.Models
|
||||||
|
{
|
||||||
|
public class MechaInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("IsJoin")]
|
||||||
|
public bool IsJoin { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("IpAddress")]
|
||||||
|
public uint IpAddress { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("MusicID")]
|
||||||
|
public int MusicID { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("Entrys")]
|
||||||
|
public List<bool> Entrys { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("UserIDs")]
|
||||||
|
public List<long> UserIDs { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("UserNames")]
|
||||||
|
public List<string> UserNames { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("IconIDs")]
|
||||||
|
public List<int> IconIDs { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("FumenDifs")]
|
||||||
|
public List<int> FumenDifs { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("Rateing")]
|
||||||
|
public List<double> Rateing { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("ClassValue")]
|
||||||
|
public List<int> ClassValue { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("MaxClassValue")]
|
||||||
|
public List<int> MaxClassValue { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("UserType")]
|
||||||
|
public List<int> UserType { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
59
Models/Msg.cs
Normal file
59
Models/Msg.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
namespace CatLink.Models
|
||||||
|
{
|
||||||
|
public class Msg
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
public int Cmd { get; set; }
|
||||||
|
public int Proto { get; set; }
|
||||||
|
public int Sid { get; set; }
|
||||||
|
public uint Src { get; set; }
|
||||||
|
public ushort SrcPort { get; set; }
|
||||||
|
public uint Dst { get; set; }
|
||||||
|
public ushort DstPort { get; set; }
|
||||||
|
public string? Data { get; set; }
|
||||||
|
|
||||||
|
public static Msg FromString(string line)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Line cannot be null or empty", nameof(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = line.Split(',');
|
||||||
|
var msg = new Msg();
|
||||||
|
|
||||||
|
if (parts.Length > 0 && !string.IsNullOrWhiteSpace(parts[0])) msg.Version = int.Parse(parts[0]);
|
||||||
|
if (parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1])) msg.Cmd = int.Parse(parts[1]);
|
||||||
|
if (parts.Length > 2 && !string.IsNullOrWhiteSpace(parts[2])) msg.Proto = int.Parse(parts[2]);
|
||||||
|
if (parts.Length > 3 && !string.IsNullOrWhiteSpace(parts[3])) msg.Sid = int.Parse(parts[3]);
|
||||||
|
if (parts.Length > 4 && !string.IsNullOrWhiteSpace(parts[4])) msg.Src = uint.Parse(parts[4]);
|
||||||
|
if (parts.Length > 5 && !string.IsNullOrWhiteSpace(parts[5])) msg.SrcPort = ushort.Parse(parts[5]);
|
||||||
|
if (parts.Length > 6 && !string.IsNullOrWhiteSpace(parts[6])) msg.Dst = uint.Parse(parts[6]);
|
||||||
|
if (parts.Length > 7 && !string.IsNullOrWhiteSpace(parts[7])) msg.DstPort = ushort.Parse(parts[7]);
|
||||||
|
if (parts.Length > 16 && !string.IsNullOrWhiteSpace(parts[16])) msg.Data = parts[16];
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Version},{Cmd},{Proto},{Sid},{Src},{SrcPort},{Dst},{DstPort},,,,,,,,,,{Data}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Msg CloneWithNewData(string? newData)
|
||||||
|
{
|
||||||
|
return new Msg
|
||||||
|
{
|
||||||
|
Version = Version,
|
||||||
|
Cmd = Cmd,
|
||||||
|
Proto = Proto,
|
||||||
|
Sid = Sid,
|
||||||
|
Src = Src,
|
||||||
|
SrcPort = SrcPort,
|
||||||
|
Dst = Dst,
|
||||||
|
DstPort = DstPort,
|
||||||
|
Data = newData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Models/Proto.cs
Normal file
8
Models/Proto.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CatLink.Models
|
||||||
|
{
|
||||||
|
public static class Proto
|
||||||
|
{
|
||||||
|
public const int TCP = 6;
|
||||||
|
public const int UDP = 17;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Models/RecruitInfo.cs
Normal file
31
Models/RecruitInfo.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CatLink.Models
|
||||||
|
{
|
||||||
|
public class RecruitInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("MechaInfo")]
|
||||||
|
public MechaInfo MechaInfo { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("MusicID")]
|
||||||
|
public int MusicID { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("GroupID")]
|
||||||
|
public int GroupID { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("EventModeID")]
|
||||||
|
public bool EventModeID { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("JoinNumber")]
|
||||||
|
public int JoinNumber { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("PartyStance")]
|
||||||
|
public int PartyStance { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("_startTimeTicks")]
|
||||||
|
public long StartTimeTicks { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("_recvTimeTicks")]
|
||||||
|
public long RecvTimeTicks { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Models/RecruitRecord.cs
Normal file
19
Models/RecruitRecord.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CatLink.Models
|
||||||
|
{
|
||||||
|
public class RecruitRecord
|
||||||
|
{
|
||||||
|
[JsonPropertyName("RecruitInfo")]
|
||||||
|
public RecruitInfo RecruitInfo { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("Keychip")]
|
||||||
|
public string Keychip { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("Server")]
|
||||||
|
public RelayServerInfo Server { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("Time")]
|
||||||
|
public long Time { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Models/RelayServerInfo.cs
Normal file
19
Models/RelayServerInfo.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CatLink.Models
|
||||||
|
{
|
||||||
|
public class RelayServerInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("addr")]
|
||||||
|
public string Addr { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("port")]
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("official")]
|
||||||
|
public bool Official { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Program.cs
Normal file
49
Program.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using CatLink.Relay;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure middleware
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Get configuration
|
||||||
|
var httpPort = builder.Configuration.GetValue<int>("HttpPort", 20100);
|
||||||
|
var relayPort = builder.Configuration.GetValue<int>("RelayPort", 20101);
|
||||||
|
|
||||||
|
var logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||||
|
|
||||||
|
// Start TCP relay server
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var relayTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var relay = new FutariRelay(relayPort, cts.Token, app.Services.GetRequiredService<ILogger<FutariRelay>>(), app.Services);
|
||||||
|
await relay.StartAsync();
|
||||||
|
}, cts.Token);
|
||||||
|
|
||||||
|
logger.LogInformation("CatLink Relay Server starting...");
|
||||||
|
logger.LogInformation("HTTP API listening on port {HttpPort}", httpPort);
|
||||||
|
logger.LogInformation("TCP Relay listening on port {RelayPort}", relayPort);
|
||||||
|
|
||||||
|
// Handle shutdown
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
|
||||||
|
{
|
||||||
|
logger.LogInformation("Shutting down...");
|
||||||
|
cts.Cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the web server
|
||||||
|
app.Run($"http://0.0.0.0:{httpPort}");
|
||||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# CatLink
|
||||||
|
|
||||||
|
街机游戏联机中继服务器的 .NET 版本实现。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- **TCP 中继服务器**:处理客户端连接和数据转发
|
||||||
|
- **HTTP API 服务器**:提供招募管理和服务器信息接口
|
||||||
|
- **协议支持**:兼容原版 Worldlink 协议
|
||||||
|
|
||||||
|
## 端口配置
|
||||||
|
|
||||||
|
- HTTP API: 20100
|
||||||
|
- TCP 中继: 20101
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 招募管理
|
||||||
|
|
||||||
|
- `POST /recruit/start` - 开始招募
|
||||||
|
- `POST /recruit/finish` - 结束招募
|
||||||
|
- `GET /recruit/list` - 获取招募列表
|
||||||
|
|
||||||
|
### 服务器信息
|
||||||
|
|
||||||
|
- `GET /info` - 获取服务器信息
|
||||||
|
- `GET /debug` - 获取调试信息
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
CatLink/
|
||||||
|
├── Models/ # 数据模型
|
||||||
|
├── Relay/ # TCP 中继服务器
|
||||||
|
├── Api/ # HTTP API 控制器
|
||||||
|
└── Utils/ # 工具类
|
||||||
|
```
|
||||||
106
Relay/ActiveClient.cs
Normal file
106
Relay/ActiveClient.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using CatLink.Models;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CatLink.Relay
|
||||||
|
{
|
||||||
|
public class ActiveClient
|
||||||
|
{
|
||||||
|
private readonly ILogger<ActiveClient> _logger;
|
||||||
|
private readonly Socket _socket;
|
||||||
|
private readonly StreamReader _reader;
|
||||||
|
private readonly StreamWriter _writer;
|
||||||
|
private readonly object _writeLock = new();
|
||||||
|
private readonly Thread _thread;
|
||||||
|
private readonly Action<ActiveClient, Msg> _messageHandler;
|
||||||
|
private readonly Action<ActiveClient> _disconnectHandler;
|
||||||
|
|
||||||
|
public string ClientKey { get; }
|
||||||
|
public uint StubIp { get; }
|
||||||
|
public Dictionary<int, uint> TcpStreams { get; } = new();
|
||||||
|
public HashSet<int> PendingStreams { get; } = new();
|
||||||
|
public long LastHeartbeat { get; set; }
|
||||||
|
|
||||||
|
public ActiveClient(
|
||||||
|
string clientKey,
|
||||||
|
uint stubIp,
|
||||||
|
Socket socket,
|
||||||
|
Action<ActiveClient, Msg> messageHandler,
|
||||||
|
Action<ActiveClient> disconnectHandler,
|
||||||
|
ILogger<ActiveClient> logger)
|
||||||
|
{
|
||||||
|
ClientKey = clientKey;
|
||||||
|
StubIp = stubIp;
|
||||||
|
_socket = socket;
|
||||||
|
_messageHandler = messageHandler;
|
||||||
|
_disconnectHandler = disconnectHandler;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
var stream = new NetworkStream(socket);
|
||||||
|
_reader = new StreamReader(stream, Encoding.UTF8);
|
||||||
|
_writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
|
||||||
|
|
||||||
|
LastHeartbeat = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
_thread = new Thread(HandleMessages)
|
||||||
|
{
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMessages()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (_socket.Connected)
|
||||||
|
{
|
||||||
|
var line = _reader.ReadLine();
|
||||||
|
if (line == null) break;
|
||||||
|
|
||||||
|
var msg = Msg.FromString(line);
|
||||||
|
_messageHandler(this, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error handling messages for client {ClientKey}", ClientKey);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_disconnectHandler(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(Msg msg)
|
||||||
|
{
|
||||||
|
lock (_writeLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_writer.WriteLine(msg.ToString());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error sending message to client {ClientKey}", ClientKey);
|
||||||
|
Disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_socket.Close();
|
||||||
|
_reader.Dispose();
|
||||||
|
_writer.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error disconnecting client {ClientKey}", ClientKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
306
Relay/FutariRelay.cs
Normal file
306
Relay/FutariRelay.cs
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using CatLink.Models;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CatLink.Relay
|
||||||
|
{
|
||||||
|
public class FutariRelay
|
||||||
|
{
|
||||||
|
private readonly ILogger<FutariRelay> _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly int _port;
|
||||||
|
private readonly CancellationToken _cancellationToken;
|
||||||
|
private readonly Dictionary<uint, ActiveClient> _clients = new();
|
||||||
|
private readonly object _clientsLock = new();
|
||||||
|
|
||||||
|
public FutariRelay(int port, CancellationToken cancellationToken, ILogger<FutariRelay> logger, IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_port = port;
|
||||||
|
_cancellationToken = cancellationToken;
|
||||||
|
_logger = logger;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
{
|
||||||
|
var listener = new TcpListener(IPAddress.Any, _port);
|
||||||
|
listener.Start();
|
||||||
|
|
||||||
|
_logger.LogInformation("TCP Relay server started on port {Port}", _port);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!_cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var socket = await listener.AcceptSocketAsync(_cancellationToken);
|
||||||
|
_ = Task.Run(() => HandleClientAsync(socket), _cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listener.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleClientAsync(Socket socket)
|
||||||
|
{
|
||||||
|
socket.ReceiveTimeout = 20000;
|
||||||
|
socket.NoDelay = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new NetworkStream(socket);
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
using var writer = new StreamWriter(stream) { AutoFlush = true };
|
||||||
|
|
||||||
|
var firstLine = await reader.ReadLineAsync();
|
||||||
|
if (string.IsNullOrEmpty(firstLine) || string.IsNullOrWhiteSpace(firstLine))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Received empty line from client");
|
||||||
|
socket.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = Msg.FromString(firstLine);
|
||||||
|
|
||||||
|
if (msg.Cmd != Command.CTL_START)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("First message was not CTL_START");
|
||||||
|
socket.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientKey = msg.Data;
|
||||||
|
if (string.IsNullOrEmpty(clientKey))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Client key is empty");
|
||||||
|
socket.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stubIp = KeychipToStubIp(clientKey);
|
||||||
|
|
||||||
|
var clientLogger = _serviceProvider.GetRequiredService<ILogger<ActiveClient>>();
|
||||||
|
var client = new ActiveClient(
|
||||||
|
clientKey,
|
||||||
|
stubIp,
|
||||||
|
socket,
|
||||||
|
HandleMessage,
|
||||||
|
HandleDisconnect,
|
||||||
|
clientLogger
|
||||||
|
);
|
||||||
|
|
||||||
|
lock (_clientsLock)
|
||||||
|
{
|
||||||
|
if (_clients.ContainsKey(stubIp))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Client with stub IP {StubIp} already exists", stubIp);
|
||||||
|
client.Disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clients[stubIp] = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Client registered: {ClientKey} -> {StubIp}", clientKey, stubIp);
|
||||||
|
|
||||||
|
// Send version confirmation
|
||||||
|
client.Send(new Msg
|
||||||
|
{
|
||||||
|
Cmd = Command.CTL_START,
|
||||||
|
Data = "version=1"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error handling client connection");
|
||||||
|
socket.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMessage(ActiveClient client, Msg msg)
|
||||||
|
{
|
||||||
|
switch (msg.Cmd)
|
||||||
|
{
|
||||||
|
case Command.CTL_HEARTBEAT:
|
||||||
|
client.LastHeartbeat = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
client.Send(new Msg { Cmd = Command.CTL_HEARTBEAT });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Command.CTL_TCP_CONNECT:
|
||||||
|
HandleTcpConnect(client, msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Command.CTL_TCP_ACCEPT:
|
||||||
|
HandleTcpAccept(client, msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Command.DATA_SEND:
|
||||||
|
HandleDataSend(client, msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Command.DATA_BROADCAST:
|
||||||
|
HandleDataBroadcast(client, msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Command.CTL_TCP_CLOSE:
|
||||||
|
HandleTcpClose(client, msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleTcpConnect(ActiveClient client, Msg msg)
|
||||||
|
{
|
||||||
|
ActiveClient? target;
|
||||||
|
lock (_clientsLock)
|
||||||
|
{
|
||||||
|
_clients.TryGetValue(msg.Dst, out target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("TCP connect: target not found {Dst}", msg.Dst);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.TcpStreams.ContainsKey(msg.Sid) || client.PendingStreams.Contains(msg.Sid))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("TCP connect: stream ID already in use {Sid}", msg.Sid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.PendingStreams.Add(msg.Sid);
|
||||||
|
|
||||||
|
var forwardMsg = msg.CloneWithNewData(msg.Data);
|
||||||
|
forwardMsg.Src = client.StubIp;
|
||||||
|
target.Send(forwardMsg);
|
||||||
|
|
||||||
|
_logger.LogDebug("TCP connect: {Src} -> {Dst}, stream {Sid}", client.StubIp, msg.Dst, msg.Sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleTcpAccept(ActiveClient client, Msg msg)
|
||||||
|
{
|
||||||
|
ActiveClient? target;
|
||||||
|
lock (_clientsLock)
|
||||||
|
{
|
||||||
|
_clients.TryGetValue(msg.Dst, out target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("TCP accept: target not found {Dst}", msg.Dst);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.PendingStreams.Contains(msg.Sid))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("TCP accept: stream not in pending {Sid}", msg.Sid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.PendingStreams.Remove(msg.Sid);
|
||||||
|
|
||||||
|
// Establish bidirectional stream mapping
|
||||||
|
target.TcpStreams[msg.Sid] = client.StubIp;
|
||||||
|
client.TcpStreams[msg.Sid] = target.StubIp;
|
||||||
|
|
||||||
|
var forwardMsg = msg.CloneWithNewData(msg.Data);
|
||||||
|
forwardMsg.Src = client.StubIp;
|
||||||
|
target.Send(forwardMsg);
|
||||||
|
|
||||||
|
_logger.LogDebug("TCP accept: {Src} <-> {Dst}, stream {Sid}", client.StubIp, msg.Dst, msg.Sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDataSend(ActiveClient client, Msg msg)
|
||||||
|
{
|
||||||
|
if (!client.TcpStreams.TryGetValue(msg.Sid, out var targetStubIp))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Data send: stream not found {Sid}", msg.Sid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveClient? target;
|
||||||
|
lock (_clientsLock)
|
||||||
|
{
|
||||||
|
_clients.TryGetValue(targetStubIp, out target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Data send: target not found {TargetStubIp}", targetStubIp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var forwardMsg = msg.CloneWithNewData(msg.Data);
|
||||||
|
forwardMsg.Src = client.StubIp;
|
||||||
|
forwardMsg.Dst = target.StubIp;
|
||||||
|
target.Send(forwardMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDataBroadcast(ActiveClient client, Msg msg)
|
||||||
|
{
|
||||||
|
List<ActiveClient> clientsCopy;
|
||||||
|
lock (_clientsLock)
|
||||||
|
{
|
||||||
|
clientsCopy = _clients.Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var forwardMsg = msg.CloneWithNewData(msg.Data);
|
||||||
|
forwardMsg.Src = client.StubIp;
|
||||||
|
|
||||||
|
foreach (var c in clientsCopy)
|
||||||
|
{
|
||||||
|
if (c.StubIp != client.StubIp)
|
||||||
|
{
|
||||||
|
c.Send(forwardMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Data broadcast from {Src}", client.StubIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleTcpClose(ActiveClient client, Msg msg)
|
||||||
|
{
|
||||||
|
if (client.TcpStreams.TryGetValue(msg.Sid, out var targetStubIp))
|
||||||
|
{
|
||||||
|
client.TcpStreams.Remove(msg.Sid);
|
||||||
|
|
||||||
|
ActiveClient? target;
|
||||||
|
lock (_clientsLock)
|
||||||
|
{
|
||||||
|
_clients.TryGetValue(targetStubIp, out target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target != null)
|
||||||
|
{
|
||||||
|
target.TcpStreams.Remove(msg.Sid);
|
||||||
|
var forwardMsg = msg.CloneWithNewData(msg.Data);
|
||||||
|
forwardMsg.Src = client.StubIp;
|
||||||
|
forwardMsg.Dst = target.StubIp;
|
||||||
|
target.Send(forwardMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("TCP close: stream {Sid}", msg.Sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDisconnect(ActiveClient client)
|
||||||
|
{
|
||||||
|
lock (_clientsLock)
|
||||||
|
{
|
||||||
|
_clients.Remove(client.StubIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Client disconnected: {ClientKey}", client.ClientKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint KeychipToStubIp(string keychip)
|
||||||
|
{
|
||||||
|
var hash = MD5.HashData(Encoding.UTF8.GetBytes(keychip));
|
||||||
|
return (uint)((hash[0] << 24) | (hash[1] << 16) | (hash[2] << 8) | hash[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
appsettings.json
Normal file
12
appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Host": "10.0.0.103",
|
||||||
|
"HttpPort": 20100,
|
||||||
|
"RelayPort": 20101
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user